diff --git a/.gitmessage.txt b/.gitmessage.txt new file mode 100644 index 00000000000..18e39abc3d9 --- /dev/null +++ b/.gitmessage.txt @@ -0,0 +1,4 @@ + + +Co-authored-by: Hyunguk Ryu + diff --git a/build.gradle b/build.gradle index 3697236c6fb..20ad08a5a5e 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,8 @@ dependencies { testImplementation platform('org.assertj:assertj-bom:3.25.1') testImplementation('org.junit.jupiter:junit-jupiter') testImplementation('org.assertj:assertj-core') + + runtimeOnly("com.mysql:mysql-connector-j:8.3.0") } java { diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..8ad0f4d0ad9 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,63 @@ +## 프로그램 흐름 + +- 사용자로부터 게임 시작 여부를 입력받는다. + - `start`를 입력받으면 게임을 생성하고 초기 체스판의 상태를 출력한다. + - `end`를 입력받으면 프로그램을 종료한다. +- 기물 이동 여부를 입력받는다. + - `move b2 b4`와 같이 입력받으면 `b2`에 존재하는 기물을 `b4`로 이동시킨다. + - `end`를 입력받으면 프로그램을 종료한다. + +## 도메인별 기능 + +### Piece + +- [x] 출발지 & 목적지 정보를 토대로 기물이 이동할 수 있는지 검사한다. +- [x] 입력받은 색상을 토대로 자신의 적 여부를 반환한다. + +#### Rook + +- 상/하/좌/우 거리 제한 없이 이동 가능 +- 비었거나 적 기물이 존재하는 칸으로만 이동 가능 +- 이동 경로에 기물이 존재하면 이동 불가 + +#### Bishop + +- 대각선 방향 거리 제한 없이 이동 가능 +- 비었거나 적 기물이 존재하는 칸으로만 이동 가능 +- 이동 경로에 기물이 존재하면 이동 불가 + +#### Queen + +- 상/하/좌/우, 대각선 방향 거리 제한 없이 이동 가능 +- 비었거나 적 기물이 존재하는 칸으로만 이동 가능 +- 이동 경로에 기물이 존재하면 이동 불가 + +#### King + +- 상/하/좌/우, 대각선 방향 중 한 칸만 이동 가능 +- 비었거나 적 기물이 존재하는 칸으로만 이동 가능 + +#### Knight + +- 상/하/좌/우 중 한 칸 이동 후 전진 방향의 대각석으로 한 칸 이동 가능 +- 비었거나 적 기물이 존재하는 칸으로만 이동 가능 +- 이동 경로에 기물이 존재해도 이동 가능 + +#### Pawn + +- 특정 조건에 따라 이동 선택 + - 기본적으로 한 칸 전진 가능 + - 출발지가 초기 위치일 경우 두 칸 전진 가능 + - 전진 경로 혹은 목적지에 기물이 존재하면 이동 불가 + - 전진 방향 대각선 한 칸에 적 기물이 존재시 이동 가능 + +## Board + +- [x] 특정 색상의 기물 이동 + - `출발지` == `도착지`인 경우 예외 처리 + - 출발지에 기물이 존재하지 않으면 예외 처리 + - 상대 색상의 기물을 이동시키려 할 시 예외 처리 + +## ChessGame + +- [x] `흰색` 플레이어와 `검은색` 플레이어의 게임 순서 관리 및 `Board`에 기물 이동 요청 전달 diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000000..a3d60303428 --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,30 @@ +import controller.GameController; +import dao.ProductionTurnColorDao; +import dao.ProductionPieceDao; +import database.JdbcConnectionPool; +import domain.board.Board; +import domain.board.BoardInitializer; +import domain.board.Position; +import domain.game.ChessGame; +import domain.piece.Piece; +import view.InputView; +import view.OutputView; + +import java.util.Map; + +public class Application { + public static void main(String[] args) { + JdbcConnectionPool connectionPool = JdbcConnectionPool.getInstance(); + Map initialPiecePositions = BoardInitializer.initBoard(); + ProductionPieceDao productionPieceDao = new ProductionPieceDao(connectionPool); + Board board = new Board(productionPieceDao, initialPiecePositions); + + ProductionTurnColorDao productionPieceColorDao = new ProductionTurnColorDao(connectionPool); + ChessGame chessGame = new ChessGame(productionPieceColorDao, board); + + GameController gameController = new GameController(new InputView(), new OutputView(), chessGame); + gameController.run(); + + connectionPool.close(); + } +} diff --git a/src/main/java/controller/GameController.java b/src/main/java/controller/GameController.java new file mode 100644 index 00000000000..49d54a3e639 --- /dev/null +++ b/src/main/java/controller/GameController.java @@ -0,0 +1,107 @@ +package controller; + +import domain.board.Position; +import domain.game.ChessGame; +import domain.game.GameCommand; +import domain.game.GameCommandType; +import domain.game.GameScore; +import domain.game.GameStatus; +import domain.piece.PieceColor; +import dto.BoardDto; +import view.InputView; +import view.OutputView; + +public class GameController { + private final InputView inputView; + private final OutputView outputView; + private final ChessGame chessGame; + + public GameController(final InputView inputView, final OutputView outputView, final ChessGame chessGame) { + this.inputView = inputView; + this.outputView = outputView; + this.chessGame = chessGame; + } + + public void run() { + initGame(); + + if (chessGame.isRunning()) { + start(); + } + } + + private void initGame() { + try { + GameCommand gameCommand = inputCommand(); + gameCommand.execute(this); + } catch (Exception e) { + outputView.printErrorMessage(e.getMessage()); + initGame(); + } + } + + private GameCommand inputCommand() { + String[] inputValues = inputView.inputCommand().split(" "); + return GameCommandType.of(inputValues); + } + + public void buildGame() { + if (chessGame.existPrevGame()) { + outputView.printInputRoadGameMessage(); + GameCommand gameCommand = inputCommand(); + gameCommand.execute(this); + return; + } + createChessGame(); + start(); + } + + public void createChessGame() { + chessGame.createChessGame(); + } + + public void roadPrevGame() { + chessGame.roadPrevGame(); + } + + public void start() { + chessGame.gameStart(); + outputView.printWelcomeMessage(); + while (chessGame.isRunning()) { + BoardDto boardDto = BoardDto.from(chessGame.piecePositions()); + PieceColor currentPlayTeamColor = chessGame.currentPlayTeamColor(); + outputView.printTurnStatus(boardDto, currentPlayTeamColor); + playTurn(); + } + } + + private void playTurn() { + try { + GameCommand gameCommand = inputCommand(); + gameCommand.execute(this); + } catch (Exception e) { + outputView.printErrorMessage(e.getMessage()); + playTurn(); + } + } + + public void movePiece(final Position source, final Position destination) { + chessGame.movePiece(source, destination); + } + + public GameStatus gameStatus() { + return chessGame.gameStatus(); + } + + public void end() { + chessGame.gameEnd(); + } + + public void printGameStatus() { + GameScore gameScore = chessGame.getGameResult(); + outputView.printGameResult( + gameScore.whiteTeamScore(), + gameScore.blackTeamScore(), + gameScore.gameResult()); + } +} diff --git a/src/main/java/dao/PieceDao.java b/src/main/java/dao/PieceDao.java new file mode 100644 index 00000000000..154bd61ffd5 --- /dev/null +++ b/src/main/java/dao/PieceDao.java @@ -0,0 +1,21 @@ +package dao; + +import domain.board.File; +import domain.board.Rank; + +import java.util.List; + +public interface PieceDao { + + List findAll(); + + boolean existPiecePositions(); + + void save(PieceEntity piece); + + void update(File sourceFile, Rank sourceRank, File destinationFile, Rank destinationRank); + + void delete(File file, Rank rank); + + void deleteAll(); +} diff --git a/src/main/java/dao/PieceEntity.java b/src/main/java/dao/PieceEntity.java new file mode 100644 index 00000000000..4a593d110f2 --- /dev/null +++ b/src/main/java/dao/PieceEntity.java @@ -0,0 +1,9 @@ +package dao; + +import domain.board.File; +import domain.board.Rank; +import domain.piece.PieceColor; +import domain.piece.PieceType; + +public record PieceEntity(PieceType pieceType, PieceColor pieceColor, File file, Rank rank) { +} diff --git a/src/main/java/dao/ProductionPieceDao.java b/src/main/java/dao/ProductionPieceDao.java new file mode 100644 index 00000000000..d444241e20b --- /dev/null +++ b/src/main/java/dao/ProductionPieceDao.java @@ -0,0 +1,146 @@ +package dao; + +import database.JdbcConnectionPool; +import domain.board.File; +import domain.board.Rank; +import domain.piece.PieceColor; +import domain.piece.PieceType; + +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; + +public class ProductionPieceDao implements PieceDao { + private static final int PIECE_COUNT_INDEX = 1; + private static final int FILE_INDEX = 1; + private static final int RANK_INDEX = 2; + private static final int PIECE_TYPE_INDEX = 3; + private static final int PIECE_COLOR_INDEX = 4; + private static final int DESTINATION_FILE_INDEX = 3; + private static final int DESTINATION_RANK_INDEX = 4; + + private final JdbcConnectionPool connectionPool; + + public ProductionPieceDao(final JdbcConnectionPool connectionPool) { + this.connectionPool = connectionPool; + } + + @Override + public List findAll() { + final String query = "SELECT * FROM piece"; + Connection connection = connectionPool.getConnection(); + + try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + ResultSet resultSet = preparedStatement.executeQuery(); + return createPieceEntities(resultSet); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + connectionPool.releaseConnection(connection); + } + } + + private List createPieceEntities(final ResultSet resultSet) throws SQLException { + List pieceEntities = new ArrayList<>(); + while (resultSet.next()) { + File file = File.of(resultSet.getString("file")); + Rank rank = Rank.of(resultSet.getString("rank")); + PieceType type = PieceType.of(resultSet.getString("type")); + PieceColor color = PieceColor.of(resultSet.getString("color")); + + pieceEntities.add(new PieceEntity(type, color, file, rank)); + } + + return pieceEntities; + } + + @Override + public boolean existPiecePositions() { + final String query = "SELECT COUNT(*) FROM piece"; + Connection connection = connectionPool.getConnection(); + + try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + ResultSet resultSet = preparedStatement.executeQuery(); + if (resultSet.next()) { + int pieceCount = resultSet.getInt(PIECE_COUNT_INDEX); + return pieceCount > 0; + } + + return false; + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + connectionPool.releaseConnection(connection); + } + } + + @Override + public void save(final PieceEntity piece) { + final String query = "INSERT INTO piece (file, `rank`, type, color) VALUES (?, ?, ?, ?)"; + Connection connection = connectionPool.getConnection(); + + try (final PreparedStatement preparedStatement = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) { + preparedStatement.setString(FILE_INDEX, piece.file().name()); + preparedStatement.setInt(RANK_INDEX, piece.rank().value()); + preparedStatement.setString(PIECE_TYPE_INDEX, piece.pieceType().name()); + preparedStatement.setString(PIECE_COLOR_INDEX, piece.pieceColor().name()); + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + connectionPool.releaseConnection(connection); + } + } + + @Override + public void update(final File sourceFile, final Rank sourceRank, final File destinationFile, final Rank destinationRank) { + final String query = "UPDATE piece SET file=?, `rank`=? WHERE file=? AND `rank`=?"; + Connection connection = connectionPool.getConnection(); + + try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setString(FILE_INDEX, destinationFile.name()); + preparedStatement.setInt(RANK_INDEX, destinationRank.value()); + preparedStatement.setString(DESTINATION_FILE_INDEX, sourceFile.name()); + preparedStatement.setInt(DESTINATION_RANK_INDEX, sourceRank.value()); + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + connectionPool.releaseConnection(connection); + } + } + + @Override + public void delete(final File file, final Rank rank) { + final String query = "DELETE FROM piece WHERE file=? AND `rank`=?"; + Connection connection = connectionPool.getConnection(); + + try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setString(FILE_INDEX, file.name()); + preparedStatement.setInt(RANK_INDEX, rank.value()); + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + connectionPool.releaseConnection(connection); + } + } + + @Override + public void deleteAll() { + final String query = "DELETE FROM piece"; + Connection connection = connectionPool.getConnection(); + + try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.execute(); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + connectionPool.releaseConnection(connection); + } + } +} diff --git a/src/main/java/dao/ProductionTurnColorDao.java b/src/main/java/dao/ProductionTurnColorDao.java new file mode 100644 index 00000000000..a51177661c8 --- /dev/null +++ b/src/main/java/dao/ProductionTurnColorDao.java @@ -0,0 +1,91 @@ +package dao; + +import database.JdbcConnectionPool; +import domain.piece.PieceColor; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class ProductionTurnColorDao implements TurnColorDao { + private static final int PIECE_COLOR_INDEX = 2; + + private final JdbcConnectionPool connectionPool; + + public ProductionTurnColorDao(final JdbcConnectionPool connectionPool) { + this.connectionPool = connectionPool; + } + + @Override + public PieceColor find() { + final String query = "SELECT * FROM current_player_color"; + Connection connection = connectionPool.getConnection(); + + try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + ResultSet resultSet = preparedStatement.executeQuery(); + return convertPieceColor(resultSet); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + connectionPool.releaseConnection(connection); + } + } + + @Override + public void save(final PieceColor playerColor) { + final String query = "INSERT INTO current_player_color (player_color) VALUES (?)"; + Connection connection = connectionPool.getConnection(); + + try (final PreparedStatement preparedStatement = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) { + preparedStatement.setString(1, playerColor.name()); + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + connectionPool.releaseConnection(connection); + } + } + + private PieceColor convertPieceColor(final ResultSet resultSet) throws SQLException { + if (!existData(resultSet)) { + throw new IllegalArgumentException("현재 플레이어의 색상 정보가 존재하지 않습니다."); + } + + return PieceColor.of(resultSet.getString(PIECE_COLOR_INDEX)); + } + + private boolean existData(final ResultSet resultSet) throws SQLException { + return resultSet.next(); + } + + @Override + public void update(final PieceColor changeColor) { + final String query = "UPDATE current_player_color SET player_color=?"; + Connection connection = connectionPool.getConnection(); + + try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setString(1, changeColor.name()); + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + connectionPool.releaseConnection(connection); + } + } + + @Override + public void delete() { + final String query = "DELETE FROM current_player_color"; + Connection connection = connectionPool.getConnection(); + + try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.execute(); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + connectionPool.releaseConnection(connection); + } + } +} diff --git a/src/main/java/dao/TurnColorDao.java b/src/main/java/dao/TurnColorDao.java new file mode 100644 index 00000000000..a6b9c774159 --- /dev/null +++ b/src/main/java/dao/TurnColorDao.java @@ -0,0 +1,14 @@ +package dao; + +import domain.piece.PieceColor; + +public interface TurnColorDao { + + PieceColor find(); + + void save(PieceColor playerColor); + + void update(PieceColor changeColor); + + void delete(); +} diff --git a/src/main/java/database/DatabaseConfiguration.java b/src/main/java/database/DatabaseConfiguration.java new file mode 100644 index 00000000000..28fd37bcc50 --- /dev/null +++ b/src/main/java/database/DatabaseConfiguration.java @@ -0,0 +1,34 @@ +package database; + +import static config.DBCredential.DB_ID; +import static config.DBCredential.DB_PASSWORD; + +public class DatabaseConfiguration { + private static final DatabaseConfiguration INSTANCE = new DatabaseConfiguration(); + private final String MYSQL_CONNECT_URL_FORMAT = "jdbc:mysql://%s:%s/%s"; + private final String host = "localhost"; + private final String post = "3306"; + private final String database = "chess"; + private final String option = "?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true"; + private final String username = DB_ID; + private final String password = DB_PASSWORD; + + private DatabaseConfiguration() { + } + + public static DatabaseConfiguration getInstance() { + return INSTANCE; + } + + public String getUrl() { + return String.format(MYSQL_CONNECT_URL_FORMAT, host, post, database + option); + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } +} diff --git a/src/main/java/database/JdbcConnectionPool.java b/src/main/java/database/JdbcConnectionPool.java new file mode 100644 index 00000000000..1817bef61dd --- /dev/null +++ b/src/main/java/database/JdbcConnectionPool.java @@ -0,0 +1,78 @@ +package database; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +public class JdbcConnectionPool { + private static final String FAILED_INITIALIZE = "커넥션 풀 초기화에 실패했습니다."; + private static final String FAILED_TO_GET_CONNECTION = "커넥션 획득에 실패했습니다."; + private static final String FAILED_RELEASE = "커넥션 해제에 실패했습니다."; + private static final String FAILED_CLOSE = "커넥션 종료에 실패했습니다."; + private static final int INITIAL_POOL_SIZE = 2; + private static final JdbcConnectionPool INSTANCE = new JdbcConnectionPool(); + private final DatabaseConfiguration configuration; + private BlockingQueue pool; + + private JdbcConnectionPool() { + configuration = DatabaseConfiguration.getInstance(); + initializeConnectionPool(); + } + + public static JdbcConnectionPool getInstance() { + return INSTANCE; + } + + private void initializeConnectionPool() { + pool = new ArrayBlockingQueue<>(INITIAL_POOL_SIZE); + try { + for (int i = 0; i < INITIAL_POOL_SIZE; i++) { + pool.offer(createNewConnection()); + } + } catch (SQLException e) { + throw new RuntimeException(FAILED_INITIALIZE); + } + } + + private Connection createNewConnection() throws SQLException { + return DriverManager.getConnection( + configuration.getUrl(), + configuration.getUsername(), + configuration.getPassword() + ); + } + + public Connection getConnection() { + try { + if (pool.isEmpty()) { + return createNewConnection(); + } else { + return pool.take(); + } + } catch (InterruptedException | SQLException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(FAILED_TO_GET_CONNECTION); + } + } + + public void releaseConnection(final Connection connection) { + try { + pool.put(connection); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(FAILED_RELEASE); + } + } + + public void close() { + try { + for (Connection collection : pool) { + collection.close(); + } + } catch (SQLException e) { + throw new RuntimeException(FAILED_CLOSE); + } + } +} diff --git a/src/main/java/database/init.sql b/src/main/java/database/init.sql new file mode 100644 index 00000000000..b32adbadeb8 --- /dev/null +++ b/src/main/java/database/init.sql @@ -0,0 +1,15 @@ +create table current_player_color +( + id bigint auto_increment primary key, + player_color varchar(10) not null +); + +create table piece +( + type varchar(10) not null, + `rank` varchar(10) not null, + file varchar(10) not null, + color varchar(10) not null, + id int auto_increment primary key +); + diff --git a/src/main/java/domain/board/Board.java b/src/main/java/domain/board/Board.java new file mode 100644 index 00000000000..fbf272fd6b3 --- /dev/null +++ b/src/main/java/domain/board/Board.java @@ -0,0 +1,100 @@ +package domain.board; + +import dao.PieceDao; +import dao.PieceEntity; +import domain.piece.Piece; +import domain.piece.PieceColor; +import domain.piece.PieceType; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class Board { + private final PieceDao pieceDao; + private final Map piecePositions; + + public Board(final PieceDao pieceDao, final Map piecePositions) { + this.pieceDao = pieceDao; + this.piecePositions = piecePositions; + } + + public boolean existPrevPiecePositionsData() { + return pieceDao.existPiecePositions(); + } + + public void createNewPiecePositions() { + pieceDao.deleteAll(); + piecePositions.entrySet() + .stream() + .map(entry -> convertPieceEntity(entry.getValue(), entry.getKey())) + .forEach(pieceDao::save); + } + + public void roadPrevPiecePositions() { + piecePositions.clear(); + List all = pieceDao.findAll(); + all.forEach(this::addPiecePosition); + } + + private void addPiecePosition(final PieceEntity pieceEntity) { + Position position = new Position(pieceEntity.file(), pieceEntity.rank()); + Piece piece = pieceEntity.pieceType() + .createPiece(pieceEntity.pieceColor()); + piecePositions.put(position, piece); + } + + public void clear() { + pieceDao.deleteAll(); + } + + private static PieceEntity convertPieceEntity(final Piece piece, final Position position) { + return new PieceEntity(piece.pieceType(), piece.pieceColor(), position.file(), position.rank()); + } + + public void movePiece(final PieceColor pieceColor, final Position source, final Position destination) { + validatePosition(pieceColor, source, destination); + + Piece targetPiece = piecePositions.get(source); + targetPiece.move(source, destination, this); + + piecePositions.put(destination, targetPiece); + piecePositions.remove(source); + + pieceDao.delete(destination.file(), destination.rank()); + pieceDao.update(source.file(), source.rank(), destination.file(), destination.rank()); + } + + private void validatePosition(final PieceColor pieceColor, final Position source, final Position destination) { + if (source.equals(destination)) { + throw new IllegalArgumentException("출발지와 목적지가 같을 수 없습니다."); + } + + if (!piecePositions.containsKey(source)) { + throw new IllegalArgumentException("출발지에 기물이 존재하지 않습니다."); + } + + if (!piecePositions.get(source).isTeam(pieceColor)) { + throw new IllegalArgumentException("상대방의 기물을 이동시킬 수 없습니다."); + } + } + + public boolean existPiece(final Position position) { + return piecePositions.containsKey(position); + } + + public boolean existTeamColor(final Position position, final PieceColor teamColor) { + return piecePositions.get(position).isTeam(teamColor); + + } + + public Map piecePositions() { + return Collections.unmodifiableMap(piecePositions); + } + + public boolean isKingAlive(final PieceColor targetColor) { + return piecePositions.values() + .stream() + .anyMatch(piece -> piece.isTeam(targetColor) && piece.pieceType() == PieceType.KING); + } +} diff --git a/src/main/java/domain/board/BoardInitializer.java b/src/main/java/domain/board/BoardInitializer.java new file mode 100644 index 00000000000..9667c1857e4 --- /dev/null +++ b/src/main/java/domain/board/BoardInitializer.java @@ -0,0 +1,94 @@ +package domain.board; + + +import domain.piece.Bishop; +import domain.piece.King; +import domain.piece.Knight; +import domain.piece.Pawn; +import domain.piece.Piece; +import domain.piece.Queen; +import domain.piece.Rook; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static domain.board.File.A; +import static domain.board.File.B; +import static domain.board.File.C; +import static domain.board.File.D; +import static domain.board.File.E; +import static domain.board.File.F; +import static domain.board.File.G; +import static domain.board.File.H; +import static domain.board.Rank.EIGHT; +import static domain.board.Rank.ONE; +import static domain.board.Rank.SEVEN; +import static domain.board.Rank.TWO; +import static domain.piece.PieceColor.BLACK; +import static domain.piece.PieceColor.WHITE; + +public class BoardInitializer { + public static Map initBoard() { + return new HashMap<>(initPieces()); + } + + private static Map initPieces() { + final Map piecePositions = new HashMap<>(); + initKing(piecePositions); + initQueen(piecePositions); + initBishop(piecePositions); + initKnight(piecePositions); + initRook(piecePositions); + initBlackPawns(piecePositions); + initWhitePawns(piecePositions); + + return piecePositions; + } + + private static void initKing(final Map piecePositions) { + piecePositions.put(position(E, ONE), new King(WHITE)); + piecePositions.put(position(E, EIGHT), new King(BLACK)); + } + + private static void initQueen(final Map piecePositions) { + piecePositions.put(position(D, ONE), new Queen(WHITE)); + piecePositions.put(position(D, EIGHT), new Queen(BLACK)); + } + + private static void initBishop(final Map piecePositions) { + piecePositions.put(position(C, ONE), new Bishop(WHITE)); + piecePositions.put(position(F, ONE), new Bishop(WHITE)); + piecePositions.put(position(C, EIGHT), new Bishop(BLACK)); + piecePositions.put(position(F, EIGHT), new Bishop(BLACK)); + } + + private static void initKnight(final Map piecePositions) { + piecePositions.put(position(B, ONE), new Knight(WHITE)); + piecePositions.put(position(G, ONE), new Knight(WHITE)); + piecePositions.put(position(B, EIGHT), new Knight(BLACK)); + piecePositions.put(position(G, EIGHT), new Knight(BLACK)); + } + + private static void initRook(final Map piecePositions) { + piecePositions.put(position(A, ONE), new Rook(WHITE)); + piecePositions.put(position(H, ONE), new Rook(WHITE)); + piecePositions.put(position(A, EIGHT), new Rook(BLACK)); + piecePositions.put(position(H, EIGHT), new Rook(BLACK)); + + } + + private static void initWhitePawns(final Map piecePositions) { + Arrays.stream(File.values()) + .forEach(file -> piecePositions.put(position(file, TWO), new Pawn(WHITE))); + } + + private static void initBlackPawns(final Map piecePositions) { + Arrays.stream(File.values()) + .forEach(file -> piecePositions.put(position(file, SEVEN), new Pawn(BLACK))); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/main/java/domain/board/File.java b/src/main/java/domain/board/File.java new file mode 100644 index 00000000000..f4504ddfb76 --- /dev/null +++ b/src/main/java/domain/board/File.java @@ -0,0 +1,38 @@ +package domain.board; + +import java.util.Arrays; + +public enum File { + A(0), + B(1), + C(2), + D(3), + E(4), + F(5), + G(6), + H(7); + + private final int index; + + File(final int index) { + this.index = index; + } + + public static File of(final String value) { + return Arrays.stream(values()) + .filter(file -> file.name().equals(value.toUpperCase())) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 File 값입니다.")); + } + + public static File of(final int index) { + return Arrays.stream(values()) + .filter(file -> file.getIndex() == index) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 인덱스입니다.")); + } + + public int getIndex() { + return this.index; + } +} diff --git a/src/main/java/domain/board/Position.java b/src/main/java/domain/board/Position.java new file mode 100644 index 00000000000..51b2270f99f --- /dev/null +++ b/src/main/java/domain/board/Position.java @@ -0,0 +1,40 @@ +package domain.board; + +import domain.piece.MovementDirection; + +public record Position(File file, Rank rank) { + private static final int POSITIONS_VALUES_SIZE = 2; + private static final int FILE_INDEX = 0; + private static final int RANK_INDEX = 1; + + public static Position of(final String value) { + String[] positionValues = value.split(""); + validatePositionValue(positionValues); + + File file = File.of(positionValues[FILE_INDEX]); + Rank rank = Rank.of(positionValues[RANK_INDEX]); + + return new Position(file, rank); + } + + private static void validatePositionValue(final String[] values) { + if (values.length != POSITIONS_VALUES_SIZE) { + throw new IllegalArgumentException("잘못된 위치값입니다."); + } + } + + public Position next(final MovementDirection movementDirection) { + File nextFile = File.of(this.columnIndex() + movementDirection.getColumnDistance()); + Rank nextRank = Rank.of(this.rowIndex() + movementDirection.getRowDistance()); + + return new Position(nextFile, nextRank); + } + + public int rowIndex() { + return rank.getIndex(); + } + + public int columnIndex() { + return file.getIndex(); + } +} diff --git a/src/main/java/domain/board/Rank.java b/src/main/java/domain/board/Rank.java new file mode 100644 index 00000000000..32697ddf3b7 --- /dev/null +++ b/src/main/java/domain/board/Rank.java @@ -0,0 +1,52 @@ +package domain.board; + +import java.util.Arrays; + +public enum Rank { + ONE(1), + TWO(2), + THREE(3), + FOUR(4), + FIVE(5), + SIX(6), + SEVEN(7), + EIGHT(8); + + private static final int BASE_INDEX = 8; + + private final int value; + + Rank(final int value) { + this.value = value; + } + + public static Rank of(final String value) { + return Arrays.stream(Rank.values()) + .filter(rank -> rank.value() == parseNumber(value)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 Rank값입니다.")); + } + + private static int parseNumber(final String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Rank는 정수만 입력할 수 있습니다."); + } + } + + public static Rank of(final int index) { + return Arrays.stream(Rank.values()) + .filter(rank -> rank.getIndex() == index) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 인덱스입니다.")); + } + + public int value() { + return value; + } + + public int getIndex() { + return BASE_INDEX - value; + } +} diff --git a/src/main/java/domain/game/ChessGame.java b/src/main/java/domain/game/ChessGame.java new file mode 100644 index 00000000000..18c4c2a2ce4 --- /dev/null +++ b/src/main/java/domain/game/ChessGame.java @@ -0,0 +1,87 @@ +package domain.game; + +import dao.TurnColorDao; +import domain.board.Board; +import domain.board.Position; +import domain.piece.Piece; +import domain.piece.PieceColor; + +import java.util.Map; + +import static domain.piece.PieceColor.BLACK; +import static domain.piece.PieceColor.WHITE; + +public class ChessGame { + private final TurnColorDao turnColorDao; + private final Board board; + + private PieceColor currentColor = WHITE; + private GameStatus gameStatus = GameStatus.WAITING; + + public ChessGame(final TurnColorDao turnColorDao, final Board board) { + this.turnColorDao = turnColorDao; + this.board = board; + } + + public boolean existPrevGame() { + return board.existPrevPiecePositionsData(); + } + + public void createChessGame() { + board.createNewPiecePositions(); + turnColorDao.delete(); + turnColorDao.save(currentColor); + } + + public void roadPrevGame() { + board.roadPrevPiecePositions(); + currentColor = turnColorDao.find(); + } + + public void gameStart() { + gameStatus = GameStatus.RUNNING; + } + + public boolean isRunning() { + return gameStatus == GameStatus.RUNNING; + } + + public Map piecePositions() { + return board.piecePositions(); + } + + public void movePiece(final Position source, final Position destination) { + board.movePiece(currentColor, source, destination); + if (!board.isKingAlive(currentColor.toggle())) { + gameEnd(); + return; + } + + currentColor = currentColor.toggle(); + turnColorDao.update(currentColor); + } + + public void gameEnd() { + if (!board.isKingAlive(currentColor) || !board.isKingAlive(currentColor.toggle())) { + board.clear(); + turnColorDao.delete(); + } + + gameStatus = GameStatus.END; + } + + public GameStatus gameStatus() { + return gameStatus; + } + + public GameScore getGameResult() { + Score whiteTeamScore = Score.of(board, WHITE); + Score blackTeamScore = Score.of(board, BLACK); + + return new GameScore(whiteTeamScore, blackTeamScore); + } + + public PieceColor currentPlayTeamColor() { + return currentColor; + } +} diff --git a/src/main/java/domain/game/Continue.java b/src/main/java/domain/game/Continue.java new file mode 100644 index 00000000000..037f14496bf --- /dev/null +++ b/src/main/java/domain/game/Continue.java @@ -0,0 +1,16 @@ +package domain.game; + +import controller.GameController; + +public class Continue implements GameCommand { + + @Override + public void execute(final GameController gameController) { + if (gameController.gameStatus() != GameStatus.WAITING) { + throw new IllegalArgumentException("지금 실행할 수 있는 명령어가 아닙니다."); + } + + gameController.roadPrevGame(); + gameController.start(); + } +} diff --git a/src/main/java/domain/game/End.java b/src/main/java/domain/game/End.java new file mode 100644 index 00000000000..4adbd3b3590 --- /dev/null +++ b/src/main/java/domain/game/End.java @@ -0,0 +1,16 @@ +package domain.game; + +import controller.GameController; + +public class End implements GameCommand { + + @Override + public void execute(final GameController gameController) { + GameStatus gameStatus = gameController.gameStatus(); + if (gameController.gameStatus() == GameStatus.END) { + throw new IllegalArgumentException("게임이 이미 종료되었습니다."); + } + + gameController.end(); + } +} diff --git a/src/main/java/domain/game/GameCommand.java b/src/main/java/domain/game/GameCommand.java new file mode 100644 index 00000000000..14211dea2fb --- /dev/null +++ b/src/main/java/domain/game/GameCommand.java @@ -0,0 +1,7 @@ +package domain.game; + +import controller.GameController; + +public interface GameCommand { + void execute(GameController gameController); +} diff --git a/src/main/java/domain/game/GameCommandType.java b/src/main/java/domain/game/GameCommandType.java new file mode 100644 index 00000000000..be613f6a9ea --- /dev/null +++ b/src/main/java/domain/game/GameCommandType.java @@ -0,0 +1,51 @@ +package domain.game; + +import domain.board.Position; + +import java.util.Arrays; +import java.util.function.Function; + +public enum GameCommandType { + START("start", command -> new Start()), + CONTINUE("continue", command -> new Continue()), + NEW("new", command -> new New()), + MOVE("move", GameCommandType::toMove), + STATUS("status", command -> new Status()), + END("end", command -> new End()); + + private static final int COMMAND_INDEX = 0; + private static final int BEFORE_POSITION = 1; + private static final int AFTER_POSITION = 2; + private static final int COMMAND_WITH_POSITIONS_SIZE = 3; + + private final String value; + private final Function gameCommand; + + GameCommandType(final String value, final Function gameCommand) { + this.value = value; + this.gameCommand = gameCommand; + } + + public static GameCommand of(final String[] values) { + return Arrays.stream(GameCommandType.values()) + .filter(it -> it.value.equalsIgnoreCase(values[COMMAND_INDEX])) + .map(it -> it.gameCommand.apply(values)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 명령어입니다!")); + } + + private static Move toMove(final String[] values) { + validatePositionValue(values); + + Position source = Position.of(values[BEFORE_POSITION]); + Position destination = Position.of(values[AFTER_POSITION]); + + return new Move(source, destination); + } + + private static void validatePositionValue(final String[] values) { + if (values.length != COMMAND_WITH_POSITIONS_SIZE) { + throw new IllegalArgumentException("잘못된 출발지/도착지입니다."); + } + } +} diff --git a/src/main/java/domain/game/GameResult.java b/src/main/java/domain/game/GameResult.java new file mode 100644 index 00000000000..4ff5592436e --- /dev/null +++ b/src/main/java/domain/game/GameResult.java @@ -0,0 +1,17 @@ +package domain.game; + +public enum GameResult { + BLACK_WIN, WHITE_WIN, DRAW; + + public static GameResult of(final Score whiteScore, final Score blackScore) { + if (blackScore.isBigger(whiteScore)) { + return BLACK_WIN; + } + + if (whiteScore.isBigger(blackScore)) { + return WHITE_WIN; + } + + return DRAW; + } +} diff --git a/src/main/java/domain/game/GameScore.java b/src/main/java/domain/game/GameScore.java new file mode 100644 index 00000000000..0e1b5d35edd --- /dev/null +++ b/src/main/java/domain/game/GameScore.java @@ -0,0 +1,8 @@ +package domain.game; + +public record GameScore(Score whiteTeamScore, Score blackTeamScore) { + + public GameResult gameResult() { + return GameResult.of(whiteTeamScore, blackTeamScore); + } +} diff --git a/src/main/java/domain/game/GameStatus.java b/src/main/java/domain/game/GameStatus.java new file mode 100644 index 00000000000..97b3de94418 --- /dev/null +++ b/src/main/java/domain/game/GameStatus.java @@ -0,0 +1,5 @@ +package domain.game; + +public enum GameStatus { + WAITING, RUNNING, END; +} diff --git a/src/main/java/domain/game/Move.java b/src/main/java/domain/game/Move.java new file mode 100644 index 00000000000..d42c24a60fa --- /dev/null +++ b/src/main/java/domain/game/Move.java @@ -0,0 +1,23 @@ +package domain.game; + +import controller.GameController; +import domain.board.Position; + +public class Move implements GameCommand { + private final Position source; + private final Position destination; + + public Move(final Position source, final Position destination) { + this.source = source; + this.destination = destination; + } + + @Override + public void execute(final GameController gameController) { + if (gameController.gameStatus() != GameStatus.RUNNING) { + throw new IllegalArgumentException("아직 게임이 시작되지 않았습니다."); + } + + gameController.movePiece(source, destination); + } +} diff --git a/src/main/java/domain/game/New.java b/src/main/java/domain/game/New.java new file mode 100644 index 00000000000..c9a3c7a803b --- /dev/null +++ b/src/main/java/domain/game/New.java @@ -0,0 +1,16 @@ +package domain.game; + +import controller.GameController; + +public class New implements GameCommand { + + @Override + public void execute(final GameController gameController) { + if (gameController.gameStatus() != GameStatus.WAITING) { + throw new IllegalArgumentException("지금 실행할 수 있는 명령어가 아닙니다."); + } + + gameController.createChessGame(); + gameController.start(); + } +} diff --git a/src/main/java/domain/game/Score.java b/src/main/java/domain/game/Score.java new file mode 100644 index 00000000000..b2ee2095962 --- /dev/null +++ b/src/main/java/domain/game/Score.java @@ -0,0 +1,61 @@ +package domain.game; + +import domain.board.Board; +import domain.board.File; +import domain.board.Position; +import domain.piece.Piece; +import domain.piece.PieceColor; +import domain.piece.PieceType; + +import java.util.List; +import java.util.Map; + +public record Score(double value) { + + public static Score of(final Board board, final PieceColor pieceColor) { + Map piecePositions = board.piecePositions(); + double existTargetColorPiecesScoreSum = sumExistTargetPiecesScore(piecePositions, pieceColor); + double scoreValue = existTargetColorPiecesScoreSum - calculateDecreaseScoreForExistSameFilePawns(piecePositions, pieceColor); + + return new Score(scoreValue); + } + + private static double sumExistTargetPiecesScore(final Map piecePositions, final PieceColor pieceColor) { + return piecePositions.values() + .stream() + .filter(piece -> piece.isTeam(pieceColor)) + .mapToDouble(Piece::score) + .sum(); + } + + private static double calculateDecreaseScoreForExistSameFilePawns(final Map piecePositions, final PieceColor pieceColor) { + List targetColorPawnPositions = piecePositions.entrySet() + .stream() + .filter(entry -> entry.getValue().isTeam(pieceColor) && entry.getValue().matchPieceType(PieceType.PAWN)) + .map(Map.Entry::getKey) + .toList(); + + int sameFilePawnCount = 0; + for (File file : File.values()) { + sameFilePawnCount += calculateSameFilePawnCount(targetColorPawnPositions, file); + } + + return 0.5 * sameFilePawnCount; + } + + private static int calculateSameFilePawnCount(final List pawnPositions, final File file) { + int sameFilePawnCount = (int) pawnPositions.stream() + .filter(position -> position.file() == file) + .count(); + + if (sameFilePawnCount <= 1) { + return 0; + } + + return sameFilePawnCount; + } + + public boolean isBigger(final Score other) { + return this.value > other.value; + } +} diff --git a/src/main/java/domain/game/Start.java b/src/main/java/domain/game/Start.java new file mode 100644 index 00000000000..a577f0f2fce --- /dev/null +++ b/src/main/java/domain/game/Start.java @@ -0,0 +1,15 @@ +package domain.game; + +import controller.GameController; + +public class Start implements GameCommand { + + @Override + public void execute(final GameController gameController) { + if (gameController.gameStatus() != GameStatus.WAITING) { + throw new IllegalArgumentException("이미 게임이 진행중입니다."); + } + + gameController.buildGame(); + } +} diff --git a/src/main/java/domain/game/Status.java b/src/main/java/domain/game/Status.java new file mode 100644 index 00000000000..07158c5c488 --- /dev/null +++ b/src/main/java/domain/game/Status.java @@ -0,0 +1,15 @@ +package domain.game; + +import controller.GameController; + +public class Status implements GameCommand { + + @Override + public void execute(final GameController gameController) { + if (gameController.gameStatus() != GameStatus.RUNNING) { + throw new IllegalArgumentException("status 명령어는 게임 중에만 실행할 수 있습니다."); + } + + gameController.printGameStatus(); + } +} diff --git a/src/main/java/domain/piece/Bishop.java b/src/main/java/domain/piece/Bishop.java new file mode 100644 index 00000000000..04c7d433810 --- /dev/null +++ b/src/main/java/domain/piece/Bishop.java @@ -0,0 +1,64 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +import java.util.List; +import java.util.stream.Stream; + +import static domain.piece.CommonMovementDirection.calculateDirection; +import static domain.piece.PieceType.BISHOP; + +public class Bishop extends Piece { + private static final PieceType PIECE_TYPE = BISHOP; + + public Bishop(final PieceColor color) { + super(color); + } + + @Override + public void move(final Position source, final Position destination, final Board board) { + CommonMovementDirection movementDirection = calculateDirection(source, destination); + movementDirection.checkBishopMovableMovement(); + + List movePaths = Stream.iterate(source, current -> current.next(movementDirection)) + .takeWhile(current -> current.equals(source) || isContinuable(current, destination, board)) + .toList(); + + Position alivePosition = movePaths.get(movePaths.size() - 1).next(movementDirection); + checkAlivePosition(alivePosition, board); + } + + private boolean isContinuable(final Position current, final Position destination, Board board) { + if (current.equals(destination)) { + return false; + } + + if (board.existPiece(current)) { + throw new IllegalArgumentException("목적지 경로에 기물이 존재하여 이동할 수 없습니다."); + } + + return true; + } + + private void checkAlivePosition(final Position alivePosition, final Board board) { + if (board.existPiece(alivePosition) && board.existTeamColor(alivePosition, color)) { + throw new IllegalArgumentException("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + } + + @Override + public PieceType pieceType() { + return PIECE_TYPE; + } + + @Override + public double score() { + return pieceType().score(); + } + + @Override + public boolean matchPieceType(final PieceType target) { + return PIECE_TYPE == target; + } +} diff --git a/src/main/java/domain/piece/CommonMovementDirection.java b/src/main/java/domain/piece/CommonMovementDirection.java new file mode 100644 index 00000000000..0ddb40fda33 --- /dev/null +++ b/src/main/java/domain/piece/CommonMovementDirection.java @@ -0,0 +1,74 @@ +package domain.piece; + +import domain.board.Position; + +import java.util.Arrays; +import java.util.List; +import java.util.function.BiPredicate; + +public enum CommonMovementDirection implements MovementDirection { + UP(-1, 0, (rowDistance, columnDistance) -> rowDistance < 0 && columnDistance == 0), + DOWN(1, 0, (rowDistance, columnDistance) -> rowDistance > 0 && columnDistance == 0), + RIGHT(0, 1, (rowDistance, columnDistance) -> rowDistance == 0 && columnDistance > 0), + LEFT(0, -1, (rowDistance, columnDistance) -> rowDistance == 0 && columnDistance < 0), + UP_RIGHT(-1, 1, (rowDistance, columnDistance) -> rowDistance < 0 && columnDistance > 0), + UP_LEFT(-1, -1, (rowDistance, columnDistance) -> rowDistance < 0 && columnDistance < 0), + DOWN_RIGHT(1, 1, (rowDistance, columnDistance) -> rowDistance > 0 && columnDistance > 0), + DOWN_LEFT(1, -1, (rowDistance, columnDistance) -> rowDistance > 0 && columnDistance < 0); + + private final int rowDistance; + private final int columnDistance; + private final BiPredicate condition; + + CommonMovementDirection(final int rowDistance, final int columnDistance, final BiPredicate condition) { + this.rowDistance = rowDistance; + this.columnDistance = columnDistance; + this.condition = condition; + } + + public static CommonMovementDirection calculateDirection(final Position source, final Position destination) { + final int rowDifference = destination.rowIndex() - source.rowIndex(); + final int columnDifference = destination.columnIndex() - source.columnIndex(); + + validateDistance(rowDifference, columnDifference); + + return Arrays.stream(CommonMovementDirection.values()) + .filter(unitVector -> unitVector.condition.test(rowDifference, columnDifference)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("상/하/좌/우 혹은 대각선으로 이동할 수 없는 칸입니다.")); + } + + private static void validateDistance(final int rowDistance, final int columnDistance) { + if (rowDistance == 0 && columnDistance == 0 + || (!(Math.abs(rowDistance) == Math.abs(columnDistance)) + && !(rowDistance == 0 || columnDistance == 0))) { + throw new IllegalArgumentException(("상/하/좌/우 혹은 대각선으로 이동할 수 없는 칸입니다.")); + } + } + + public void checkBishopMovableMovement() { + List bishopMovableMovement = List.of(UP_RIGHT, UP_LEFT, DOWN_RIGHT, DOWN_LEFT); + + if (!bishopMovableMovement.contains(this)) { + throw new IllegalArgumentException("비숍이 이동할 수 있는 방향이 아닙니다."); + } + } + + public void checkRookMovableMovement() { + List rookMovableMovement = List.of(UP, DOWN, RIGHT, LEFT); + + if (!rookMovableMovement.contains(this)) { + throw new IllegalArgumentException("룩이 이동할 수 있는 방향이 아닙니다."); + } + } + + @Override + public int getRowDistance() { + return rowDistance; + } + + @Override + public int getColumnDistance() { + return columnDistance; + } +} diff --git a/src/main/java/domain/piece/King.java b/src/main/java/domain/piece/King.java new file mode 100644 index 00000000000..a1fcdff0a95 --- /dev/null +++ b/src/main/java/domain/piece/King.java @@ -0,0 +1,55 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +import static domain.piece.PieceType.KING; +import static domain.piece.CommonMovementDirection.calculateDirection; + +public class King extends Piece { + private static final PieceType PIECE_TYPE = KING; + + public King(final PieceColor color) { + super(color); + } + + @Override + public void move(final Position source, final Position destination, final Board board) { + CommonMovementDirection movementDirection = calculateDirection(source, destination); + checkMoveDistance(source, destination, movementDirection); + + Position alivePosition = source.next(movementDirection); + + checkAlivePosition(alivePosition, board); + } + + private void checkMoveDistance(final Position source, final Position destination, final CommonMovementDirection movementDirection) { + int rowDistance = destination.rowIndex() - source.rowIndex(); + int columnDistance = destination.columnIndex() - source.columnIndex(); + + if (movementDirection.getRowDistance() != rowDistance || movementDirection.getColumnDistance() != columnDistance) { + throw new IllegalArgumentException("이동할 수 없는 거리입니다."); + } + } + + private void checkAlivePosition(final Position alivePosition, final Board board) { + if (board.existPiece(alivePosition) && board.existTeamColor(alivePosition, color)) { + throw new IllegalArgumentException("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + } + + @Override + public PieceType pieceType() { + return PIECE_TYPE; + } + + @Override + public double score() { + return pieceType().score(); + } + + @Override + public boolean matchPieceType(final PieceType target) { + return PIECE_TYPE == target; + } +} diff --git a/src/main/java/domain/piece/Knight.java b/src/main/java/domain/piece/Knight.java new file mode 100644 index 00000000000..82aa70a7678 --- /dev/null +++ b/src/main/java/domain/piece/Knight.java @@ -0,0 +1,44 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +import static domain.piece.PieceType.KNIGHT; +import static domain.piece.KnightMovementDirection.calculateDirection; + +public class Knight extends Piece { + private static final PieceType PIECE_TYPE = KNIGHT; + + public Knight(final PieceColor color) { + super(color); + } + + @Override + public void move(final Position source, final Position destination, final Board board) { + KnightMovementDirection movementDirection = calculateDirection(source, destination); + + Position alivePosition = source.next(movementDirection); + checkAlivePosition(alivePosition, board); + } + + private void checkAlivePosition(final Position alivePosition, final Board board) { + if (board.existPiece(alivePosition) && board.existTeamColor(alivePosition, color)) { + throw new IllegalArgumentException("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + } + + @Override + public PieceType pieceType() { + return PIECE_TYPE; + } + + @Override + public double score() { + return pieceType().score(); + } + + @Override + public boolean matchPieceType(final PieceType target) { + return PIECE_TYPE == target; + } +} diff --git a/src/main/java/domain/piece/KnightMovementDirection.java b/src/main/java/domain/piece/KnightMovementDirection.java new file mode 100644 index 00000000000..eda0dd7d451 --- /dev/null +++ b/src/main/java/domain/piece/KnightMovementDirection.java @@ -0,0 +1,62 @@ +package domain.piece; + +import domain.board.Position; + +import java.util.Arrays; + +public enum KnightMovementDirection implements MovementDirection { + UP_RIGHT(-2, 1), + UP_LEFT(-2, -1), + DOWN_RIGHT(2, 1), + DOWN_LEFT(2, -1), + RIGHT_UP(-1, 2), + RIGHT_DOWN(1, 2), + LEFT_UP(-1, -2), + LEFT_DOWN(1, -2); + + private final int rowDistance; + private final int columnDistance; + + KnightMovementDirection(final int rowDistance, final int columnDistance) { + this.rowDistance = rowDistance; + this.columnDistance = columnDistance; + } + + public static KnightMovementDirection calculateDirection(final Position source, final Position destination) { + final int rowDifference = destination.rowIndex() - source.rowIndex(); + final int columnDifference = destination.columnIndex() - source.columnIndex(); + + return Arrays.stream(values()) + .filter(knightMovementDirection -> knightMovementDirection.matchDistance(rowDifference, columnDifference)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("나이트가 이동할 수 없는 방향입니다.")); + } + + private boolean matchDistance(final int rowDistance, final int columnDistance) { + return this.rowDistance == rowDistance && this.columnDistance == columnDistance; + } + + @Override + public int getRowDistance() { + return rowDistance; + } + + @Override + public int getColumnDistance() { + return columnDistance; + } + + + // TODO : 레거시 + public static boolean isMovableDirection(final Position source, final Position destination) { + return Arrays.stream(values()) + .anyMatch(knightDirection -> knightDirection.find(source, destination)); + } + + private boolean find(final Position source, final Position destination) { + final int rowDistance = destination.rowIndex() - source.rowIndex(); + final int columnDistance = destination.columnIndex() - source.columnIndex(); + + return this.rowDistance == rowDistance && this.columnDistance == columnDistance; + } +} diff --git a/src/main/java/domain/piece/MovementDirection.java b/src/main/java/domain/piece/MovementDirection.java new file mode 100644 index 00000000000..e0413387989 --- /dev/null +++ b/src/main/java/domain/piece/MovementDirection.java @@ -0,0 +1,8 @@ +package domain.piece; + +public interface MovementDirection { + + int getRowDistance(); + + int getColumnDistance(); +} diff --git a/src/main/java/domain/piece/Pawn.java b/src/main/java/domain/piece/Pawn.java new file mode 100644 index 00000000000..f73c7f06750 --- /dev/null +++ b/src/main/java/domain/piece/Pawn.java @@ -0,0 +1,79 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +import static domain.board.Rank.SEVEN; +import static domain.board.Rank.TWO; +import static domain.piece.PieceType.PAWN; +import static domain.piece.PawnMovementDirection.calculateDirection; +import static domain.piece.PieceColor.BLACK; +import static domain.piece.PieceColor.WHITE; + +public class Pawn extends Piece { + private static final PieceType PIECE_TYPE = PAWN; + + public Pawn(final PieceColor color) { + super(color); + } + + @Override + public void move(final Position source, final Position destination, final Board board) { + PawnMovementDirection movementDirection = calculateDirection(color, source, destination); + if (movementDirection.isCrossStep()) { + checkAlivePositionOfCrossStep(source.next(movementDirection), board); + return; + } + + if (movementDirection.isTwoStep()) { + checkIsStartPosition(source); + } + checkMovePaths(source, movementDirection, board); + } + + private void checkAlivePositionOfCrossStep(final Position alivePosition, final Board board) { + if (!board.existPiece(alivePosition) || board.existTeamColor(alivePosition, color)) { + throw new IllegalArgumentException("적 기물이 존재하지 않으면 대각선으로 이동할 수 없습니다"); + } + } + + private void checkIsStartPosition(final Position source) { + if ((color == WHITE && source.rank() != TWO) || (color == BLACK && source.rank() != SEVEN)) { + throw new IllegalArgumentException("시작 위치가 아니면 두 칸 이동할 수 없습니다."); + } + } + + private void checkMovePaths( + final Position source, + final PawnMovementDirection movementDirection, + final Board board + ) { + Position current = source; + int moveDistance = Math.abs(movementDirection.getRowDistance()); + for (int i = 0; i < moveDistance; i++) { + current = current.next(movementDirection.convertOneStep()); + checkPathHasPiece(current, board); + } + } + + private void checkPathHasPiece(final Position path, final Board board) { + if (board.existPiece(path)) { + throw new IllegalArgumentException("전진시 기물이 존재하는 경로 혹은 목적지로 이동할 수 없습니다."); + } + } + + @Override + public PieceType pieceType() { + return PIECE_TYPE; + } + + @Override + public double score() { + return pieceType().score(); + } + + @Override + public boolean matchPieceType(final PieceType target) { + return PIECE_TYPE == target; + } +} diff --git a/src/main/java/domain/piece/PawnMovementDirection.java b/src/main/java/domain/piece/PawnMovementDirection.java new file mode 100644 index 00000000000..d539720861c --- /dev/null +++ b/src/main/java/domain/piece/PawnMovementDirection.java @@ -0,0 +1,84 @@ +package domain.piece; + +import domain.board.Position; + +import java.util.Arrays; + +import static domain.piece.PieceColor.BLACK; +import static domain.piece.PieceColor.WHITE; + +public enum PawnMovementDirection implements MovementDirection { + UP_ONE_STEP(-1, 0, WHITE), + UP_TWO_STEP(-2, 0, WHITE), + UP_RIGHT(-1, 1, WHITE), + UP_LEFT(-1, -1, WHITE), + DOWN_ONE_STEP(1, 0, BLACK), + DOWN_TWO_STEP(2, 0, BLACK), + DOWN_RIGHT(1, 1, BLACK), + DOWN_LEFT(1, -1, BLACK); + + private final int rowDistance; + private final int columnDistance; + private final PieceColor pieceColor; + + PawnMovementDirection(final int rowDistance, final int columnDistance, final PieceColor pieceColor) { + this.rowDistance = rowDistance; + this.columnDistance = columnDistance; + this.pieceColor = pieceColor; + } + + public static PawnMovementDirection calculateDirection(final PieceColor pieceColor, final Position source, final Position destination) { + final int rowDifference = destination.rowIndex() - source.rowIndex(); + final int columnDifference = destination.columnIndex() - source.columnIndex(); + + return Arrays.stream(values()) + .filter(knightMovementDirection -> knightMovementDirection.matchDistance(pieceColor, rowDifference, columnDifference)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException(pieceColor.name() + "색상의 폰이 이동할 수 없는 방향입니다.")); + } + + private boolean matchDistance(final PieceColor pieceColor, final int rowDistance, final int columnDistance) { + return this.pieceColor == pieceColor + && this.rowDistance == rowDistance + && this.columnDistance == columnDistance; + } + + public boolean isCrossStep() { + return this == UP_RIGHT || this == UP_LEFT + || this == DOWN_RIGHT || this == DOWN_LEFT; + } + + public boolean isTwoStep() { + return this == UP_TWO_STEP || this == DOWN_TWO_STEP; + } + + public PawnMovementDirection convertOneStep() { + if (!this.isTwoStep() && !this.isOneStep()) { + throw new IllegalArgumentException("전진 방향만 변환할 수 있습니다."); + } + + if (this == UP_TWO_STEP) { + return UP_ONE_STEP; + } + + if (this == DOWN_TWO_STEP) { + return DOWN_ONE_STEP; + } + + return this; + } + + private boolean isOneStep() { + return this == UP_ONE_STEP || this == DOWN_ONE_STEP; + } + + @Override + public int getRowDistance() { + return rowDistance; + } + + @Override + public int getColumnDistance() { + return columnDistance; + } +} diff --git a/src/main/java/domain/piece/Piece.java b/src/main/java/domain/piece/Piece.java new file mode 100644 index 00000000000..231fd8e8f87 --- /dev/null +++ b/src/main/java/domain/piece/Piece.java @@ -0,0 +1,28 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +public abstract class Piece { + protected final PieceColor color; + + public Piece(final PieceColor color) { + this.color = color; + } + + public abstract void move(final Position source, final Position destination, final Board board); + + public abstract PieceType pieceType(); + + public abstract double score(); + + public abstract boolean matchPieceType(PieceType pieceType); + + public boolean isTeam(final PieceColor teamColor) { + return this.color == teamColor; + } + + public PieceColor pieceColor() { + return color; + } +} diff --git a/src/main/java/domain/piece/PieceColor.java b/src/main/java/domain/piece/PieceColor.java new file mode 100644 index 00000000000..f9f7d5b880b --- /dev/null +++ b/src/main/java/domain/piece/PieceColor.java @@ -0,0 +1,22 @@ +package domain.piece; + +import java.util.Arrays; + +public enum PieceColor { + BLACK, WHITE; + + public static PieceColor of(final String value) { + return Arrays.stream(PieceColor.values()) + .filter(pieceColor -> pieceColor.name().equals(value)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않은 PieceColor 입니다.")); + } + + public PieceColor toggle() { + if (this == BLACK) { + return WHITE; + } + + return BLACK; + } +} diff --git a/src/main/java/domain/piece/PieceType.java b/src/main/java/domain/piece/PieceType.java new file mode 100644 index 00000000000..18c84e40024 --- /dev/null +++ b/src/main/java/domain/piece/PieceType.java @@ -0,0 +1,36 @@ +package domain.piece; + +import java.util.Arrays; +import java.util.function.Function; + +public enum PieceType { + PAWN(1, Pawn::new), + ROOK(5, Rook::new), + KNIGHT(2.5, Knight::new), + BISHOP(3, Bishop::new), + QUEEN(9, Queen::new), + KING(0, King::new); + + private final double score; + private final Function convertPiece; + + PieceType(final double score, final Function convertPiece) { + this.score = score; + this.convertPiece = convertPiece; + } + + public static PieceType of(final String value) { + return Arrays.stream(PieceType.values()) + .filter(pieceType -> pieceType.name().equals(value.toUpperCase())) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않은 PieceType 입니다.")); + } + + public double score() { + return score; + } + + public Piece createPiece(final PieceColor pieceColor) { + return convertPiece.apply(pieceColor); + } +} diff --git a/src/main/java/domain/piece/Queen.java b/src/main/java/domain/piece/Queen.java new file mode 100644 index 00000000000..322f356dc04 --- /dev/null +++ b/src/main/java/domain/piece/Queen.java @@ -0,0 +1,63 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +import java.util.List; +import java.util.stream.Stream; + +import static domain.piece.CommonMovementDirection.calculateDirection; +import static domain.piece.PieceType.QUEEN; + +public class Queen extends Piece { + private static final PieceType PIECE_TYPE = QUEEN; + + public Queen(final PieceColor color) { + super(color); + } + + @Override + public void move(final Position source, final Position destination, final Board board) { + CommonMovementDirection movementDirection = calculateDirection(source, destination); + + List movePaths = Stream.iterate(source, current -> current.next(movementDirection)) + .takeWhile(current -> current.equals(source) || isContinuable(current, destination, board)) + .toList(); + + Position alivePosition = movePaths.get(movePaths.size() - 1).next(movementDirection); + checkAlivePosition(alivePosition, board); + } + + private boolean isContinuable(final Position current, final Position destination, final Board board) { + if (current.equals(destination)) { + return false; + } + + if (board.existPiece(current)) { + throw new IllegalArgumentException("목적지 경로에 기물이 존재하여 이동할 수 없습니다."); + } + + return true; + } + + private void checkAlivePosition(final Position alivePosition, final Board board) { + if (board.existPiece(alivePosition) && board.existTeamColor(alivePosition, color)) { + throw new IllegalArgumentException("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + } + + @Override + public PieceType pieceType() { + return PIECE_TYPE; + } + + @Override + public double score() { + return pieceType().score(); + } + + @Override + public boolean matchPieceType(final PieceType target) { + return PIECE_TYPE == target; + } +} diff --git a/src/main/java/domain/piece/Rook.java b/src/main/java/domain/piece/Rook.java new file mode 100644 index 00000000000..24729881d37 --- /dev/null +++ b/src/main/java/domain/piece/Rook.java @@ -0,0 +1,64 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +import java.util.List; +import java.util.stream.Stream; + +import static domain.piece.CommonMovementDirection.calculateDirection; +import static domain.piece.PieceType.ROOK; + +public class Rook extends Piece { + private static final PieceType PIECE_TYPE = ROOK; + + public Rook(final PieceColor color) { + super(color); + } + + @Override + public void move(final Position source, final Position destination, final Board board) { + CommonMovementDirection movementDirection = calculateDirection(source, destination); + movementDirection.checkRookMovableMovement(); + + List movePaths = Stream.iterate(source, current -> current.next(movementDirection)) + .takeWhile(current -> current.equals(source) || isContinuable(current, destination, board)) + .toList(); + + Position alivePosition = movePaths.get(movePaths.size() - 1).next(movementDirection); + checkAlivePosition(alivePosition, board); + } + + private boolean isContinuable(final Position current, final Position destination, final Board board) { + if (current.equals(destination)) { + return false; + } + + if (board.existPiece(current)) { + throw new IllegalArgumentException("목적지 경로에 기물이 존재하여 이동할 수 없습니다."); + } + + return true; + } + + private void checkAlivePosition(final Position alivePosition, final Board board) { + if (board.existPiece(alivePosition) && board.existTeamColor(alivePosition, color)) { + throw new IllegalArgumentException("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + } + + @Override + public PieceType pieceType() { + return PIECE_TYPE; + } + + @Override + public double score() { + return pieceType().score(); + } + + @Override + public boolean matchPieceType(final PieceType target) { + return PIECE_TYPE == target; + } +} diff --git a/src/main/java/dto/BoardDto.java b/src/main/java/dto/BoardDto.java new file mode 100644 index 00000000000..c41de62ca33 --- /dev/null +++ b/src/main/java/dto/BoardDto.java @@ -0,0 +1,19 @@ +package dto; + +import domain.board.Position; +import domain.piece.Piece; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public record BoardDto(Map value) { + + public static BoardDto from(Map piecePositions) { + Map value = new HashMap<>(); + piecePositions.forEach(((position, piece) -> + value.put(PositionDto.from(position), PieceDto.from(piece)))); + + return new BoardDto(Collections.unmodifiableMap(value)); + } +} diff --git a/src/main/java/dto/PieceDto.java b/src/main/java/dto/PieceDto.java new file mode 100644 index 00000000000..66a33e05564 --- /dev/null +++ b/src/main/java/dto/PieceDto.java @@ -0,0 +1,12 @@ +package dto; + +import domain.piece.PieceType; +import domain.piece.Piece; +import domain.piece.PieceColor; + +public record PieceDto(PieceType type, PieceColor color) { + + public static PieceDto from(final Piece piece) { + return new PieceDto(piece.pieceType(), piece.pieceColor()); + } +} diff --git a/src/main/java/dto/PositionDto.java b/src/main/java/dto/PositionDto.java new file mode 100644 index 00000000000..6f4eb347c09 --- /dev/null +++ b/src/main/java/dto/PositionDto.java @@ -0,0 +1,14 @@ +package dto; + +import domain.board.Position; + +public record PositionDto(int row, int column) { + + public static PositionDto from(final Position position) { + return new PositionDto(position.rowIndex(), position.columnIndex()); + } + + public static PositionDto emptyPosition() { + return new PositionDto(0, 0); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000000..7aecc363a09 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,21 @@ +package view; + +import java.util.Scanner; + +public class InputView { + private final Scanner sc = new Scanner(System.in); + + public String inputCommand() { + String input = sc.nextLine(); + validateEmpty(input); + System.out.println(); + + return input; + } + + private void validateEmpty(final String input) { + if (input == null || input.isBlank()) { + throw new IllegalArgumentException("공백을 입력할 수 없습니다."); + } + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000000..690a10e1980 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,111 @@ +package view; + +import domain.game.GameResult; +import domain.game.Score; +import domain.piece.PieceColor; +import domain.piece.PieceType; +import dto.BoardDto; +import dto.PieceDto; +import dto.PositionDto; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static domain.piece.PieceType.PAWN; +import static domain.piece.PieceType.ROOK; +import static domain.piece.PieceType.KNIGHT; +import static domain.piece.PieceType.BISHOP; +import static domain.piece.PieceType.QUEEN; +import static domain.piece.PieceType.KING; + +public class OutputView { + private static final Map pieceFormat = Map.ofEntries( + Map.entry(ROOK, "R"), + Map.entry(KNIGHT, "N"), + Map.entry(BISHOP, "B"), + Map.entry(QUEEN, "Q"), + Map.entry(KING, "K"), + Map.entry(PAWN, "P") + ); + + public void printErrorMessage(final String message) { + System.out.println(message); + System.out.println(); + } + + public void printWelcomeMessage() { + System.out.println("> 체스 게임을 시작합니다."); + System.out.println("> 게임 시작: start"); + System.out.println("> 게임 종료: end"); + System.out.println("> 게임 이동: move source위치 target위치 - 예. move b2 b3"); + System.out.println(); + } + + public void printTurnStatus(final BoardDto boardDto, final PieceColor pieceColor) { + System.out.println("====Status====="); + System.out.println(pieceColor.name() + "팀 차례입니다."); + System.out.println("==============="); + List boardStatus = convertBoardStatus(boardDto.value()); + boardStatus.forEach(System.out::println); + System.out.println(); + } + + private List convertBoardStatus(final Map boardStatus) { + String[][] boardMessage = initEmptyBoardMessage(); + boardStatus.forEach((position, piece) -> addPieceStatusInBoardMessage(boardMessage, position, piece)); + + return Arrays.stream(boardMessage) + .map(rowString -> String.join("", rowString)) + .toList(); + } + + private static String[][] initEmptyBoardMessage() { + String[][] strings = new String[8][8]; + for (String[] row : strings) { + Arrays.fill(row, "."); + } + + return strings; + } + + private void addPieceStatusInBoardMessage(final String[][] boardMessage, final PositionDto position, final PieceDto piece) { + boardMessage[position.row()][position.column()] = convertPieceTypeToString(piece.type(), piece.color()); + } + + private String convertPieceTypeToString(final PieceType pieceType, final PieceColor pieceColor) { + String pieceMessage = pieceFormat.get(pieceType); + + if (pieceColor == PieceColor.WHITE) { + return pieceMessage.toLowerCase(); + } + + return pieceMessage; + } + + public void printGameResult(final Score whiteTeamScore, final Score blackTeamScore, final GameResult gameResult) { + System.out.println("White Score : " + whiteTeamScore.value()); + System.out.println("Black Score : " + blackTeamScore.value()); + System.out.println(convertToGameResultMessage(gameResult)); + System.out.println(); + } + + private String convertToGameResultMessage(final GameResult gameResult) { + if (gameResult == GameResult.BLACK_WIN) { + return "블랙이 이기고 있습니다!"; + } + + if (gameResult == GameResult.WHITE_WIN) { + return "화이트가 이기고 있습니다!"; + } + + return "막상막하입니다!"; + } + + public void printInputRoadGameMessage() { + System.out.println("> 이전 게임이 존재합니다. 불러오시겠습니까?"); + System.out.println("> 불러오기 : continue"); + System.out.println("> 새게임 : new"); + System.out.println(); + } +} diff --git a/src/test/java/domain/board/BoardTest.java b/src/test/java/domain/board/BoardTest.java new file mode 100644 index 00000000000..6967864eeb3 --- /dev/null +++ b/src/test/java/domain/board/BoardTest.java @@ -0,0 +1,117 @@ +package domain.board; + +import domain.piece.Pawn; +import domain.piece.Piece; +import domain.piece.Rook; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static domain.board.File.B; +import static domain.board.File.D; +import static domain.board.Rank.FIVE; +import static domain.board.Rank.FOUR; +import static domain.board.Rank.SEVEN; +import static domain.board.Rank.SIX; +import static domain.board.Rank.TWO; +import static domain.piece.PieceColor.BLACK; +import static domain.piece.PieceColor.WHITE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class BoardTest { + + @DisplayName("기물을 이동시킨다.") + @Test + void movePieceTest() { + // Given + Position source = position(B, TWO); + Position destination = position(B, FOUR); + Map piecePositions = new HashMap<>(Map.of(source, new Pawn(WHITE))); + Board board = board(piecePositions); + + // When + board.movePiece(WHITE, source, destination); + boolean existsPieceOnSource = piecePositions.containsKey(source); + boolean existsPieceOnDestination = piecePositions.containsKey(destination); + + // Then + assertThat(existsPieceOnSource).isFalse(); + assertThat(existsPieceOnDestination).isTrue(); + } + + @DisplayName("목적지에 적 기물이 존재하면 적 기물을 제거하고 이동시킨다.") + @Test + void movePieceWithRemoveEnemyPieceTest() { + // Given + Position source = position(B, TWO); + Position destination = position(B, FOUR); + Rook myPiece = new Rook(WHITE); + Pawn enemyPiece = new Pawn(BLACK); + Map piecePositions = new HashMap<>(Map.of(source, myPiece, destination, enemyPiece)); + Board board = board(piecePositions); + + // When + board.movePiece(WHITE, source, destination); + boolean existsPieceOnSource = piecePositions.containsKey(source); + Piece destinationPiece = piecePositions.get(destination); + + // Then + assertThat(existsPieceOnSource).isFalse(); + assertThat(destinationPiece).isNotEqualTo(enemyPiece); + assertThat(destinationPiece).isEqualTo(myPiece); + } + + @DisplayName("입력된 출발지와 목적지의 위치가 같으면 예외를 발생시킨다.") + @Test + void throwExceptionWhenSourceEqualsDestinationTest() { + // Given + Position source = new Position(B, TWO); + Position destination = new Position(B, TWO); + Board board = board(Collections.emptyMap()); + + // When & Then + assertThatThrownBy(() -> board.movePiece(WHITE, source, destination)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("출발지와 목적지가 같을 수 없습니다."); + } + + @DisplayName("기물이 존재하지 않는 출발지 위치가 입력되면 예외를 발생시킨다.") + @Test + void throwExceptionWhenNotExistPieceSourceTest() { + // Given + Board board = board(Collections.emptyMap()); + Position source = new Position(D, FIVE); + Position destination = new Position(B, TWO); + + // When & Then + assertThatThrownBy(() -> board.movePiece(WHITE, source, destination)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("출발지에 기물이 존재하지 않습니다."); + } + + @DisplayName("상대방의 기물을 이동시키려고 하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenMoveEnemyPieceTest() { + // Given + Position source = new Position(B, SEVEN); + Position destination = new Position(B, SIX); + Board board = board(Map.of(source, new Pawn(BLACK))); + + // When & Then + assertThatThrownBy(() -> board.movePiece(WHITE, source, destination)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("상대방의 기물을 이동시킬 수 없습니다."); + } + + private Position position(final File file, final Rank rank) { + return new Position(file, rank); + } + + private Board board(final Map piecePositions) { + return new Board(new TestPieceDao(), piecePositions); + } +} diff --git a/src/test/java/domain/board/TestPieceDao.java b/src/test/java/domain/board/TestPieceDao.java new file mode 100644 index 00000000000..ae5aa4d9aa1 --- /dev/null +++ b/src/test/java/domain/board/TestPieceDao.java @@ -0,0 +1,40 @@ +package domain.board; + +import dao.PieceDao; +import dao.PieceEntity; + +import java.util.Collections; +import java.util.List; + +public class TestPieceDao implements PieceDao { + + @Override + public List findAll() { + return Collections.emptyList(); + } + + @Override + public boolean existPiecePositions() { + return false; + } + + @Override + public void save(final PieceEntity piece) { + + } + + @Override + public void update(final File sourceFile, final Rank sourceRank, final File destinationFile, final Rank destinationRank) { + + } + + @Override + public void delete(final File file, final Rank rank) { + + } + + @Override + public void deleteAll() { + + } +} diff --git a/src/test/java/domain/piece/BishopTest.java b/src/test/java/domain/piece/BishopTest.java new file mode 100644 index 00000000000..7844d6e9fcc --- /dev/null +++ b/src/test/java/domain/piece/BishopTest.java @@ -0,0 +1,115 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.board.TestPieceDao; +import org.junit.jupiter.api.DisplayName; +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; + +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class BishopTest { + + @DisplayName("주어진 출발지 -> 도착지를 Bishop이 이동할 수 있는지 검증한다.") + @MethodSource("checkMovableTestCase") + @ParameterizedTest + void checkMovableTest(final Position source, final Position destination) { + // Given + Bishop bishop = new Bishop(PieceColor.WHITE); + Map piecePositions = Map.of(position(File.C, Rank.ONE), new Rook(PieceColor.BLACK)); + Board board = board(piecePositions); + + // When & Then + assertThatCode(() -> bishop.move(source, destination, board)) + .doesNotThrowAnyException(); + } + + private static Stream checkMovableTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.E, Rank.FIVE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.THREE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.ONE)), + Arguments.of(position(File.B, Rank.TWO), position(File.C, Rank.ONE)) + ); + } + + @DisplayName("Bishop이 이동할 수 없는 방향의 도착지가 입력되면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenInvalidDirectionTestCase") + @ParameterizedTest + void throwExceptionWhenInvalidDirectionTest(final Position source, final Position destination) { + // Given + Bishop bishop = new Bishop(PieceColor.WHITE); + Map piecePositions = Collections.emptyMap(); + Board board = board(piecePositions); + + // When & Then + assertThatThrownBy(() ->bishop.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("비숍이 이동할 수 있는 방향이 아닙니다."); + } + + private static Stream throwExceptionWhenInvalidDirectionTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.FIVE)), + Arguments.of(position(File.B, Rank.TWO), position(File.G, Rank.TWO)) + ); + } + + @DisplayName("이동 경로에 기물이 존재하면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenPathsHasPieceTestCase") + @ParameterizedTest + void throwExceptionWhenPathsHasPieceTest(final Position source, final Position destination) { + // Given + Bishop bishop = new Bishop(PieceColor.WHITE); + Map piecePositions = Map.of( + position(File.C, Rank.THREE), new Bishop(PieceColor.BLACK) + ); + Board board = board(piecePositions); + + // When & Then + assertThatThrownBy(() -> bishop.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("목적지 경로에 기물이 존재하여 이동할 수 없습니다."); + } + + private static Stream throwExceptionWhenPathsHasPieceTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.E, Rank.FIVE)), + Arguments.of(position(File.D, Rank.FOUR), position(File.B, Rank.TWO)) + ); + } + + @DisplayName("도착지에 아군 기물이 존재하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenDestinationHasTeamPieceTest() { + // Given + Position source = position(File.B, Rank.TWO); + Position destination = position(File.D, Rank.FOUR); + Bishop bishop = new Bishop(PieceColor.WHITE); + Map piecePositions = Map.of(destination, new Bishop(PieceColor.WHITE)); + Board board = board(piecePositions); + + // When & Then + assertThatThrownBy(() -> bishop.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } + + private static Board board(final Map piecePositions) { + return new Board(new TestPieceDao(), piecePositions); + } +} diff --git a/src/test/java/domain/piece/KingTest.java b/src/test/java/domain/piece/KingTest.java new file mode 100644 index 00000000000..a402b31ee42 --- /dev/null +++ b/src/test/java/domain/piece/KingTest.java @@ -0,0 +1,89 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.board.TestPieceDao; +import org.junit.jupiter.api.DisplayName; +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; + +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class KingTest { + + @DisplayName("주어진 출발지 -> 도착지를 Rook이 이동할 수 있는지 검증한다.") + @MethodSource("checkMovableTestCase") + @ParameterizedTest + void checkMovableTest(final Position source, final Position destination) { + // Given + King king = new King(PieceColor.WHITE); + Map piecePositions = Map.of(position(File.D, Rank.TWO), new Rook(PieceColor.BLACK)); + Board board = board(piecePositions); + + // When & Then + assertThatCode(() -> king.move(source, destination, board)) + .doesNotThrowAnyException(); + } + + private static Stream checkMovableTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.THREE)), + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.ONE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.TWO)), + Arguments.of(position(File.B, Rank.TWO), position(File.C, Rank.TWO)), + Arguments.of(position(File.B, Rank.TWO), position(File.C, Rank.THREE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.THREE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.ONE)), + Arguments.of(position(File.B, Rank.TWO), position(File.C, Rank.ONE)) + ); + } + + @DisplayName("출발지에서 목적지까지 거리가 두 칸 이상이면 예외를 발생시킨다.") + @Test + void throwExceptionWhenDistanceOverOrEqualTwo() { + // Given + Position source = position(File.B, Rank.THREE); + Position destination = position(File.B, Rank.SIX); + King king = new King(PieceColor.WHITE); + Map piecePositions = Collections.emptyMap(); + Board board = board(piecePositions); + + // When & Then + assertThatThrownBy(() -> king.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 거리입니다."); + } + + @DisplayName("도착지에 아군 기물이 존재하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenDestinationHasTeamPieceTest() { + // Given + Position source = position(File.B, Rank.TWO); + Position destination = position(File.B, Rank.THREE); + King king = new King(PieceColor.WHITE); + Map piecePositions = Map.of(destination, new Rook(PieceColor.WHITE)); + Board board = board(piecePositions); + + // When & Then + assertThatThrownBy(() -> king.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } + + private Board board(final Map piecePositions) { + return new Board(new TestPieceDao(), piecePositions); + } +} diff --git a/src/test/java/domain/piece/KnightTest.java b/src/test/java/domain/piece/KnightTest.java new file mode 100644 index 00000000000..a69fdc1d288 --- /dev/null +++ b/src/test/java/domain/piece/KnightTest.java @@ -0,0 +1,32 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.board.TestPieceDao; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class KnightTest { + + @DisplayName("도착지에 아군 기물이 존재하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenDestinationHasTeamPieceTest() { + // Given + Position source = new Position(File.C, Rank.FOUR); + Position destination = new Position(File.D, Rank.SIX); + Knight knight = new Knight(PieceColor.WHITE); + Map piecePositions = Map.of(destination, new Rook(PieceColor.WHITE)); + Board board = new Board(new TestPieceDao(), piecePositions); + + // When & Then + assertThatThrownBy(() -> knight.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } +} diff --git a/src/test/java/domain/piece/PawnTest.java b/src/test/java/domain/piece/PawnTest.java new file mode 100644 index 00000000000..ea7435e796a --- /dev/null +++ b/src/test/java/domain/piece/PawnTest.java @@ -0,0 +1,153 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.board.TestPieceDao; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; + +import static domain.board.File.C; +import static domain.board.File.D; +import static domain.board.File.E; +import static domain.board.File.F; +import static domain.board.File.G; +import static domain.board.Rank.FIVE; +import static domain.board.Rank.FOUR; +import static domain.board.Rank.SEVEN; +import static domain.board.Rank.SIX; +import static domain.board.Rank.THREE; +import static domain.board.Rank.TWO; +import static domain.piece.PawnMovementDirection.DOWN_LEFT; +import static domain.piece.PawnMovementDirection.DOWN_ONE_STEP; +import static domain.piece.PawnMovementDirection.DOWN_RIGHT; +import static domain.piece.PawnMovementDirection.DOWN_TWO_STEP; +import static domain.piece.PawnMovementDirection.UP_LEFT; +import static domain.piece.PawnMovementDirection.UP_ONE_STEP; +import static domain.piece.PawnMovementDirection.UP_RIGHT; +import static domain.piece.PawnMovementDirection.UP_TWO_STEP; +import static domain.piece.PieceColor.BLACK; +import static domain.piece.PieceColor.WHITE; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PawnTest { + + @DisplayName("주어진 출발지 -> 도착지를 Pawn이 이동할 수 있는지 검증한다.") + @MethodSource("checkMovableTestCase") + @ParameterizedTest + void checkMovableTest(final PieceColor pieceColor, final PieceColor enemyColor, final Position source, final Position destination) { + // Given + Pawn pawn = new Pawn(pieceColor); + Map piecePositions = Map.of( + position(E, SIX), new Rook(enemyColor), + position(C, SIX), new Rook(enemyColor), + position(E, FOUR), new Rook(enemyColor), + position(C, FOUR), new Rook(enemyColor) + ); + Board board = board(piecePositions); + + // When & Then + assertThatCode(() -> pawn.move(source, destination, board)) + .doesNotThrowAnyException(); + } + + private static Stream checkMovableTestCase() { + return Stream.of( + Arguments.of(WHITE, BLACK, position(F, TWO), position(F, FOUR), UP_TWO_STEP), + Arguments.of(WHITE, BLACK, position(C, FOUR), position(C, FIVE), UP_ONE_STEP), + Arguments.of(WHITE, BLACK, position(D, FIVE), position(E, SIX), UP_RIGHT), + Arguments.of(WHITE, BLACK, position(D, FIVE), position(C, SIX), UP_LEFT), + Arguments.of(BLACK, WHITE, position(G, SEVEN), position(G, FIVE), DOWN_TWO_STEP), + Arguments.of(BLACK, WHITE, position(C, FOUR), position(C, THREE), DOWN_ONE_STEP), + Arguments.of(BLACK, WHITE, position(D, FIVE), position(E, FOUR), DOWN_RIGHT), + Arguments.of(BLACK, WHITE, position(D, FIVE), position(C, FOUR), DOWN_LEFT) + ); + } + + @DisplayName("대각석으로 이동할 경우 목적지에 적 기물이 존재하지 않으면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenCrossStepDestinationNotHasEnemyTestCase") + @ParameterizedTest + void throwExceptionWhenCrossStepDestinationNotHasEnemyTest(final PieceColor pieceColor, final Position source, final Position destination) { + // Given + Pawn pawn = new Pawn(pieceColor); + Map piecePositions = Collections.emptyMap(); + Board board = board(piecePositions); + + // When & Then + assertThatThrownBy(() -> pawn.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("적 기물이 존재하지 않으면 대각선으로 이동할 수 없습니다"); + } + + private static Stream throwExceptionWhenCrossStepDestinationNotHasEnemyTestCase() { + return Stream.of( + Arguments.of(WHITE, position(D, FIVE), position(E, SIX), UP_RIGHT), + Arguments.of(WHITE, position(D, FIVE), position(C, SIX), UP_LEFT), + Arguments.of(BLACK, position(D, FIVE), position(E, FOUR), DOWN_RIGHT), + Arguments.of(BLACK, position(D, FIVE), position(C, FOUR), DOWN_LEFT) + ); + } + + @DisplayName("두 칸 전진할 경우 출발지가 폰의 시작 위치가 아니면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenOneStepSourceIsNotStartPositionTestCase") + @ParameterizedTest + void throwExceptionWhenOneStepSourceIsNotStartPositionTest(final PieceColor pieceColor, final Position source, final Position destination) { + // Given + Pawn pawn = new Pawn(pieceColor); + Map piecePositions = Collections.emptyMap(); + Board board = board(piecePositions); + + // When & Then + assertThatThrownBy(() -> pawn.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시작 위치가 아니면 두 칸 이동할 수 없습니다."); + } + + private static Stream throwExceptionWhenOneStepSourceIsNotStartPositionTestCase() { + return Stream.of( + Arguments.of(WHITE, position(C, THREE), position(C, FIVE)), + Arguments.of(BLACK, position(C, FIVE), position(C, THREE)) + ); + } + + @DisplayName("폰이 전진할 시 경로에 기물이 존재하면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenForwardPathHasPieceTestCase") + @ParameterizedTest + void throwExceptionWhenForwardPathHasPieceTest(final PieceColor pieceColor, final Position source, final Position destination) { + // Given + Pawn pawn = new Pawn(pieceColor); + Map piecePositions = Map.of( + position(D, THREE), new Rook(WHITE), + position(C, THREE), new Rook(BLACK) + ); + Board board = board(piecePositions); + + // When & Then + assertThatThrownBy(() -> pawn.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("전진시 기물이 존재하는 경로 혹은 목적지로 이동할 수 없습니다."); + } + + private static Stream throwExceptionWhenForwardPathHasPieceTestCase() { + return Stream.of( + Arguments.of(WHITE, position(D, TWO), position(D, FOUR)), + Arguments.of(BLACK, position(C, FOUR), position(C, THREE)) + ); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } + + private static Board board(final Map piecePositions) { + return new Board(new TestPieceDao(), piecePositions); + } +} diff --git a/src/test/java/domain/piece/QueenTest.java b/src/test/java/domain/piece/QueenTest.java new file mode 100644 index 00000000000..3bb443f865d --- /dev/null +++ b/src/test/java/domain/piece/QueenTest.java @@ -0,0 +1,97 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.board.TestPieceDao; +import org.junit.jupiter.api.DisplayName; +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; + +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class QueenTest { + + @DisplayName("주어진 출발지 -> 도착지를 Rook이 이동할 수 있는지 검증한다.") + @MethodSource("checkMovableTestCase") + @ParameterizedTest + void checkMovableTest(final Position source, final Position destination) { + // Given + Queen queen = new Queen(PieceColor.WHITE); + Map piecePositions = Map.of(position(File.D, Rank.TWO), new Rook(PieceColor.BLACK)); + Board board = board(piecePositions); + + // When & Then + assertThatCode(() -> queen.move(source, destination, board)) + .doesNotThrowAnyException(); + } + + private static Stream checkMovableTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.SIX)), + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.ONE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.TWO)), + Arguments.of(position(File.B, Rank.TWO), position(File.D, Rank.TWO)), + Arguments.of(position(File.B, Rank.TWO), position(File.E, Rank.FIVE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.THREE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.ONE)), + Arguments.of(position(File.B, Rank.TWO), position(File.C, Rank.ONE)) + ); + } + + @DisplayName("이동 경로에 기물이 존재하면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenPathsHasPieceTestCase") + @ParameterizedTest + void throwExceptionWhenPathsHasPieceTest(final Position source, final Position destination) { + // Given + Queen queen = new Queen(PieceColor.WHITE); + Map piecePositions = Map.of( + position(File.B, Rank.FOUR), new Bishop(PieceColor.BLACK), + position(File.D, Rank.TWO), new Rook(PieceColor.WHITE) + ); + Board board = board(piecePositions); + + // When & Then + assertThatThrownBy(() -> queen.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("목적지 경로에 기물이 존재하여 이동할 수 없습니다."); + } + + private static Stream throwExceptionWhenPathsHasPieceTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.SIX)), + Arguments.of(position(File.B, Rank.TWO), position(File.G, Rank.TWO)) + ); + } + + @DisplayName("도착지에 아군 기물이 존재하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenDestinationHasTeamPieceTest() { + // Given + Position source = position(File.B, Rank.TWO); + Position destination = position(File.B, Rank.SIX); + Queen queen = new Queen(PieceColor.WHITE); + Map piecePositions = Map.of(destination, new Rook(PieceColor.WHITE)); + Board board = board(piecePositions); + + // When & Then + assertThatThrownBy(() -> queen.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } + + private static Board board(final Map piecePositions) { + return new Board(new TestPieceDao(), piecePositions); + } +} diff --git a/src/test/java/domain/piece/RookTest.java b/src/test/java/domain/piece/RookTest.java new file mode 100644 index 00000000000..7cdfebc7ba4 --- /dev/null +++ b/src/test/java/domain/piece/RookTest.java @@ -0,0 +1,116 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.board.TestPieceDao; +import org.junit.jupiter.api.DisplayName; +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; + +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RookTest { + + @DisplayName("주어진 출발지 -> 도착지를 Rook이 이동할 수 있는지 검증한다.") + @MethodSource("checkMovableTestCase") + @ParameterizedTest + void checkMovableTest(final Position source, final Position destination) { + // Given + Rook rook = new Rook(PieceColor.WHITE); + Map piecePositions = Map.of(position(File.D, Rank.TWO), new Rook(PieceColor.BLACK)); + Board board = board(piecePositions); + + // When & Then + assertThatCode(() -> rook.move(source, destination, board)) + .doesNotThrowAnyException(); + } + + private static Stream checkMovableTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.SIX)), + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.ONE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.TWO)), + Arguments.of(position(File.B, Rank.TWO), position(File.D, Rank.TWO)) + ); + } + + @DisplayName("Rook이 이동할 수 없는 방향의 도착지가 입력되면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenInvalidDirectionTestCase") + @ParameterizedTest + void throwExceptionWhenInvalidDirectionTest(final Position source, final Position destination) { + // Given + Rook rook = new Rook(PieceColor.WHITE); + Map piecePositions = Collections.emptyMap(); + Board board = board(piecePositions); + + // When & Then + assertThatThrownBy(() ->rook.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("룩이 이동할 수 있는 방향이 아닙니다."); + } + + private static Stream throwExceptionWhenInvalidDirectionTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.E, Rank.FIVE)), + Arguments.of(position(File.B, Rank.TWO), position(File.G, Rank.SEVEN)) + ); + } + + @DisplayName("이동 경로에 기물이 존재하면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenPathsHasPieceTestCase") + @ParameterizedTest + void throwExceptionWhenPathsHasPieceTest(final Position source, final Position destination) { + // Given + Rook rook = new Rook(PieceColor.WHITE); + Map piecePositions = Map.of( + position(File.B, Rank.FOUR), new Rook(PieceColor.BLACK), + position(File.D, Rank.TWO), new Rook(PieceColor.WHITE) + ); + Board board = board(piecePositions); + + // When & Then + assertThatThrownBy(() -> rook.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("목적지 경로에 기물이 존재하여 이동할 수 없습니다."); + } + + private static Stream throwExceptionWhenPathsHasPieceTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.SIX)), + Arguments.of(position(File.B, Rank.TWO), position(File.G, Rank.TWO)) + ); + } + + @DisplayName("도착지에 아군 기물이 존재하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenDestinationHasTeamPieceTest() { + // Given + Position source = position(File.B, Rank.TWO); + Position destination = position(File.B, Rank.SIX); + Rook rook = new Rook(PieceColor.WHITE); + Map piecePositions = Map.of(destination, new Rook(PieceColor.WHITE)); + Board board = board(piecePositions); + + // When & Then + assertThatThrownBy(() -> rook.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } + + private static Board board(final Map piecePositions) { + return new Board(new TestPieceDao(), piecePositions); + } +} diff --git a/src/test/java/domain/position/CommonMovementDirectionTest.java b/src/test/java/domain/position/CommonMovementDirectionTest.java new file mode 100644 index 00000000000..7d0bf22eb45 --- /dev/null +++ b/src/test/java/domain/position/CommonMovementDirectionTest.java @@ -0,0 +1,85 @@ +package domain.position; + +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.piece.CommonMovementDirection; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static domain.board.File.A; +import static domain.board.File.B; +import static domain.board.File.C; +import static domain.board.File.D; +import static domain.board.File.E; +import static domain.board.File.F; +import static domain.board.File.G; +import static domain.board.Rank.EIGHT; +import static domain.board.Rank.FIVE; +import static domain.board.Rank.FOUR; +import static domain.board.Rank.ONE; +import static domain.board.Rank.SIX; +import static domain.board.Rank.TWO; +import static domain.piece.CommonMovementDirection.DOWN; +import static domain.piece.CommonMovementDirection.DOWN_LEFT; +import static domain.piece.CommonMovementDirection.DOWN_RIGHT; +import static domain.piece.CommonMovementDirection.LEFT; +import static domain.piece.CommonMovementDirection.RIGHT; +import static domain.piece.CommonMovementDirection.UP; +import static domain.piece.CommonMovementDirection.UP_LEFT; +import static domain.piece.CommonMovementDirection.UP_RIGHT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CommonMovementDirectionTest { + + @DisplayName("입력받은 출발지/목적지의 거리를 바탕으로 이동방향을 반환한다.") + @MethodSource("findMovementDirectionTestCase") + @ParameterizedTest + void findMovementDirection(final Position source, final Position destination, final CommonMovementDirection expect) { + // When + CommonMovementDirection commonMovementDirection = CommonMovementDirection.calculateDirection(source, destination); + + // Then + assertThat(commonMovementDirection).isEqualTo(expect); + } + + private static Stream findMovementDirectionTestCase() { + return Stream.of( + Arguments.of(position(B, TWO), position(B, SIX), UP), + Arguments.of(position(B, FIVE), position(B, TWO), DOWN), + Arguments.of(position(B, TWO), position(G, TWO), RIGHT), + Arguments.of(position(E, FIVE), position(B, FIVE), LEFT), + Arguments.of(position(C, TWO), position(E, FOUR), UP_RIGHT), + Arguments.of(position(F, TWO), position(C, FIVE), UP_LEFT), + Arguments.of(position(D, FOUR), position(F, TWO), DOWN_RIGHT), + Arguments.of(position(E, FOUR), position(C, TWO), DOWN_LEFT) + ); + } + + @DisplayName("방향 계산이 불가능한 출발지/목적지 위치 정보가 입력되면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenInvalidSourceAndDestinationTestCase") + @ParameterizedTest + void throwExceptionWhenInvalidSourceAndDestination(final Position source, final Position destination) { + // When & THen + assertThatThrownBy(() -> CommonMovementDirection.calculateDirection(source, destination)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("상/하/좌/우 혹은 대각선으로 이동할 수 없는 칸입니다."); + } + + private static Stream throwExceptionWhenInvalidSourceAndDestinationTestCase() { + return Stream.of( + Arguments.of(position(B, TWO), position(B, TWO)), + Arguments.of(position(B, ONE), position(C, FOUR)), + Arguments.of(position(A, EIGHT), position(G, ONE)) + ); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/test/java/domain/position/FileTest.java b/src/test/java/domain/position/FileTest.java new file mode 100644 index 00000000000..56b8f21370f --- /dev/null +++ b/src/test/java/domain/position/FileTest.java @@ -0,0 +1,36 @@ +package domain.position; + +import domain.board.File; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class FileTest { + + @DisplayName("인덱스를 입력하면 해당하는 File을 반환한다.") + @Test + void getFileTest() { + // Given + final int index = 2; + + // When + File file = File.of(index); + + // Then + assertThat(file).isEqualTo(File.C); + } + + @DisplayName("유효하지 않은 인덱스를 입력하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenInputInvalidIndex() { + // Given + final int index = 13; + + // When & Then + assertThatThrownBy(() -> File.of(index)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("유효하지 않은 인덱스입니다."); + } +} diff --git a/src/test/java/domain/position/KnightMovementDirectionTest.java b/src/test/java/domain/position/KnightMovementDirectionTest.java new file mode 100644 index 00000000000..a322b2a0ecb --- /dev/null +++ b/src/test/java/domain/position/KnightMovementDirectionTest.java @@ -0,0 +1,95 @@ +package domain.position; + +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.piece.KnightMovementDirection; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static domain.board.File.A; +import static domain.board.File.B; +import static domain.board.File.C; +import static domain.board.File.D; +import static domain.board.File.E; +import static domain.board.File.F; +import static domain.board.File.G; +import static domain.board.Rank.EIGHT; +import static domain.board.Rank.FIVE; +import static domain.board.Rank.FOUR; +import static domain.board.Rank.ONE; +import static domain.board.Rank.SIX; +import static domain.board.Rank.THREE; +import static domain.board.Rank.TWO; +import static domain.piece.KnightMovementDirection.DOWN_LEFT; +import static domain.piece.KnightMovementDirection.DOWN_RIGHT; +import static domain.piece.KnightMovementDirection.LEFT_DOWN; +import static domain.piece.KnightMovementDirection.LEFT_UP; +import static domain.piece.KnightMovementDirection.RIGHT_DOWN; +import static domain.piece.KnightMovementDirection.RIGHT_UP; +import static domain.piece.KnightMovementDirection.UP_LEFT; +import static domain.piece.KnightMovementDirection.UP_RIGHT; +import static domain.piece.KnightMovementDirection.calculateDirection; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class KnightMovementDirectionTest { + + @DisplayName("입력된 출발지 & 목적지를 계산해서 나이트의 이동 방향을 반환한다.") + @MethodSource("calculateDirectionTestCase") + @ParameterizedTest + void calculateDirectionTest(final Position source, final Position destination, final KnightMovementDirection expect) { + // When + KnightMovementDirection knightMovementDirection = calculateDirection(source, destination); + + // Then + assertThat(knightMovementDirection).isEqualTo(expect); + } + + private static Stream calculateDirectionTestCase() { + return Stream.of( + Arguments.of(position(C, FOUR), position(D, SIX), UP_RIGHT), + Arguments.of(position(C, FOUR), position(B, SIX), UP_LEFT), + Arguments.of(position(D, FIVE), position(E, THREE), DOWN_RIGHT), + Arguments.of(position(D, FIVE), position(C, THREE), DOWN_LEFT), + Arguments.of(position(C, FOUR), position(E, FIVE), RIGHT_UP), + Arguments.of(position(C, FOUR), position(E, THREE), RIGHT_DOWN), + Arguments.of(position(E, FOUR), position(C, FIVE), LEFT_UP), + Arguments.of(position(E, FOUR), position(C, THREE), LEFT_DOWN) + ); + } + + @DisplayName("나이트가 이동할 수 없는 출발지/도착지 위치 정보가 입력되면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenInvalidLocationTestCase") + @ParameterizedTest + void throwExceptionWhenInvalidLocationTest(final Position source, final Position destination) { + // When & Then + assertThatThrownBy(() -> calculateDirection(source, destination)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("나이트가 이동할 수 없는 방향입니다."); + } + + private static Stream throwExceptionWhenInvalidLocationTestCase() { + return Stream.of( + Arguments.of(position(B, TWO), position(B, SIX)), + Arguments.of(position(B, FIVE), position(B, TWO)), + Arguments.of(position(B, TWO), position(G, TWO)), + Arguments.of(position(E, FIVE), position(B, FIVE)), + Arguments.of(position(C, TWO), position(E, FOUR)), + Arguments.of(position(F, TWO), position(C, FIVE)), + Arguments.of(position(D, FOUR), position(F, TWO)), + Arguments.of(position(E, FOUR), position(C, TWO)), + Arguments.of(position(B, TWO), position(B, TWO)), + Arguments.of(position(B, ONE), position(C, FOUR)), + Arguments.of(position(A, EIGHT), position(G, ONE)) + ); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/test/java/domain/position/PawnMovementDirectionTest.java b/src/test/java/domain/position/PawnMovementDirectionTest.java new file mode 100644 index 00000000000..fa1dde372fd --- /dev/null +++ b/src/test/java/domain/position/PawnMovementDirectionTest.java @@ -0,0 +1,100 @@ +package domain.position; + +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.piece.PawnMovementDirection; +import domain.piece.PieceColor; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static domain.board.File.A; +import static domain.board.File.B; +import static domain.board.File.C; +import static domain.board.File.D; +import static domain.board.File.E; +import static domain.board.Rank.FIVE; +import static domain.board.Rank.FOUR; +import static domain.board.Rank.ONE; +import static domain.board.Rank.SEVEN; +import static domain.board.Rank.SIX; +import static domain.board.Rank.THREE; +import static domain.board.Rank.TWO; +import static domain.piece.PawnMovementDirection.DOWN_LEFT; +import static domain.piece.PawnMovementDirection.DOWN_ONE_STEP; +import static domain.piece.PawnMovementDirection.DOWN_RIGHT; +import static domain.piece.PawnMovementDirection.DOWN_TWO_STEP; +import static domain.piece.PawnMovementDirection.UP_LEFT; +import static domain.piece.PawnMovementDirection.UP_ONE_STEP; +import static domain.piece.PawnMovementDirection.UP_RIGHT; +import static domain.piece.PawnMovementDirection.UP_TWO_STEP; +import static domain.piece.PawnMovementDirection.calculateDirection; +import static domain.piece.PieceColor.BLACK; +import static domain.piece.PieceColor.WHITE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PawnMovementDirectionTest { + + @DisplayName("입력된 출발지 & 목적지를 계산해서 폰의 이동 방향을 반환한다.") + @MethodSource("calculateDirectionTestCase") + @ParameterizedTest + void calculateDirectionTest(final PieceColor pieceColor, final Position source, final Position destination, final PawnMovementDirection expect) { + // When + PawnMovementDirection movementDirection = calculateDirection(pieceColor, source, destination); + + // Then + assertThat(movementDirection).isEqualTo(expect); + } + + private static Stream calculateDirectionTestCase() { + return Stream.of( + Arguments.of(WHITE, position(C, TWO), position(C, FOUR), UP_TWO_STEP), + Arguments.of(WHITE, position(C, FOUR), position(C, FIVE), UP_ONE_STEP), + Arguments.of(WHITE, position(D, FIVE), position(E, SIX), UP_RIGHT), + Arguments.of(WHITE, position(D, FIVE), position(C, SIX), UP_LEFT), + Arguments.of(BLACK, position(C, SEVEN), position(C, FIVE), DOWN_TWO_STEP), + Arguments.of(BLACK, position(C, FOUR), position(C, THREE), DOWN_ONE_STEP), + Arguments.of(BLACK, position(D, FIVE), position(E, FOUR), DOWN_RIGHT), + Arguments.of(BLACK, position(D, FIVE), position(C, FOUR), DOWN_LEFT) + ); + } + + @DisplayName("색상별 방향을 계산할 수 없는 춮발지 & 목적지가 입력되면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenInputInvalidPositionTestCase") + @ParameterizedTest + void throwExceptionWhenInputInvalidPositionTest(final PieceColor pieceColor, final Position source, final Position destination) { + // When & Then + assertThatThrownBy(() -> calculateDirection(pieceColor, source, destination)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(pieceColor.name() + "색상의 폰이 이동할 수 없는 방향입니다."); + } + + private static Stream throwExceptionWhenInputInvalidPositionTestCase() { + return Stream.of( + Arguments.of(BLACK, position(C, TWO), position(C, FOUR)), + Arguments.of(BLACK, position(C, FOUR), position(C, FIVE)), + Arguments.of(BLACK, position(D, FIVE), position(E, SIX)), + Arguments.of(BLACK, position(D, FIVE), position(C, SIX)), + Arguments.of(WHITE, position(C, SEVEN), position(C, FIVE)), + Arguments.of(WHITE, position(C, FOUR), position(C, THREE)), + Arguments.of(WHITE, position(D, FIVE), position(E, FOUR)), + Arguments.of(WHITE, position(D, FIVE), position(C, FOUR)), + Arguments.of(WHITE, position(B, TWO), position(B, SIX)), + Arguments.of(WHITE, position(B, TWO), position(B, ONE)), + Arguments.of(WHITE, position(B, TWO), position(A, TWO)), + Arguments.of(WHITE, position(B, TWO), position(C, ONE)), + Arguments.of(BLACK, position(B, TWO), position(D, TWO)), + Arguments.of(BLACK, position(B, TWO), position(E, FIVE)), + Arguments.of(BLACK, position(B, TWO), position(A, THREE)) + ); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/test/java/domain/position/PositionTest.java b/src/test/java/domain/position/PositionTest.java new file mode 100644 index 00000000000..121870d3032 --- /dev/null +++ b/src/test/java/domain/position/PositionTest.java @@ -0,0 +1,41 @@ +package domain.position; + +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.piece.CommonMovementDirection; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PositionTest { + @DisplayName("행과 열 정보를 가진 Position 인스턴스를 생성한다.") + @Test + void createPositionTest() { + // Given + File file = File.A; + Rank rank = Rank.ONE; + + // When + Position position = new Position(file, rank); + + // Then + assertThat(position.rank()).isEqualTo(rank); + assertThat(position.file()).isEqualTo(file); + } + + @DisplayName("이동 방향을 전달하면 새로운 위치의 Position을 반환한다.") + @Test + void addPositionTest() { + // Given + Position position = new Position(File.D, Rank.TWO); + + // When + Position newPosition = position.next(CommonMovementDirection.UP_RIGHT); + + // Then + assertThat(newPosition.file()).isEqualTo(File.E); + assertThat(newPosition.rank()).isEqualTo(Rank.THREE); + } +} diff --git a/src/test/java/domain/position/RankTest.java b/src/test/java/domain/position/RankTest.java new file mode 100644 index 00000000000..6411e9401c9 --- /dev/null +++ b/src/test/java/domain/position/RankTest.java @@ -0,0 +1,36 @@ +package domain.position; + +import domain.board.Rank; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RankTest { + + @DisplayName("인덱스를 입력하면 해당하는 Rank를 반환한다.") + @Test + void getFileTest() { + // Given + final int index = 2; + + // When + Rank file = Rank.of(index); + + // Then + assertThat(file).isEqualTo(Rank.SIX); + } + + @DisplayName("유효하지 않은 인덱스를 입력하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenInputInvalidIndex() { + // Given + final int index = 13; + + // When & Then + assertThatThrownBy(() -> Rank.of(index)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("유효하지 않은 인덱스입니다."); + } +}