diff --git a/.gitignore b/.gitignore index 6c018781387..96711b991d8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ out/ ### VS Code ### .vscode/ + +### dockder ### +docker/ diff --git a/README.md b/README.md index 8102f91c870..0371d567c28 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,18 @@ 체스 미션 저장소 + +### [기능 요구 사항 바로가기](https://github.com/reddevilmidzy/java-chess/blob/62e632aa09435db4f69502e663a0cc7ef78a31bb/docs/README.md) + +
+ +### [product db 스키마](https://github.com/reddevilmidzy/java-chess/blob/0f6324e83c0fef95c84bd95c5dba8df742a8b613/src/main/resources0) + +
+ +### [test db 스키마](https://github.com/reddevilmidzy/java-chess/blob/0f6324e83c0fef95c84bd95c5dba8df742a8b613/src/test/resources) + + ## 우아한테크코스 코드리뷰 - [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) diff --git a/build.gradle b/build.gradle index 3697236c6fb..8c605072e94 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ 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/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000000..558a1d5a53f --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.9" +services: + db: + image: mysql:8.0.28 + platform: linux/x86_64 + restart: always + ports: + - "13306:3306" + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: chess + MYSQL_USER: user + MYSQL_PASSWORD: password + TZ: Asia/Seoul + volumes: + - ./db/mysql/data:/var/lib/mysql + - ./db/mysql/config:/etc/mysql/conf.d + - ./db/mysql/init:/docker-entrypoint-initdb.d diff --git a/docs/README.md b/docs/README.md index ee8506d8cf0..af5fa595726 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,48 @@ # 체스 미션 +## DB 연동 + +### Docker 실행하기 + +```bash +docker-compose -p chess up -d +``` + +### DB 스키마 + +```sql +use chess; + +drop table if exists board; + +create table board +( + position varchar(2) not null, + piece_type varchar(6) not null, + camp varchar(5) not null + +); + +drop table if exists moving; + +create table moving +( + movement_id INT primary key auto_increment, + camp varchar(5) not null, + start varchar(2) not null, + destination varchar(2) not null +); + +drop table if exists turn; + +create table turn +( + camp varchar(5) not null, + count int not null +); +``` + + ## 기능 요구 사항 * [x] 8 * 8 의 체스 판 생성 @@ -16,7 +59,22 @@ * [x] 기물의 이동 규칙을 벗어나는 경우 예외 발생 * [x] 현재 위치로 빈 칸을 입력받은 경우 예외 발생 * [x] 현재 턴이 아닌 기물을 이동하는 경우 예외 발생 +* [x] status 기능 구현 + * [x] 남아있는 기물의 점수 계산 + * queen: 9 + * rook: 5 + * bishop 3 + * knight: 2.5 + * pawn: 1 + * [x] 같은 세로줄에 있는 같은 pawn인 경우 0.5 + * [x] 진영의 점수를 출력 + * [x] 승리한 진영의 결과 확인 +* [x] 킹이 잡히면 게임 종료 * [x] end 가 입력되면 프로그램 종료 +* [x] 예외 시 재입력 +* [x] 진행중인 게임 end 입력 후 재시작시 이전 게임 실행 +* [x] quit 입력시 저장하지 않고 게임 종료 +* [x] db 예외 처리 ## 기물의 이동 기능 @@ -37,18 +95,12 @@ * start: 게임을 시작하는 명령어 * 게임 시작은 start 명령어만 가능하다 -* end: 게임을 종료하는 명령어 +* end: 게임을 저장 후 종료하는 명령어 +* quit: 게임을 저장하지 않고 종료하는 명령어 * move: 게임 중 기물을 움직이는 명령어 * 게임이 시작되지 않았는데 move 명령어를 입력한다면 예외가 발생한다. * 형식은 move [a-h1-8] [a-h1-8] 형태이고 아닐시 예외가 발생한다. - - -## 리팩터링 - -* [x] 테스트 코드 보충 -* [x] 프로그래밍 요구 사항 지키기 -* [x] 예외 메시지를 커스텀 예외로 만들기 -* [x] 예외 시 재입력 구현하기 +* status: 현재 점수를 확인하는 명령어 ## 우선순위가 낮지만 구현해보고 싶은 기능 @@ -57,5 +109,5 @@ * [ ] 프로모션 * [ ] 캐슬링 * 체크 상황 - * [ ] 체크 메이트면 자동으로 게임 종료 * [ ] 체크 상황이면 체크를 피하는 방향으로만 이동 가능 +* [ ] log 입력시 현재까지 기보 출력 diff --git a/src/main/java/application/Application.java b/src/main/java/application/Application.java index 4bf227f5dfb..baf4c8e727d 100644 --- a/src/main/java/application/Application.java +++ b/src/main/java/application/Application.java @@ -1,6 +1,7 @@ package application; import controller.ChessController; +import db.exception.DBException; import java.util.Scanner; import view.InputView; import view.OutputView; @@ -12,6 +13,10 @@ public static void main(String[] args) { final OutputView outputView = new OutputView(); final ChessController chessController = new ChessController(inputView, outputView); - chessController.run(); + try { + chessController.run(); + } catch (DBException exception) { + outputView.printException(exception.getErrorCode()); + } } } diff --git a/src/main/java/constant/ErrorCode.java b/src/main/java/constant/ErrorCode.java index a1892cc1243..60a3d44b9f4 100644 --- a/src/main/java/constant/ErrorCode.java +++ b/src/main/java/constant/ErrorCode.java @@ -1,15 +1,24 @@ package constant; public enum ErrorCode { - INVALID_INPUT, INVALID_STATUS, INVALID_COMMAND, INVALID_POSITION, - INVALID_MOVEMENT_RULE, + INVALID_PAWN_MOVEMENT, + INVALID_KING_MOVEMENT, + INVALID_BISHOP_MOVEMENT, + INVALID_KNIGHT_MOVEMENT, + INVALID_QUEEN_MOVEMENT, + INVALID_ROOK_MOVEMENT, PIECE_EXIST_IN_ROUTE, + OWN_PIECE_EXIST_POSITION, PIECE_DOES_NOT_EXIST_POSITION, INVALID_CAMP_PIECE, - NO_MESSAGE; - + KING_DEAD, + CONNECTION, + FAIL_SAVE, + FAIL_FIND, + FAIL_DELETE, + NO_MESSAGE } diff --git a/src/main/java/controller/ChessController.java b/src/main/java/controller/ChessController.java index 61ed937e0e5..4d235ba49b9 100644 --- a/src/main/java/controller/ChessController.java +++ b/src/main/java/controller/ChessController.java @@ -1,13 +1,20 @@ package controller; +import db.Repository; +import db.dto.BoardDto; +import db.dto.MovingDto; +import db.dto.TurnDto; import dto.ChessBoardDto; +import dto.ScoreDto; import exception.CustomException; -import java.util.Collections; import java.util.List; -import model.ChessBoard; -import model.Command; +import model.Board; +import model.Camp; +import model.ChessGame; +import model.Turn; +import model.command.CommandLine; import model.status.GameStatus; -import model.status.Initialization; +import model.status.StatusFactory; import view.InputView; import view.OutputView; @@ -15,6 +22,7 @@ public class ChessController { private final InputView inputView; private final OutputView outputView; + private final Repository repository = new Repository("chess"); public ChessController(final InputView inputView, final OutputView outputView) { this.inputView = inputView; @@ -22,55 +30,89 @@ public ChessController(final InputView inputView, final OutputView outputView) { } public void run() { - final ChessBoard chessBoard = ChessBoard.setupStartingPosition(); outputView.printStartMessage(); GameStatus gameStatus = initGame(); - + final ChessGame chessGame = create(); + if (gameStatus.isRunning()) { + outputView.printChessBoard(ChessBoardDto.from(chessGame)); + } while (gameStatus.isRunning()) { - printCurrentStatus(chessBoard); - gameStatus = play(gameStatus, chessBoard); + gameStatus = play(gameStatus, chessGame); } + save(gameStatus, chessGame); + } + + private void save(final GameStatus gameStatus, final ChessGame chessGame) { + if (gameStatus.isCheck() || gameStatus.isQuit()) { + repository.removeAll(); + return; + } + final Board board = chessGame.getBoard(); + final Camp camp = chessGame.getCamp(); + final Turn turn = chessGame.getTurn(); + final BoardDto boardDto = BoardDto.from(board); + final TurnDto turnDto = TurnDto.from(camp, turn); + repository.save(boardDto, turnDto); + } + + private ChessGame create() { + if (repository.hasGame()) { + return repository.findGame(); + } + return ChessGame.setupStartingPosition(); } private GameStatus initGame() { try { - return Initialization.gameSetting(getCommand()); + return StatusFactory.create(readCommandLine()); } catch (final CustomException exception) { outputView.printException(exception.getErrorCode()); return initGame(); } } - private GameStatus play(final GameStatus gameStatus, final ChessBoard chessBoard) { + private GameStatus play(final GameStatus preStatus, final ChessGame chessGame) { try { - return gameStatus.play(getCommand(), chessBoard); - } catch (CustomException exception) { + final CommandLine commandLine = readCommandLine(); + final GameStatus status = preStatus.play(commandLine, chessGame); + saveMoving(chessGame, commandLine); + print(status, commandLine, chessGame); + return status; + } catch (final CustomException exception) { outputView.printException(exception.getErrorCode()); - return play(gameStatus, chessBoard); + return play(preStatus, chessGame); } } - private void printCurrentStatus(final ChessBoard chessBoard) { - outputView.printChessBoard(ChessBoardDto.from(chessBoard)); - outputView.printCamp(chessBoard.getCamp()); + private void saveMoving(final ChessGame chessGame, final CommandLine commandLine) { + if (commandLine.isMove()) { + final List body = commandLine.getBody(); + final Camp camp = chessGame.getCamp().toggle(); + final MovingDto movingDto = MovingDto.from(body, camp); + repository.saveMoving(movingDto); + } } - private List getCommand() { - List command = Collections.emptyList(); - while (command.isEmpty()) { - command = readCommand(); + private void print(final GameStatus gameStatus, final CommandLine commandLine, final ChessGame chessGame) { + if (gameStatus.isCheck()) { + outputView.printWinner(chessGame.getCamp().toString()); + return; + } + if (commandLine.isStatus()) { + outputView.printScore(ScoreDto.from(chessGame)); + } + if (commandLine.isStart() || commandLine.isMove()) { + outputView.printChessBoard(ChessBoardDto.from(chessGame)); } - return command; } - private List readCommand() { + private CommandLine readCommandLine() { try { - List command = inputView.readCommandList(); - Command.validate(command); - return command; - } catch (CustomException exception) { + final List command = inputView.readCommandList(); + return CommandLine.from(command); + } catch (final CustomException exception) { outputView.printException(exception.getErrorCode()); } - return Collections.emptyList(); + return readCommandLine(); } } diff --git a/src/main/java/db/BoardDao.java b/src/main/java/db/BoardDao.java new file mode 100644 index 00000000000..a84c9183bbd --- /dev/null +++ b/src/main/java/db/BoardDao.java @@ -0,0 +1,92 @@ +package db; + +import constant.ErrorCode; +import db.connection.DBConnectionUtil; +import db.dto.BoardDto; +import db.dto.PieceDto; +import db.dto.PositionDto; +import db.exception.DaoException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import model.Board; + +public class BoardDao { + + private final String database; + + public BoardDao(final String database) { + this.database = database; + } + + public void saveBoard(final BoardDto board) { + final Map pieces = board.pieces(); + + for (final Entry entry : pieces.entrySet()) { + savePosition(entry.getKey(), entry.getValue()); + } + } + + private void savePosition(final PositionDto position, final PieceDto piece) { + final String query = "INSERT INTO board VALUES(?, ?, ?)"; + try (final Connection connection = DBConnectionUtil.getConnection(database); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setString(1, position.value()); + preparedStatement.setString(2, piece.type()); + preparedStatement.setString(3, piece.camp()); + preparedStatement.executeUpdate(); + } catch (final SQLException exception) { + throw new DaoException(ErrorCode.FAIL_SAVE); + } + } + + public BoardDto find() { + final String query = "SELECT * FROM board"; + try (final Connection connection = DBConnectionUtil.getConnection(database); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + final ResultSet resultSet = preparedStatement.executeQuery(); + return convert(resultSet); + } catch (final SQLException exception) { + throw new DaoException(ErrorCode.FAIL_FIND); + } + } + + private BoardDto convert(final ResultSet resultSet) throws SQLException { + final Map result = new HashMap<>(); + while (resultSet.next()) { + final PieceDto piece = convertToPiece(resultSet); + final PositionDto positionDto = convertToPosition(resultSet); + result.put(positionDto, piece); + } + if (result.isEmpty()) { + return BoardDto.from(Board.create()); + } + return new BoardDto(result); + } + + private PieceDto convertToPiece(final ResultSet resultSet) throws SQLException { + final String type = resultSet.getString("piece_type"); + final String camp = resultSet.getString("camp"); + return new PieceDto(type, camp); + } + + private PositionDto convertToPosition(final ResultSet resultSet) throws SQLException { + final String position = resultSet.getString("position"); + return new PositionDto(position); + } + + public void remove() { + final String query = "TRUNCATE TABLE board"; + try (final Connection connection = DBConnectionUtil.getConnection(database); + final PreparedStatement preparedStatement = connection.prepareStatement(query) + ) { + preparedStatement.executeUpdate(); + } catch (final SQLException exception) { + throw new DaoException(ErrorCode.FAIL_DELETE); + } + } +} diff --git a/src/main/java/db/MovingDao.java b/src/main/java/db/MovingDao.java new file mode 100644 index 00000000000..edb3551b6ea --- /dev/null +++ b/src/main/java/db/MovingDao.java @@ -0,0 +1,123 @@ +package db; + +import constant.ErrorCode; +import db.connection.DBConnectionUtil; +import db.dto.MovingDto; +import db.exception.DaoException; +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 MovingDao { + + private final String database; + + public MovingDao(final String database) { + this.database = database; + } + + public long addMoving(final MovingDto moving) { + final String query = "INSERT INTO moving VALUES(?, ?, ?, ?)"; + try (final Connection connection = DBConnectionUtil.getConnection(database); + final PreparedStatement preparedStatement = connection.prepareStatement(query, + Statement.RETURN_GENERATED_KEYS)) { + preparedStatementSet(moving, preparedStatement); + preparedStatement.executeUpdate(); + return increaseKey(preparedStatement.getGeneratedKeys()); + } catch (final SQLException exception) { + throw new DaoException(ErrorCode.FAIL_SAVE); + } + } + + private long increaseKey(final ResultSet generatedKeys) throws SQLException { + if (generatedKeys.next()) { + return generatedKeys.getLong(1); + } + return 0; + } + + private void preparedStatementSet(final MovingDto moving, final PreparedStatement preparedStatement) + throws SQLException { + preparedStatement.setNull(1, 0); + preparedStatement.setString(2, moving.camp()); + preparedStatement.setString(3, moving.current()); + preparedStatement.setString(4, moving.next()); + } + + public List findAll() { + final String query = "SELECT * FROM moving"; + try (final Connection connection = DBConnectionUtil.getConnection(database); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + final ResultSet resultSet = preparedStatement.executeQuery(); + return convert(resultSet); + } catch (SQLException exception) { + throw new DaoException(ErrorCode.FAIL_FIND); + } + } + + private List convert(final ResultSet resultSet) throws SQLException { + final List moving = new ArrayList<>(); + while (resultSet.next()) { + String camp = resultSet.getString("camp"); + String current = resultSet.getString("start"); + String next = resultSet.getString("destination"); + moving.add(new MovingDto(camp, current, next)); + } + return moving; + } + + public int countMoving() { + final String query = "SELECT count(*) AS count FROM moving"; + try (final Connection connection = DBConnectionUtil.getConnection(database); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + final ResultSet resultSet = preparedStatement.executeQuery(); + return convertToCount(resultSet); + } catch (final SQLException exception) { + throw new DaoException(ErrorCode.FAIL_FIND); + } + } + + private int convertToCount(final ResultSet resultSet) throws SQLException { + if (resultSet.next()) { + return resultSet.getInt("count"); + } + throw new DaoException(ErrorCode.FAIL_FIND); + } + + public MovingDto findByMovementId(final long movementId) { + final String query = "SELECT * FROM moving WHERE movement_id = ?"; + try (final Connection connection = DBConnectionUtil.getConnection(database); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setLong(1, movementId); + final ResultSet resultSet = preparedStatement.executeQuery(); + return convertToMoving(resultSet); + } catch (final SQLException exception) { + throw new DaoException(ErrorCode.FAIL_FIND); + } + } + + private MovingDto convertToMoving(final ResultSet resultSet) throws SQLException { + if (resultSet.next()) { + return new MovingDto( + resultSet.getString("camp"), + resultSet.getString("start"), + resultSet.getString("destination") + ); + } + throw new DaoException(ErrorCode.FAIL_FIND); + } + + public void remove() { + final String query = "TRUNCATE TABLE moving"; + try (final Connection connection = DBConnectionUtil.getConnection(database); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.executeUpdate(); + } catch (final SQLException exception) { + throw new DaoException(ErrorCode.FAIL_DELETE); + } + } +} diff --git a/src/main/java/db/Repository.java b/src/main/java/db/Repository.java new file mode 100644 index 00000000000..64dd8b46ba6 --- /dev/null +++ b/src/main/java/db/Repository.java @@ -0,0 +1,87 @@ +package db; + +import db.dto.BoardDto; +import db.dto.MovingDto; +import db.dto.TurnDto; +import java.util.List; +import model.Board; +import model.Camp; +import model.ChessGame; +import model.GameTurn; +import model.Turn; +import model.position.Moving; +import model.position.Position; + +public class Repository { + + private final MovingDao movingDao; + private final TurnDao turnDao; + private final BoardDao boardDao; + + public Repository(final String database) { + this.movingDao = new MovingDao(database); + this.turnDao = new TurnDao(database); + this.boardDao = new BoardDao(database); + } + + public void removeAll() { + turnDao.remove(); + boardDao.remove(); + movingDao.remove(); + } + + public boolean hasGame() { + return movingDao.countMoving() > 0; + } + + public void save(final BoardDto board, final TurnDto turnDto) { + boardDao.remove(); + boardDao.saveBoard(board); + turnDao.remove(); + turnDao.saveTurn(turnDto); + } + + public void saveMoving(final MovingDto moving) { + movingDao.addMoving(moving); + } + + public ChessGame findGame() { + final BoardDto findBoard = findBoard(); + final TurnDto findTurn = findTurn(); + final List findMoving = movingDao.findAll(); + final Board board = findBoard.toBoard(); + if (unsaved(findTurn, findMoving)) { + restore(findTurn, findMoving, board); + } + final Camp camp = findLastTurnCamp(findMoving); + final GameTurn gameTurn = new GameTurn(camp, new Turn(findMoving.size())); + return new ChessGame(board, gameTurn); + } + + private BoardDto findBoard() { + return boardDao.find(); + } + + private TurnDto findTurn() { + return turnDao.findTurn(); + } + + private boolean unsaved(final TurnDto findTurn, final List findMoving) { + return findTurn.count() < findMoving.size(); + } + + private Camp findLastTurnCamp(final List findMoving) { + if (findMoving.size() % 2 == 0) { + return Camp.WHITE; + } + return Camp.BLACK; + } + + private void restore(final TurnDto findTurn, final List findMoving, final Board board) { + for (int i = findTurn.count(); i < findMoving.size(); i++) { + final MovingDto movingDto = findMoving.get(i); + final Moving moving = new Moving(Position.from(movingDto.current()), Position.from(movingDto.next())); + board.move(moving); + } + } +} diff --git a/src/main/java/db/TurnDao.java b/src/main/java/db/TurnDao.java new file mode 100644 index 00000000000..99542af9855 --- /dev/null +++ b/src/main/java/db/TurnDao.java @@ -0,0 +1,59 @@ +package db; + +import constant.ErrorCode; +import db.connection.DBConnectionUtil; +import db.dto.TurnDto; +import db.exception.DaoException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class TurnDao { + + private final String database; + + public TurnDao(final String database) { + this.database = database; + } + + public void saveTurn(final TurnDto turnDto) { + final String query = "INSERT INTO turn values(?, ?)"; + try (final Connection connection = DBConnectionUtil.getConnection(database); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setString(1, turnDto.currentCamp()); + preparedStatement.setInt(2, turnDto.count()); + preparedStatement.executeUpdate(); + } catch (final SQLException exception) { + throw new DaoException(ErrorCode.FAIL_SAVE); + } + } + + public TurnDto findTurn() { + final String query = "SELECT * FROM turn"; + try (final Connection connection = DBConnectionUtil.getConnection(database); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + final ResultSet resultSet = preparedStatement.executeQuery(); + return convertToTurn(resultSet); + } catch (SQLException exception) { + throw new DaoException(ErrorCode.FAIL_FIND); + } + } + + private TurnDto convertToTurn(final ResultSet resultSet) throws SQLException { + if (resultSet.next()) { + return new TurnDto(resultSet.getString("camp"), resultSet.getInt("count")); + } + return new TurnDto("WHITE", 0); + } + + public void remove() { + final String query = "TRUNCATE TABLE turn"; + try (final Connection connection = DBConnectionUtil.getConnection(database); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.executeUpdate(); + } catch (final SQLException exception) { + throw new DaoException(ErrorCode.FAIL_DELETE); + } + } +} diff --git a/src/main/java/db/connection/DBConnectionUtil.java b/src/main/java/db/connection/DBConnectionUtil.java new file mode 100644 index 00000000000..a3d607fdeb0 --- /dev/null +++ b/src/main/java/db/connection/DBConnectionUtil.java @@ -0,0 +1,27 @@ +package db.connection; + +import constant.ErrorCode; +import db.exception.ConnectionException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class DBConnectionUtil { + + private static final String SERVER = "localhost:13306"; + private static final String OPTION = "?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"; + private static final String USERNAME = "root"; + private static final String PASSWORD = "root"; + + private DBConnectionUtil() { + } + + public static Connection getConnection(final String database) { + try { + final String url = "jdbc:mysql://" + SERVER + "/" + database + OPTION; + return DriverManager.getConnection(url, USERNAME, PASSWORD); + } catch (final SQLException exception) { + throw new ConnectionException(ErrorCode.CONNECTION); + } + } +} diff --git a/src/main/java/db/dto/BoardDto.java b/src/main/java/db/dto/BoardDto.java new file mode 100644 index 00000000000..70a96506204 --- /dev/null +++ b/src/main/java/db/dto/BoardDto.java @@ -0,0 +1,31 @@ +package db.dto; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import model.Board; +import model.piece.Piece; +import model.position.Position; + +public record BoardDto(Map pieces) { + + public static BoardDto from(final Board board) { + final Map result = new HashMap<>(); + final Map pieces = board.getPieces(); + + for (Entry entry : pieces.entrySet()) { + final PositionDto position = PositionDto.from(entry.getKey()); + final PieceDto piece = PieceDto.from(entry.getValue()); + result.put(position, piece); + } + return new BoardDto(result); + } + + public Board toBoard() { + final Map result = new HashMap<>(); + for (Entry entry : pieces.entrySet()) { + result.put(entry.getKey().toPosition(), entry.getValue().toPiece()); + } + return new Board(result); + } +} diff --git a/src/main/java/db/dto/CampType.java b/src/main/java/db/dto/CampType.java new file mode 100644 index 00000000000..fd243d84196 --- /dev/null +++ b/src/main/java/db/dto/CampType.java @@ -0,0 +1,40 @@ +package db.dto; + +import java.util.Arrays; +import model.Camp; + +public enum CampType { + + WHITE(Camp.WHITE, "WHITE"), + BLACK(Camp.BLACK, "BLACK"); + + private final Camp camp; + private final String colorName; + + CampType(final Camp camp, final String colorName) { + this.camp = camp; + this.colorName = colorName; + } + + public static CampType findByColorName(final String color) { + return Arrays.stream(values()) + .filter(campType -> campType.colorName.equals(color)) + .findFirst() + .orElseThrow(); + } + + public static CampType findByCamp(final Camp camp) { + return Arrays.stream(values()) + .filter(campType -> campType.camp == camp) + .findFirst() + .orElseThrow(); + } + + public String getColorName() { + return colorName; + } + + public Camp getCamp() { + return camp; + } +} diff --git a/src/main/java/db/dto/MovingDto.java b/src/main/java/db/dto/MovingDto.java new file mode 100644 index 00000000000..b9f0f67e044 --- /dev/null +++ b/src/main/java/db/dto/MovingDto.java @@ -0,0 +1,13 @@ +package db.dto; + +import java.util.List; +import model.Camp; + +public record MovingDto(String camp, String current, String next) { + + public static MovingDto from(final List moving, final Camp camp) { + final String current = moving.get(0); + final String next = moving.get(1); + return new MovingDto(camp.toString(), current, next); + } +} diff --git a/src/main/java/db/dto/PieceDto.java b/src/main/java/db/dto/PieceDto.java new file mode 100644 index 00000000000..9b6b01c18be --- /dev/null +++ b/src/main/java/db/dto/PieceDto.java @@ -0,0 +1,18 @@ +package db.dto; + +import model.piece.Piece; + +public record PieceDto(String type, String camp) { + + public static PieceDto from(final Piece piece) { + final PieceType pieceType = PieceType.findValue(piece); + final CampType campType = CampType.findByCamp(piece.getCamp()); + return new PieceDto(pieceType.getPieceName(), campType.getColorName()); + } + + public Piece toPiece() { + final CampType army = CampType.findByColorName(camp); + final PieceType pieceType = PieceType.findValue(army.getCamp(), type); + return pieceType.getPiece(); + } +} diff --git a/src/main/java/db/dto/PieceType.java b/src/main/java/db/dto/PieceType.java new file mode 100644 index 00000000000..2817b4bf109 --- /dev/null +++ b/src/main/java/db/dto/PieceType.java @@ -0,0 +1,62 @@ +package db.dto; + +import java.util.Arrays; +import java.util.List; +import model.Camp; +import model.piece.Bishop; +import model.piece.BlackPawn; +import model.piece.King; +import model.piece.Knight; +import model.piece.Piece; +import model.piece.Queen; +import model.piece.Rook; +import model.piece.WhitePawn; + +public enum PieceType { + + WHITE_KING(new King(Camp.WHITE), "King"), + WHITE_QUEEN(new Queen(Camp.WHITE), "Queen"), + WHITE_ROOK(new Rook(Camp.WHITE), "Rook"), + WHITE_BISHOP(new Bishop(Camp.WHITE), "Bishop"), + WHITE_KNIGHT(new Knight(Camp.WHITE), "Knight"), + WHITE_PAWN(new WhitePawn(), "Pawn"), + BLACK_KING(new King(Camp.BLACK), "King"), + BLACK_QUEEN(new Queen(Camp.BLACK), "Queen"), + BLACK_ROOK(new Rook(Camp.BLACK), "Rook"), + BLACK_BISHOP(new Bishop(Camp.BLACK), "Bishop"), + BLACK_KNIGHT(new Knight(Camp.BLACK), "Knight"), + BLACK_PAWN(new BlackPawn(), "Pawn"); + + private final Piece piece; + private final String pieceName; + + PieceType(final Piece piece, final String pieceName) { + this.piece = piece; + this.pieceName = pieceName; + } + + public static PieceType findValue(final Piece piece) { + return Arrays.stream(values()) + .filter(pieceType1 -> pieceType1.piece.getClass().isInstance(piece)) + .findFirst() + .orElseThrow(); + } + + public static PieceType findValue(final Camp camp, final String type) { + final List pieceTypes = Arrays.stream(values()) + .filter(pieceType -> pieceType.pieceName.equals(type)) + .toList(); + return pieceTypes.stream() + .filter(pieceType -> pieceType.piece.isSameCamp(camp)) + .findFirst() + .orElseThrow(); + } + + public String getPieceName() { + return pieceName; + } + + public Piece getPiece() { + return piece; + } +} diff --git a/src/main/java/db/dto/PositionDto.java b/src/main/java/db/dto/PositionDto.java new file mode 100644 index 00000000000..5c451e94e67 --- /dev/null +++ b/src/main/java/db/dto/PositionDto.java @@ -0,0 +1,16 @@ +package db.dto; + +import model.position.File; +import model.position.Position; +import model.position.Rank; + +public record PositionDto(String value) { + + public static PositionDto from(final Position position) { + return new PositionDto(position.toString()); + } + + public Position toPosition() { + return new Position(File.from(value.charAt(0)), Rank.from(value.charAt(1))); + } +} diff --git a/src/main/java/db/dto/TurnDto.java b/src/main/java/db/dto/TurnDto.java new file mode 100644 index 00000000000..339aaa070c1 --- /dev/null +++ b/src/main/java/db/dto/TurnDto.java @@ -0,0 +1,12 @@ +package db.dto; + +import model.Camp; +import model.Turn; + +public record TurnDto(String currentCamp, int count) { + + public static TurnDto from(final Camp camp, final Turn turn) { + final CampType campType = CampType.findByCamp(camp); + return new TurnDto(campType.getColorName(), turn.count()); + } +} diff --git a/src/main/java/db/exception/ConnectionException.java b/src/main/java/db/exception/ConnectionException.java new file mode 100644 index 00000000000..65a6022feb3 --- /dev/null +++ b/src/main/java/db/exception/ConnectionException.java @@ -0,0 +1,10 @@ +package db.exception; + +import constant.ErrorCode; + +public class ConnectionException extends DBException { + + public ConnectionException(final ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/db/exception/DBException.java b/src/main/java/db/exception/DBException.java new file mode 100644 index 00000000000..34bc84e2ae9 --- /dev/null +++ b/src/main/java/db/exception/DBException.java @@ -0,0 +1,16 @@ +package db.exception; + +import constant.ErrorCode; + +public class DBException extends RuntimeException { + + private final ErrorCode errorCode; + + public DBException(final ErrorCode errorCode) { + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/db/exception/DaoException.java b/src/main/java/db/exception/DaoException.java new file mode 100644 index 00000000000..0834c5d54fd --- /dev/null +++ b/src/main/java/db/exception/DaoException.java @@ -0,0 +1,10 @@ +package db.exception; + +import constant.ErrorCode; + +public class DaoException extends DBException { + + public DaoException(final ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/dto/ChessBoardDto.java b/src/main/java/dto/ChessBoardDto.java index fd74ac65bca..e8f7c1b22c8 100644 --- a/src/main/java/dto/ChessBoardDto.java +++ b/src/main/java/dto/ChessBoardDto.java @@ -1,9 +1,7 @@ package dto; import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import model.ChessBoard; +import model.ChessGame; import model.piece.Piece; import model.position.File; import model.position.Position; @@ -13,32 +11,43 @@ public class ChessBoardDto { private static final String FILE_GUIDE_LINE = "abcdefgh"; + private static final String RANK_GUIDE_LINE_FORM = " %s"; private static final String EMPTY_POINT = "."; - private static final String RANK_GUIDE_LINE = " %s"; private static final int BOARD_SIZE = 8; - private final String value; + private final String board; + private final String currentTurn; - private ChessBoardDto(final String value) { - this.value = value; + private ChessBoardDto(final String board, final String currentTurn) { + this.board = board; + this.currentTurn = currentTurn; } - public static ChessBoardDto from(final ChessBoard chessBoard) { - final Map pieceOfPosition = chessBoard.getBoard(); - final String result = IntStream.range(0, BOARD_SIZE) - .mapToObj(Rank::from) - .map(rank -> IntStream.range(0, BOARD_SIZE) - .mapToObj(File::from) - .map(file -> convertToString(pieceOfPosition, file, rank)) - .collect(Collectors.joining())) - .collect(Collectors.joining(System.lineSeparator())) - .concat(String.format("%n%n%s%n", FILE_GUIDE_LINE)); - return new ChessBoardDto(result); + public static ChessBoardDto from(final ChessGame chessGame) { + final Map pieceOfPosition = chessGame.getPieces(); + + final StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < BOARD_SIZE; i++) { + stringBuilder.append(generateBoardLine(pieceOfPosition, Rank.from(i))); + stringBuilder.append(System.lineSeparator()); + } + stringBuilder.append(String.format("%n%s%n", FILE_GUIDE_LINE)); + + return new ChessBoardDto(stringBuilder.toString(), chessGame.getCamp().toString()); + } + + private static String generateBoardLine(final Map pieceOfPosition, final Rank rank) { + final StringBuilder stringBuilder = new StringBuilder(); + for (int j = 0; j < BOARD_SIZE; j++) { + final File file = File.from(j); + final Position position = new Position(file, rank); + final Piece piece = pieceOfPosition.get(position); + stringBuilder.append(convertToString(piece, file, rank)); + } + return stringBuilder.toString(); } - private static String convertToString(final Map board, final File file, final Rank rank) { - final Position position = new Position(file, rank); - final Piece piece = board.get(position); + private static String convertToString(final Piece piece, final File file, final Rank rank) { if (piece != null) { return PieceType.from(piece).getValue() + paddedRankGuidLine(file, rank); } @@ -47,12 +56,16 @@ private static String convertToString(final Map board, final Fi private static String paddedRankGuidLine(final File file, final Rank rank) { if (file.isLast()) { - return String.format(RANK_GUIDE_LINE, rank.getValue()); + return String.format(RANK_GUIDE_LINE_FORM, rank.getValue()); } return ""; } - public String getValue() { - return value; + public String getBoard() { + return board; + } + + public String getCurrentTurn() { + return currentTurn; } } diff --git a/src/main/java/dto/ScoreDto.java b/src/main/java/dto/ScoreDto.java new file mode 100644 index 00000000000..24f9466308d --- /dev/null +++ b/src/main/java/dto/ScoreDto.java @@ -0,0 +1,30 @@ +package dto; + +import model.Camp; +import model.ChessGame; +import model.Score; + +public class ScoreDto { + + private final float blackScore; + private final float whiteScore; + + private ScoreDto(final float blackScore, final float whiteScore) { + this.blackScore = blackScore; + this.whiteScore = whiteScore; + } + + public static ScoreDto from(final ChessGame chessGame) { + final Score black = chessGame.calculateScore(Camp.BLACK); + final Score white = chessGame.calculateScore(Camp.WHITE); + return new ScoreDto(black.value(), white.value()); + } + + public float getBlackScore() { + return blackScore; + } + + public float getWhiteScore() { + return whiteScore; + } +} diff --git a/src/main/java/exception/KingDeadException.java b/src/main/java/exception/KingDeadException.java new file mode 100644 index 00000000000..c37549aaff0 --- /dev/null +++ b/src/main/java/exception/KingDeadException.java @@ -0,0 +1,10 @@ +package exception; + +import constant.ErrorCode; + +public class KingDeadException extends CustomException { + + public KingDeadException(final ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/model/Board.java b/src/main/java/model/Board.java new file mode 100644 index 00000000000..8b6032ce042 --- /dev/null +++ b/src/main/java/model/Board.java @@ -0,0 +1,183 @@ +package model; + +import constant.ErrorCode; +import exception.InvalidTurnException; +import exception.KingDeadException; +import exception.PieceDoesNotExistException; +import exception.PieceExistInRouteException; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Function; +import model.piece.Bishop; +import model.piece.BlackPawn; +import model.piece.King; +import model.piece.Knight; +import model.piece.Pawn; +import model.piece.Piece; +import model.piece.Queen; +import model.piece.Rook; +import model.piece.WhitePawn; +import model.position.File; +import model.position.Moving; +import model.position.Position; +import model.position.Rank; + +public class Board { + + private static final Map> startingPosition = Map.of( + File.A, Rook::new, + File.B, Knight::new, + File.C, Bishop::new, + File.D, Queen::new, + File.E, King::new, + File.F, Bishop::new, + File.G, Knight::new, + File.H, Rook::new + ); + private static final Set KINGS = Set.of(new King(Camp.WHITE), new King(Camp.BLACK)); + private static final Set PAWNS = Set.of(new WhitePawn(), new BlackPawn()); + private static final Score SAME_FILE_PAWN_SCORE = new Score(0.5F); + + private final Map pieces; + + public Board(final Map pieces) { + this.pieces = pieces; + } + + public static Board create() { + final Map result = new HashMap<>(); + settingExceptPawn(result, Camp.BLACK, Rank.EIGHT); + settingPawn(result, Rank.SEVEN, new BlackPawn()); + settingPawn(result, Rank.TWO, new WhitePawn()); + settingExceptPawn(result, Camp.WHITE, Rank.ONE); + return new Board(result); + } + + private static void settingExceptPawn(final Map board, final Camp camp, final Rank rank) { + for (File file : File.values()) { + final Piece piece = startingPosition.get(file).apply(camp); + board.put(new Position(file, rank), piece); + } + } + + private static void settingPawn(final Map board, final Rank rank, final Pawn pawn) { + for (File file : File.values()) { + board.put(new Position(file, rank), pawn); + } + } + + public void validate(final Moving moving, final Camp currentCamp) { + final Position currentPosition = moving.getCurrentPosition(); + validateExistPiece(currentPosition); + final Piece piece = pieces.get(currentPosition); + validateOwnPiece(currentCamp, piece); + final Set route = getRoute(moving, piece); + validateUnBlocked(route); + final Position nextPosition = moving.getNextPosition(); + validateTargetEnemy(currentCamp, nextPosition); + validateIsKing(nextPosition); + } + + private void validateIsKing(final Position nextPosition) { + if (!pieces.containsKey(nextPosition)) { + return; + } + final Piece piece = pieces.get(nextPosition); + if (KINGS.contains(piece)) { + throw new KingDeadException(ErrorCode.KING_DEAD); + } + } + + private void validateExistPiece(final Position currentPosition) { + if (!pieces.containsKey(currentPosition)) { + throw new PieceDoesNotExistException(ErrorCode.PIECE_DOES_NOT_EXIST_POSITION); + } + } + + private void validateOwnPiece(final Camp currentCamp, final Piece piece) { + if (!piece.isSameCamp(currentCamp)) { + throw new InvalidTurnException(ErrorCode.INVALID_CAMP_PIECE); + } + } + + private void validateUnBlocked(final Set route) { + boolean blocked = route.stream() + .anyMatch(pieces::containsKey); + if (blocked) { + throw new PieceExistInRouteException(ErrorCode.PIECE_EXIST_IN_ROUTE); + } + } + + private void validateTargetEnemy(final Camp currentCamp, final Position nextPosition) { + if (pieces.containsKey(nextPosition) && pieces.get(nextPosition).isSameCamp(currentCamp)) { + throw new PieceExistInRouteException(ErrorCode.OWN_PIECE_EXIST_POSITION); + } + } + + private Set getRoute(final Moving moving, final Piece piece) { + if (pieces.containsKey(moving.getNextPosition())) { + return piece.getAttackRoute(moving); + } + return piece.getMoveRoute(moving); + } + + public void move(final Moving moving) { + final Piece piece = pieces.get(moving.getCurrentPosition()); + pieces.put(moving.getNextPosition(), piece); + pieces.remove(moving.getCurrentPosition()); + } + + public Score calculateScore(final Camp camp) { + final Map pawnCount = countSameFilePawn(camp); + final List scores = collectScore(camp); + final Score result = scores.stream() + .reduce(Score::plus) + .orElse(new Score(0)); + + return result.minus(duplicateFilePawns(pawnCount)); + } + + private List collectScore(final Camp camp) { + return pieces.values() + .stream() + .filter(piece -> piece.isSameCamp(camp)) + .map(PieceScore::getScore) + .toList(); + } + + private Map countSameFilePawn(final Camp camp) { + final Map pawnCount = new EnumMap<>(File.class); + pieces.entrySet() + .stream() + .filter(entry -> entry.getValue().isSameCamp(camp)) + .forEach(entry -> checkPawn(entry, pawnCount)); + return pawnCount; + } + + private Score duplicateFilePawns(final Map count) { + return count.values() + .stream() + .filter(sameFilePawnCount -> sameFilePawnCount > 1) + .map(sameFilePawnCount -> new Score(sameFilePawnCount * SAME_FILE_PAWN_SCORE.value())) + .reduce(Score::plus) + .orElse(new Score(0)); + } + + private void checkPawn(final Entry entry, final Map count) { + final Piece piece = entry.getValue(); + if (PAWNS.contains(piece)) { + final Position position = entry.getKey(); + final File file = position.getFile(); + count.put(file, count.getOrDefault(file, 0) + 1); + } + } + + public Map getPieces() { + return Collections.unmodifiableMap(pieces); + } +} diff --git a/src/main/java/model/ChessGame.java b/src/main/java/model/ChessGame.java new file mode 100644 index 00000000000..94954dc9482 --- /dev/null +++ b/src/main/java/model/ChessGame.java @@ -0,0 +1,47 @@ +package model; + +import java.util.Map; +import model.piece.Piece; +import model.position.Moving; +import model.position.Position; + +public class ChessGame { + + private final Board board; + private final GameTurn gameTurn; + + public ChessGame(final Board board, final GameTurn gameTurn) { + this.board = board; + this.gameTurn = gameTurn; + } + + public static ChessGame setupStartingPosition() { + return new ChessGame(Board.create(), GameTurn.create()); + } + + public void move(final Moving moving) { + board.validate(moving, getCamp()); + board.move(moving); + gameTurn.progress(); + } + + public Score calculateScore(final Camp camp) { + return board.calculateScore(camp); + } + + public Map getPieces() { + return board.getPieces(); + } + + public Camp getCamp() { + return gameTurn.getCamp(); + } + + public Turn getTurn() { + return gameTurn.getTurn(); + } + + public Board getBoard() { + return board; + } +} diff --git a/src/main/java/model/GameTurn.java b/src/main/java/model/GameTurn.java new file mode 100644 index 00000000000..32928eae461 --- /dev/null +++ b/src/main/java/model/GameTurn.java @@ -0,0 +1,36 @@ +package model; + +public class GameTurn { + + private static final Camp STARTING_CAMP = Camp.WHITE; + + private Camp camp; + private Turn turn; + + private GameTurn(final Camp camp) { + this.camp = camp; + this.turn = new Turn(0); + } + + public GameTurn(final Camp camp, final Turn turn) { + this.camp = camp; + this.turn = turn; + } + + public static GameTurn create() { + return new GameTurn(STARTING_CAMP); + } + + public void progress() { + camp = camp.toggle(); + turn = turn.take(); + } + + public Camp getCamp() { + return camp; + } + + public Turn getTurn() { + return turn; + } +} diff --git a/src/main/java/model/PieceScore.java b/src/main/java/model/PieceScore.java new file mode 100644 index 00000000000..5fa3a51e0b0 --- /dev/null +++ b/src/main/java/model/PieceScore.java @@ -0,0 +1,36 @@ +package model; + +import java.util.Arrays; +import model.piece.Bishop; +import model.piece.King; +import model.piece.Knight; +import model.piece.Pawn; +import model.piece.Piece; +import model.piece.Queen; +import model.piece.Rook; + +public enum PieceScore { + + KING(King.class, new Score(0)), + QUEEN(Queen.class, new Score(9)), + ROOK(Rook.class, new Score(5)), + BISHOP(Bishop.class, new Score(3)), + KNIGHT(Knight.class, new Score(2.5F)), + PAWN(Pawn.class, new Score(1)); + + private final Class clazz; + private final Score score; + + PieceScore(final Class clazz, final Score score) { + this.clazz = clazz; + this.score = score; + } + + public static Score getScore(final Piece piece) { + return Arrays.stream(values()) + .filter(pieceScore -> (pieceScore.clazz.isInstance(piece))) + .map(pieceScore -> pieceScore.score) + .findFirst() + .orElseThrow(); + } +} diff --git a/src/main/java/model/Score.java b/src/main/java/model/Score.java new file mode 100644 index 00000000000..8ea41d6dbd1 --- /dev/null +++ b/src/main/java/model/Score.java @@ -0,0 +1,12 @@ +package model; + +public record Score(float value) { + + public Score plus(final Score target) { + return new Score(value + target.value); + } + + public Score minus(final Score target) { + return new Score(value - target.value); + } +} diff --git a/src/main/java/model/Turn.java b/src/main/java/model/Turn.java new file mode 100644 index 00000000000..179a2a85031 --- /dev/null +++ b/src/main/java/model/Turn.java @@ -0,0 +1,7 @@ +package model; + +public record Turn(int count) { + public Turn take() { + return new Turn(count + 1); + } +} diff --git a/src/main/java/model/command/Command.java b/src/main/java/model/command/Command.java new file mode 100644 index 00000000000..c12e5e29ece --- /dev/null +++ b/src/main/java/model/command/Command.java @@ -0,0 +1,35 @@ +package model.command; + +import constant.ErrorCode; +import exception.InvalidCommandException; +import java.util.Arrays; +import java.util.regex.Pattern; + +public enum Command { + + START(Pattern.compile("start"), 0), + MOVE(Pattern.compile("move"), 2), + POSITION(Pattern.compile("[a-hA-H][1-8]"), 0), + STATUS(Pattern.compile("status"), 0), + QUIT(Pattern.compile("quit"), 0), + END(Pattern.compile("end"), 0); + + private final Pattern pattern; + private final int bodySize; + + Command(final Pattern pattern, final int bodySize) { + this.pattern = pattern; + this.bodySize = bodySize; + } + + public static Command from(String value) { + return Arrays.stream(values()) + .filter(command -> command.pattern.matcher(value).matches()) + .findFirst() + .orElseThrow(() -> new InvalidCommandException(ErrorCode.INVALID_COMMAND)); + } + + public boolean isEqualToBodySize(int targetSize) { + return bodySize == targetSize; + } +} diff --git a/src/main/java/model/command/CommandLine.java b/src/main/java/model/command/CommandLine.java new file mode 100644 index 00000000000..b4e91556b53 --- /dev/null +++ b/src/main/java/model/command/CommandLine.java @@ -0,0 +1,69 @@ +package model.command; + +import constant.ErrorCode; +import exception.InvalidCommandException; +import java.util.Collections; +import java.util.List; + +public class CommandLine { + + public static final int HEAD_INDEX = 0; + public static final int CURRENT_POSITION_INDEX = 0; + public static final int NEXT_POSITION_INDEX = 1; + + private final Command head; + private final List body; + + private CommandLine(final Command head, final List body) { + this.head = head; + this.body = body; + } + + public static CommandLine from(final List input) { + validateEmpty(input); + validateCommand(input); + Command command = Command.from(input.get(HEAD_INDEX)); + validateSize(command, input); + return new CommandLine(command, input.subList(1, input.size())); + } + + private static void validateEmpty(final List input) { + if (input == null || input.isEmpty()) { + throw new InvalidCommandException(ErrorCode.INVALID_COMMAND); + } + } + + private static void validateCommand(final List input) { + input.forEach(Command::from); + } + + private static void validateSize(final Command command, final List input) { + if (!command.isEqualToBodySize(input.size() - 1)) { + throw new InvalidCommandException(ErrorCode.INVALID_COMMAND); + } + } + + public boolean isStart() { + return head == Command.START; + } + + public boolean isEnd() { + return head == Command.END; + } + + public boolean isMove() { + return head == Command.MOVE; + } + + public boolean isStatus() { + return head == Command.STATUS; + } + + public boolean isQuit() { + return head == Command.QUIT; + } + + public List getBody() { + return Collections.unmodifiableList(body); + } +} diff --git a/src/main/java/model/piece/Bishop.java b/src/main/java/model/piece/Bishop.java index 5f27aa3d84c..8532a32ed5f 100644 --- a/src/main/java/model/piece/Bishop.java +++ b/src/main/java/model/piece/Bishop.java @@ -6,7 +6,6 @@ import model.Camp; import model.position.Moving; import model.position.Position; -import view.message.PieceType; public class Bishop extends Piece { @@ -19,7 +18,7 @@ public Set getMoveRoute(final Moving moving) { if (canMovable(moving)) { return moving.route(); } - throw new InvalidMovingException(ErrorCode.INVALID_MOVEMENT_RULE); + throw new InvalidMovingException(ErrorCode.INVALID_BISHOP_MOVEMENT); } @Override @@ -29,9 +28,4 @@ protected boolean canMovable(final Moving moving) { } return moving.isDiagonal(); } - - @Override - public String toString() { - return PieceType.from(this).getValue(); - } } diff --git a/src/main/java/model/piece/BlackPawn.java b/src/main/java/model/piece/BlackPawn.java new file mode 100644 index 00000000000..b9d7014f8f1 --- /dev/null +++ b/src/main/java/model/piece/BlackPawn.java @@ -0,0 +1,42 @@ +package model.piece; + +import java.util.Set; +import model.Camp; +import model.position.Position; +import model.position.Rank; + +public class BlackPawn extends Pawn { + + public BlackPawn() { + super(Camp.BLACK); + } + + @Override + protected Set twoMovedRoute(final Position currentPosition) { + return Set.of(new Position(currentPosition.getFile(), Rank.SIX)); + } + + @Override + protected boolean isStraight(final Position currentPosition, final int differenceRank, final int differenceFile) { + if (differenceFile != 0) { + return false; + } + if (Rank.SEVEN.getIndex() == currentPosition.getRankIndex() && differenceRank == -2) { + return true; + } + return differenceRank == -1; + } + + @Override + protected boolean isDiagonal(final int differenceRank, final int differenceFile) { + if (Math.abs(differenceFile) != 1) { + return false; + } + return differenceRank == -1; + } + + @Override + public Camp getCamp() { + return Camp.BLACK; + } +} diff --git a/src/main/java/model/piece/King.java b/src/main/java/model/piece/King.java index 417937bb8b5..1d39e16b546 100644 --- a/src/main/java/model/piece/King.java +++ b/src/main/java/model/piece/King.java @@ -6,7 +6,6 @@ import model.Camp; import model.position.Moving; import model.position.Position; -import view.message.PieceType; public class King extends Piece { @@ -19,7 +18,7 @@ public Set getMoveRoute(final Moving moving) { if (canMovable(moving)) { return Set.of(); } - throw new InvalidMovingException(ErrorCode.INVALID_MOVEMENT_RULE); + throw new InvalidMovingException(ErrorCode.INVALID_KING_MOVEMENT); } @Override @@ -29,9 +28,4 @@ protected boolean canMovable(final Moving moving) { } return moving.isAdjacent(); } - - @Override - public String toString() { - return PieceType.from(this).getValue(); - } } diff --git a/src/main/java/model/piece/Knight.java b/src/main/java/model/piece/Knight.java index 5f21116885c..c4ef48c33ec 100644 --- a/src/main/java/model/piece/Knight.java +++ b/src/main/java/model/piece/Knight.java @@ -6,7 +6,6 @@ import model.Camp; import model.position.Moving; import model.position.Position; -import view.message.PieceType; public class Knight extends Piece { @@ -19,7 +18,7 @@ public Set getMoveRoute(final Moving moving) { if (canMovable(moving)) { return Set.of(); } - throw new InvalidMovingException(ErrorCode.INVALID_MOVEMENT_RULE); + throw new InvalidMovingException(ErrorCode.INVALID_KNIGHT_MOVEMENT); } @Override @@ -29,9 +28,4 @@ protected boolean canMovable(final Moving moving) { } return moving.isShapeCapitalL(); } - - @Override - public String toString() { - return PieceType.from(this).getValue(); - } } diff --git a/src/main/java/model/piece/Pawn.java b/src/main/java/model/piece/Pawn.java index 3644f4fbf22..d1a5a5ce2a4 100644 --- a/src/main/java/model/piece/Pawn.java +++ b/src/main/java/model/piece/Pawn.java @@ -6,40 +6,24 @@ import model.Camp; import model.position.Moving; import model.position.Position; -import model.position.Rank; -import view.message.PieceType; -public class Pawn extends Piece { +public abstract class Pawn extends Piece { - public Pawn(final Camp camp) { + protected Pawn(final Camp camp) { super(camp); } - @Override - public Set getMoveRoute(final Moving moving) { - if (!canMovable(moving)) { - throw new InvalidMovingException(ErrorCode.INVALID_MOVEMENT_RULE); - } - final Position currentPosition = moving.getCurrentPosition(); - final Position nextPosition = moving.getNextPosition(); + protected abstract boolean isDiagonal(final int differenceRank, final int differenceFile); - if (Math.abs(nextPosition.getRankIndex() - currentPosition.getRankIndex()) == 1) { - return Set.of(); - } - return getTwoStraightRoute(currentPosition); - } + protected abstract boolean isStraight(final Position currentPosition, + final int differenceRank, + final int differenceFile); - private Set getTwoStraightRoute(final Position currentPosition) { - if (Camp.BLACK == camp) { - return Set.of(new Position(currentPosition.getFile(), Rank.SIX)); - } - return Set.of(new Position(currentPosition.getFile(), Rank.THREE)); - } + protected abstract Set twoMovedRoute(final Position currentPosition); - @Override - protected boolean canMovable(final Moving moving) { + protected boolean canAttack(final Moving moving) { if (moving.isNotMoved()) { - return false; + return true; } final Position currentPosition = moving.getCurrentPosition(); final Position nextPosition = moving.getNextPosition(); @@ -47,38 +31,11 @@ protected boolean canMovable(final Moving moving) { final int differenceRank = currentPosition.getRankIndex() - nextPosition.getRankIndex(); final int differenceFile = currentPosition.getFileIndex() - nextPosition.getFileIndex(); - return isStraight(currentPosition, differenceRank, differenceFile); - } - - private boolean isStraight(final Position currentPosition, final int differenceRank, final int differenceFile) { - if (differenceFile != 0) { - return false; - } - if (Camp.BLACK == camp) { - return isBlackTwoStraight(currentPosition, differenceRank); - } - if (Rank.TWO.getIndex() == currentPosition.getRankIndex() && differenceRank == 2) { - return true; - } - return differenceRank == 1; - } - - private boolean isBlackTwoStraight(final Position currentPosition, final int differenceRank) { - if (Rank.SEVEN.getIndex() == currentPosition.getRankIndex() && differenceRank == -2) { - return true; - } - return differenceRank == -1; + return !isDiagonal(differenceRank, differenceFile); } @Override - public Set getAttackRoute(final Moving moving) { - if (!canAttack(moving)) { - throw new InvalidMovingException(ErrorCode.INVALID_MOVEMENT_RULE); - } - return Set.of(); - } - - private boolean canAttack(final Moving moving) { + protected boolean canMovable(final Moving moving) { if (moving.isNotMoved()) { return false; } @@ -88,21 +45,28 @@ private boolean canAttack(final Moving moving) { final int differenceRank = currentPosition.getRankIndex() - nextPosition.getRankIndex(); final int differenceFile = currentPosition.getFileIndex() - nextPosition.getFileIndex(); - return isDiagonal(differenceRank, differenceFile); + return isStraight(currentPosition, differenceRank, differenceFile); } - private boolean isDiagonal(final int differenceRank, final int differenceFile) { - if (Math.abs(differenceFile) != 1) { - return false; + @Override + public Set getMoveRoute(final Moving moving) { + if (!canMovable(moving)) { + throw new InvalidMovingException(ErrorCode.INVALID_PAWN_MOVEMENT); } - if (Camp.BLACK == camp) { - return differenceRank == -1; + final Position currentPosition = moving.getCurrentPosition(); + final Position nextPosition = moving.getNextPosition(); + + if (Math.abs(nextPosition.getRankIndex() - currentPosition.getRankIndex()) == 1) { + return Set.of(); } - return differenceRank == 1; + return twoMovedRoute(currentPosition); } @Override - public String toString() { - return PieceType.from(this).getValue(); + public Set getAttackRoute(final Moving moving) { + if (canAttack(moving)) { + throw new InvalidMovingException(ErrorCode.INVALID_PAWN_MOVEMENT); + } + return Set.of(); } } diff --git a/src/main/java/model/piece/Piece.java b/src/main/java/model/piece/Piece.java index 65faa851034..d12622068ff 100644 --- a/src/main/java/model/piece/Piece.java +++ b/src/main/java/model/piece/Piece.java @@ -8,7 +8,7 @@ public abstract class Piece { - protected final Camp camp; + private final Camp camp; protected Piece(final Camp camp) { this.camp = camp; diff --git a/src/main/java/model/piece/Queen.java b/src/main/java/model/piece/Queen.java index 5a329fd31e0..62c95b20b26 100644 --- a/src/main/java/model/piece/Queen.java +++ b/src/main/java/model/piece/Queen.java @@ -6,7 +6,6 @@ import model.Camp; import model.position.Moving; import model.position.Position; -import view.message.PieceType; public class Queen extends Piece { @@ -19,7 +18,7 @@ public Set getMoveRoute(final Moving moving) { if (canMovable(moving)) { return moving.route(); } - throw new InvalidMovingException(ErrorCode.INVALID_MOVEMENT_RULE); + throw new InvalidMovingException(ErrorCode.INVALID_QUEEN_MOVEMENT); } @Override @@ -29,9 +28,4 @@ protected boolean canMovable(final Moving moving) { } return moving.isDiagonal() || moving.isVertical() || moving.isHorizontal(); } - - @Override - public String toString() { - return PieceType.from(this).getValue(); - } } diff --git a/src/main/java/model/piece/Rook.java b/src/main/java/model/piece/Rook.java index 2f47c751fd8..67fc56dbe08 100644 --- a/src/main/java/model/piece/Rook.java +++ b/src/main/java/model/piece/Rook.java @@ -6,7 +6,6 @@ import model.Camp; import model.position.Moving; import model.position.Position; -import view.message.PieceType; public class Rook extends Piece { @@ -19,7 +18,7 @@ public Set getMoveRoute(final Moving moving) { if (canMovable(moving)) { return moving.route(); } - throw new InvalidMovingException(ErrorCode.INVALID_MOVEMENT_RULE); + throw new InvalidMovingException(ErrorCode.INVALID_ROOK_MOVEMENT); } @Override @@ -29,9 +28,4 @@ protected boolean canMovable(final Moving moving) { } return moving.isHorizontal() || moving.isVertical(); } - - @Override - public String toString() { - return PieceType.from(this).getValue(); - } } diff --git a/src/main/java/model/piece/WhitePawn.java b/src/main/java/model/piece/WhitePawn.java new file mode 100644 index 00000000000..941206ea317 --- /dev/null +++ b/src/main/java/model/piece/WhitePawn.java @@ -0,0 +1,42 @@ +package model.piece; + +import java.util.Set; +import model.Camp; +import model.position.Position; +import model.position.Rank; + +public class WhitePawn extends Pawn { + + public WhitePawn() { + super(Camp.WHITE); + } + + @Override + protected Set twoMovedRoute(final Position currentPosition) { + return Set.of(new Position(currentPosition.getFile(), Rank.THREE)); + } + + @Override + protected boolean isStraight(final Position currentPosition, final int differenceRank, final int differenceFile) { + if (differenceFile != 0) { + return false; + } + if (Rank.TWO.getIndex() == currentPosition.getRankIndex() && differenceRank == 2) { + return true; + } + return differenceRank == 1; + } + + @Override + protected boolean isDiagonal(final int differenceRank, final int differenceFile) { + if (Math.abs(differenceFile) != 1) { + return false; + } + return differenceRank == 1; + } + + @Override + public Camp getCamp() { + return Camp.WHITE; + } +} diff --git a/src/main/java/model/position/Position.java b/src/main/java/model/position/Position.java index 5b753986aaf..2d2ba7026f1 100644 --- a/src/main/java/model/position/Position.java +++ b/src/main/java/model/position/Position.java @@ -60,4 +60,5 @@ public boolean equals(final Object target) { public String toString() { return file.getValue() + rank.getValue(); } + } diff --git a/src/main/java/model/status/Checkmate.java b/src/main/java/model/status/Checkmate.java new file mode 100644 index 00000000000..e9e2e69a9f0 --- /dev/null +++ b/src/main/java/model/status/Checkmate.java @@ -0,0 +1,24 @@ +package model.status; + +import constant.ErrorCode; +import exception.InvalidStatusException; +import model.ChessGame; +import model.command.CommandLine; + +public class Checkmate implements GameStatus { + + @Override + public GameStatus play(final CommandLine commandLine, final ChessGame chessGame) { + throw new InvalidStatusException(ErrorCode.INVALID_STATUS); + } + + @Override + public boolean isRunning() { + return false; + } + + @Override + public boolean isCheck() { + return true; + } +} diff --git a/src/main/java/model/status/End.java b/src/main/java/model/status/End.java index 3c8f7287deb..db30851e915 100644 --- a/src/main/java/model/status/End.java +++ b/src/main/java/model/status/End.java @@ -2,13 +2,13 @@ import constant.ErrorCode; import exception.InvalidStatusException; -import java.util.List; -import model.ChessBoard; +import model.ChessGame; +import model.command.CommandLine; public class End implements GameStatus { @Override - public GameStatus play(final List command, final ChessBoard chessBoard) { + public GameStatus play(final CommandLine commandLine, final ChessGame chessGame) { throw new InvalidStatusException(ErrorCode.INVALID_STATUS); } @@ -16,4 +16,9 @@ public GameStatus play(final List command, final ChessBoard chessBoard) public boolean isRunning() { return false; } + + @Override + public boolean isCheck() { + return false; + } } diff --git a/src/main/java/model/status/GameStatus.java b/src/main/java/model/status/GameStatus.java index 3d29b87cd1f..5697a39517f 100644 --- a/src/main/java/model/status/GameStatus.java +++ b/src/main/java/model/status/GameStatus.java @@ -1,11 +1,17 @@ package model.status; -import java.util.List; -import model.ChessBoard; +import model.ChessGame; +import model.command.CommandLine; public interface GameStatus { - GameStatus play(List command, ChessBoard chessBoard); + GameStatus play(final CommandLine commandLine, final ChessGame chessGame); boolean isRunning(); + + boolean isCheck(); + + default boolean isQuit() { + return false; + } } diff --git a/src/main/java/model/status/Quit.java b/src/main/java/model/status/Quit.java new file mode 100644 index 00000000000..bfc3601d26b --- /dev/null +++ b/src/main/java/model/status/Quit.java @@ -0,0 +1,29 @@ +package model.status; + +import constant.ErrorCode; +import exception.InvalidStatusException; +import model.ChessGame; +import model.command.CommandLine; + +public class Quit implements GameStatus { + + @Override + public GameStatus play(final CommandLine commandLine, final ChessGame chessGame) { + throw new InvalidStatusException(ErrorCode.INVALID_STATUS); + } + + @Override + public boolean isRunning() { + return false; + } + + @Override + public boolean isCheck() { + return false; + } + + @Override + public boolean isQuit() { + return true; + } +} diff --git a/src/main/java/model/status/Running.java b/src/main/java/model/status/Running.java index f0c2a88fa59..4a29fd9a3f3 100644 --- a/src/main/java/model/status/Running.java +++ b/src/main/java/model/status/Running.java @@ -2,35 +2,56 @@ import constant.ErrorCode; import exception.InvalidStatusException; +import exception.KingDeadException; import java.util.List; -import model.ChessBoard; -import model.Command; +import model.ChessGame; +import model.command.CommandLine; import model.position.Moving; import model.position.Position; public class Running implements GameStatus { @Override - public GameStatus play(final List command, final ChessBoard chessBoard) { - final Command cmd = Command.from(command.get(Command.HEAD_INDEX)); - if (cmd == Command.END && cmd.isAvailableSize(command.size())) { + public GameStatus play(final CommandLine commandLine, final ChessGame chessGame) { + if (commandLine.isEnd()) { return new End(); } - if (cmd == Command.MOVE && cmd.isAvailableSize(command.size())) { - final Moving moving = convert(command); - chessBoard.move(moving); + if (commandLine.isMove()) { + return status(commandLine, chessGame); + } + if (commandLine.isStatus()) { return new Running(); } + if (commandLine.isQuit()) { + return new Quit(); + } throw new InvalidStatusException(ErrorCode.INVALID_STATUS); } + private GameStatus status(final CommandLine commandLine, final ChessGame chessGame) { + final Moving moving = convert(commandLine.getBody()); + try { + chessGame.move(moving); + return new Running(); + } catch (KingDeadException exception) { + return new Checkmate(); + } + } + private Moving convert(final List command) { - return new Moving(Position.from(command.get(Command.CURRENT_INDEX)), - Position.from(command.get(Command.NEXT_INDEX))); + final String currentPosition = command.get(CommandLine.CURRENT_POSITION_INDEX); + final String nextPosition = command.get(CommandLine.NEXT_POSITION_INDEX); + + return new Moving(Position.from(currentPosition), Position.from(nextPosition)); } @Override public boolean isRunning() { return true; } + + @Override + public boolean isCheck() { + return false; + } } diff --git a/src/main/java/model/status/StatusFactory.java b/src/main/java/model/status/StatusFactory.java new file mode 100644 index 00000000000..a11f624b57e --- /dev/null +++ b/src/main/java/model/status/StatusFactory.java @@ -0,0 +1,24 @@ +package model.status; + +import constant.ErrorCode; +import exception.InvalidStatusException; +import model.command.CommandLine; + +public class StatusFactory { + + private StatusFactory() { + } + + public static GameStatus create(final CommandLine commandLine) { + if (commandLine.isStart()) { + return new Running(); + } + if (commandLine.isEnd()) { + return new End(); + } + if (commandLine.isQuit()) { + return new Quit(); + } + throw new InvalidStatusException(ErrorCode.INVALID_STATUS); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java index f5884cad739..d09ad38c2ff 100644 --- a/src/main/java/view/OutputView.java +++ b/src/main/java/view/OutputView.java @@ -2,7 +2,7 @@ import constant.ErrorCode; import dto.ChessBoardDto; -import model.Camp; +import dto.ScoreDto; import view.message.ErrorCodeMessage; public class OutputView { @@ -10,19 +10,27 @@ public class OutputView { public void printStartMessage() { System.out.println("> 체스 게임을 시작합니다."); System.out.println("> 게임 시작 : start"); - System.out.println("> 게임 종료 : end"); + System.out.println("> 게임 저장 후 종료 : end"); + System.out.println("> 게임 저장 하지 않고 종료 : quit"); System.out.println("> 게임 이동 : move source위치 target위치 - 예. move b2 b3"); + System.out.println("> 게임 현황 : status"); } - public void printChessBoard(ChessBoardDto chessBoardDto) { - System.out.printf(chessBoardDto.getValue()); + public void printChessBoard(final ChessBoardDto chessBoardDto) { + System.out.printf(chessBoardDto.getBoard()); + System.out.printf("현재 턴: %s%n%n", chessBoardDto.getCurrentTurn()); + } + + public void printScore(final ScoreDto scoreDto) { + System.out.printf("BLACK 점수: %s%n", scoreDto.getBlackScore()); + System.out.printf("WHITE 점수: %s%n", scoreDto.getWhiteScore()); } public void printException(final ErrorCode errorCode) { System.out.printf("[ERROR] %s%n", ErrorCodeMessage.from(errorCode).getMessage()); } - public void printCamp(final Camp camp) { - System.out.printf("현재 턴: %s%n%n", camp.toString()); + public void printWinner(final String camp) { + System.out.printf("%s 승리%n", camp); } } diff --git a/src/main/java/view/message/ErrorCodeMessage.java b/src/main/java/view/message/ErrorCodeMessage.java index 3844ba29233..44d5d3934ff 100644 --- a/src/main/java/view/message/ErrorCodeMessage.java +++ b/src/main/java/view/message/ErrorCodeMessage.java @@ -13,10 +13,21 @@ public enum ErrorCodeMessage { INVALID_STATUS(ErrorCode.INVALID_STATUS, "유효하지 않은 상태입니다."), INVALID_COMMAND(ErrorCode.INVALID_COMMAND, "유효하지 않은 명령입니다."), INVALID_POSITION(ErrorCode.INVALID_POSITION, "유효하지 않은 위치입니다."), - INVALID_MOVEMENT_RULE(ErrorCode.INVALID_MOVEMENT_RULE, "이동할 수 없는 위치입니다."), + INVALID_PAWN_MOVEMENT(ErrorCode.INVALID_PAWN_MOVEMENT, "유효하지 않은 폰의 이동 경로입니다."), + INVALID_KING_MOVEMENT(ErrorCode.INVALID_KING_MOVEMENT, "유효하지 않은 킹의 이동 경로입니다."), + INVALID_BISHOP_MOVEMENT(ErrorCode.INVALID_BISHOP_MOVEMENT, "유효하지 않은 비숍의 이동 경로입니다."), + INVALID_KNIGHT_MOVEMENT(ErrorCode.INVALID_KNIGHT_MOVEMENT, "유효하지 않은 나이트의 이동 경로입니다."), + INVALID_QUEEN_MOVEMENT(ErrorCode.INVALID_QUEEN_MOVEMENT, "유효하지 않은 퀸의 이동 경로입니다."), + INVALID_ROOK_MOVEMENT(ErrorCode.INVALID_ROOK_MOVEMENT, "유효하지 않은 룩의 이동 경로입니다."), PIECE_EXIST_IN_ROUTE(ErrorCode.PIECE_EXIST_IN_ROUTE, "이동 경로에 기물이 있습니다."), + OWN_PIECE_EXIST_POSITION(ErrorCode.OWN_PIECE_EXIST_POSITION, "도착 위치에 자신의 기물이 있습니다."), PIECE_DOES_NOT_EXIST_POSITION(ErrorCode.PIECE_DOES_NOT_EXIST_POSITION, "해당 위치에 기물이 없습니다."), INVALID_CAMP_PIECE(ErrorCode.INVALID_CAMP_PIECE, "자신의 기물만 움직일 수 있습니다."), + CONNECTION(ErrorCode.CONNECTION, "DB 연결 오류 입니다."), + FAIL_SAVE(ErrorCode.FAIL_SAVE, "저장에 실패하였습니다."), + FAIL_FIND(ErrorCode.FAIL_FIND, "게임을 불러오는데 실패하였습니다."), + FAIL_DELETE(ErrorCode.FAIL_DELETE, "게임을 삭제하는데 실패하였습니다."), + NO_MESSAGE(ErrorCode.NO_MESSAGE, "해당 메시지가 없습니다."); private static final Map SUIT_MESSAGE = Arrays.stream(values()) diff --git a/src/main/java/view/message/PieceType.java b/src/main/java/view/message/PieceType.java index 2d0b602c226..dd2ee1bc4dc 100644 --- a/src/main/java/view/message/PieceType.java +++ b/src/main/java/view/message/PieceType.java @@ -8,12 +8,13 @@ import java.util.stream.Collectors; import model.Camp; import model.piece.Bishop; +import model.piece.BlackPawn; import model.piece.King; import model.piece.Knight; -import model.piece.Pawn; import model.piece.Piece; import model.piece.Queen; import model.piece.Rook; +import model.piece.WhitePawn; public enum PieceType { @@ -23,8 +24,8 @@ public enum PieceType { KING_WHITE(new King(Camp.WHITE), "k"), KNIGHT_BLACK(new Knight(Camp.BLACK), "N"), KNIGHT_WHITE(new Knight(Camp.WHITE), "n"), - PAWN_BLACK(new Pawn(Camp.BLACK), "P"), - PAWN_WHITE(new Pawn(Camp.WHITE), "p"), + PAWN_BLACK(new BlackPawn(), "P"), + PAWN_WHITE(new WhitePawn(), "p"), QUEEN_BLACK(new Queen(Camp.BLACK), "Q"), QUEEN_WHITE(new Queen(Camp.WHITE), "q"), ROOK_BLACK(new Rook(Camp.BLACK), "R"), diff --git a/src/main/resources/board.sql b/src/main/resources/board.sql new file mode 100644 index 00000000000..7f6154ab713 --- /dev/null +++ b/src/main/resources/board.sql @@ -0,0 +1,9 @@ +use chess; + +create table board +( + position varchar(2) not null, + piece_type varchar(6) not null, + camp varchar(5) not null + +); diff --git a/src/main/resources/moving.sql b/src/main/resources/moving.sql new file mode 100644 index 00000000000..d092cdbd5f8 --- /dev/null +++ b/src/main/resources/moving.sql @@ -0,0 +1,9 @@ +use chess; + +create table moving +( + movement_id INT primary key auto_increment, + camp varchar(5) not null, + start varchar(2) not null, + destination varchar(2) not null +); diff --git a/src/main/resources/turn.sql b/src/main/resources/turn.sql new file mode 100644 index 00000000000..00e339248b7 --- /dev/null +++ b/src/main/resources/turn.sql @@ -0,0 +1,7 @@ +use chess; + +create table turn +( + camp varchar(5) not null, + count int not null +); diff --git a/src/test/java/db/BoardDaoTest.java b/src/test/java/db/BoardDaoTest.java new file mode 100644 index 00000000000..0793e5b0052 --- /dev/null +++ b/src/test/java/db/BoardDaoTest.java @@ -0,0 +1,47 @@ +package db; + +import static org.assertj.core.api.Assertions.assertThat; + +import db.dto.BoardDto; +import model.Board; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class BoardDaoTest { + + private final BoardDao boardDao = new BoardDao("chess_test"); + + @BeforeEach + void beforeEach() { + boardDao.remove(); + } + + @Test + @DisplayName("보드 저장 확인") + void addBoard() { + //given + final BoardDto board = BoardDto.from(Board.create()); + + //when + boardDao.saveBoard(board); + final BoardDto findBoard = boardDao.find(); + + //then + assertThat(board).isEqualTo(findBoard); + } + + @DisplayName("테이블이 비어있다면 새로운 보드를 만든다.") + @Test + void findBoard() { + //given + boardDao.remove(); + final BoardDto expected = BoardDto.from(Board.create()); + + //when + final BoardDto boardDto = boardDao.find(); + + //then + assertThat(boardDto).isEqualTo(expected); + } +} diff --git a/src/test/java/db/MovingDaoTest.java b/src/test/java/db/MovingDaoTest.java new file mode 100644 index 00000000000..1e4637fde17 --- /dev/null +++ b/src/test/java/db/MovingDaoTest.java @@ -0,0 +1,79 @@ +package db; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import db.connection.DBConnectionUtil; +import db.dto.MovingDto; +import db.exception.DaoException; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class MovingDaoTest { + + private final MovingDao movingDao = new MovingDao("chess_test"); + + @BeforeEach + void beforeEach() { + movingDao.remove(); + } + + @DisplayName("데이터베이스 접속 확인") + @Test + void connection() throws SQLException { + try (final Connection connection = DBConnectionUtil.getConnection("chess_test")) { + assertThat(connection).isNotNull(); + } + } + + @Test + @DisplayName("이동 저장 확인") + void addMoving() { + final MovingDto moving = new MovingDto("WHITE", "a2", "a3"); + final long id = movingDao.addMoving(moving); + + assertThat(movingDao.findByMovementId(id)).isEqualTo(moving); + } + + @Test + @DisplayName("찾고자 하는 기보가 없으면 예외가 발생한다.") + void failFindMoving() { + //given + movingDao.addMoving(new MovingDto("WHITE", "a2", "a3")); + movingDao.addMoving(new MovingDto("BLACK", "h7", "h6")); + + //when then + assertThatThrownBy(() -> movingDao.findByMovementId(3)) + .isInstanceOf(DaoException.class); + } + + @Test + @DisplayName("행의 개수를 파악한다.") + void count() { + assertThat(movingDao.countMoving()).isZero(); + } + + @Test + @DisplayName("저장된 기보를 확인한다.") + void findAll() { + //given + final List movingDtos = List.of( + new MovingDto("WHITE", "a2", "a3"), + new MovingDto("BLACK", "h7", "h6"), + new MovingDto("WHITE", "a3", "a4"), + new MovingDto("BLACK", "g7", "g6") + ); + + //when + for (MovingDto movingDto : movingDtos) { + movingDao.addMoving(movingDto); + } + + //then + assertThat(movingDao.findAll()).isEqualTo(movingDtos); + } +} diff --git a/src/test/java/db/RepositoryTest.java b/src/test/java/db/RepositoryTest.java new file mode 100644 index 00000000000..b313c5968f3 --- /dev/null +++ b/src/test/java/db/RepositoryTest.java @@ -0,0 +1,96 @@ +package db; + +import static model.Fixtures.A2; +import static model.Fixtures.A3; +import static model.Fixtures.G6; +import static model.Fixtures.G7; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import db.dto.BoardDto; +import db.dto.MovingDto; +import db.dto.TurnDto; +import model.ChessGame; +import model.position.Moving; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RepositoryTest { + + private final Repository repository = new Repository("chess_test"); + + @BeforeEach + void beforeEach() { + repository.removeAll(); + } + + @DisplayName("기보를 저장한다.") + @Test + void saveMoving() { + repository.saveMoving(new MovingDto("WHITE", "a2", "a3")); + assertThat(repository.hasGame()).isTrue(); + } + + @DisplayName("진행된 게입이 없으면 false를 반환한다.") + @Test + void hasNoGame() { + assertThat(repository.hasGame()).isFalse(); + } + + @DisplayName("저장된 게임이 없을 때 새로운 게임을 만든다.") + @Test + void createNewChessGame() { + final ChessGame game = repository.findGame(); + final ChessGame expected = ChessGame.setupStartingPosition(); + assertAll( + () -> assertThat(game.getPieces()).isEqualTo(expected.getPieces()), + () -> assertThat(game.getCamp()).isEqualTo(expected.getCamp()), + () -> assertThat(game.getTurn()).isEqualTo(expected.getTurn()) + ); + } + + @Test + @DisplayName("기보만 저장됐을 때 기보를 바탕으로 복구한다.") + void restore() { + //given + final ChessGame expected = ChessGame.setupStartingPosition(); + expected.move(new Moving(A2, A3)); + + //when + repository.saveMoving(new MovingDto("WHITE", "a2", "a3")); + final ChessGame game = repository.findGame(); + + //then + assertAll( + () -> assertThat(game.getPieces()).isEqualTo(expected.getPieces()), + () -> assertThat(game.getCamp()).isEqualTo(expected.getCamp()), + () -> assertThat(game.getTurn()).isEqualTo(expected.getTurn()) + ); + } + + @Test + @DisplayName("보드와 턴을 저장한다.") + void saveBoardAndTurn() { + //given + final ChessGame expected = ChessGame.setupStartingPosition(); + expected.move(new Moving(A2, A3)); + repository.saveMoving(new MovingDto("WHITE", "a2", "a3")); + expected.move(new Moving(G7, G6)); + repository.saveMoving(new MovingDto("BLACK", "g7", "g6")); + + final BoardDto boardDto = BoardDto.from(expected.getBoard()); + final TurnDto turnDto = TurnDto.from(expected.getCamp(), expected.getTurn()); + + //when + repository.save(boardDto, turnDto); + + //then + final ChessGame game = repository.findGame(); + assertAll( + () -> assertThat(game.getPieces()).isEqualTo(expected.getPieces()), + () -> assertThat(game.getCamp()).isEqualTo(expected.getCamp()), + () -> assertThat(game.getTurn()).isEqualTo(expected.getTurn()) + ); + } +} diff --git a/src/test/java/db/TurnDaoTest.java b/src/test/java/db/TurnDaoTest.java new file mode 100644 index 00000000000..fb6c8752e25 --- /dev/null +++ b/src/test/java/db/TurnDaoTest.java @@ -0,0 +1,33 @@ +package db; + +import static org.assertj.core.api.Assertions.assertThat; + +import db.dto.TurnDto; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class TurnDaoTest { + + private final TurnDao turnDao = new TurnDao("chess_test"); + + @BeforeEach + void beforeEach() { + turnDao.remove(); + } + + @DisplayName("턴 저장 확인") + @Test + void saveTurn() { + //given + final TurnDto turnDto = new TurnDto("WHITE", 1); + turnDao.saveTurn(turnDto); + final TurnDto expected = new TurnDto("WHITE", 1); + + //when + final TurnDto turn = turnDao.findTurn(); + + //then + assertThat(turn).isEqualTo(expected); + } +} diff --git a/src/test/java/db/dto/CampTypeTest.java b/src/test/java/db/dto/CampTypeTest.java new file mode 100644 index 00000000000..89ded3b227d --- /dev/null +++ b/src/test/java/db/dto/CampTypeTest.java @@ -0,0 +1,19 @@ +package db.dto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CampTypeTest { + + @DisplayName("문자로 Camp를 찾는다.") + @Test + void findByColor() { + assertAll( + () -> assertThat(CampType.findByColorName("WHITE")).isEqualTo(CampType.WHITE), + () -> assertThat(CampType.findByColorName("BLACK")).isEqualTo(CampType.BLACK) + ); + } +} diff --git a/src/test/java/model/BoardTest.java b/src/test/java/model/BoardTest.java new file mode 100644 index 00000000000..f56a16ec92b --- /dev/null +++ b/src/test/java/model/BoardTest.java @@ -0,0 +1,112 @@ +package model; + +import static model.Fixtures.A2; +import static model.Fixtures.A4; +import static model.Fixtures.B4; +import static model.Fixtures.B5; +import static model.Fixtures.B7; +import static model.Fixtures.B8; +import static model.Fixtures.C2; +import static model.Fixtures.C3; +import static model.Fixtures.C6; +import static model.Fixtures.F4; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.Map; +import model.piece.Piece; +import model.piece.WhitePawn; +import model.position.Moving; +import model.position.Position; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class BoardTest { + + @DisplayName("초기 상태의 기물 점수를 계산한다.") + @Test + void calculateInitBoard() { + //given + final ChessGame chessGame = ChessGame.setupStartingPosition(); + final Score expected = new Score(38.0F); + + //when then + assertAll( + () -> assertThat(chessGame.calculateScore(Camp.WHITE)).isEqualTo(expected), + () -> assertThat(chessGame.calculateScore(Camp.BLACK)).isEqualTo(expected) + ); + } + + @Test + @DisplayName("같은 세로줄에 폰이 있으면 0.5점이다.") + void calculateBoardWhenPawnSameFile() { + //given + final ChessGame chessGame = ChessGame.setupStartingPosition(); + final Score blackExpected = new Score(37.0F); + final Score whiteExpected = new Score(37.0F); + + //when + chessGame.move(new Moving(A2, A4)); + chessGame.move(new Moving(B7, B5)); + chessGame.move(new Moving(A4, B5)); + + //when then + assertAll( + () -> assertThat(chessGame.calculateScore(Camp.WHITE)).isEqualTo(whiteExpected), + () -> assertThat(chessGame.calculateScore(Camp.BLACK)).isEqualTo(blackExpected) + ); + } + + @Test + @DisplayName("세로 줄에 3개의 폰이 있는 경우") + void threePawn() { + //given + final ChessGame chessGame = ChessGame.setupStartingPosition(); + final Score blackExpected = new Score(34.5F); + final Score whiteExpected = new Score(36.5F); + + //when + chessGame.move(new Moving(A2, A4)); + chessGame.move(new Moving(B7, B5)); + chessGame.move(new Moving(A4, B5)); + chessGame.move(new Moving(B8, C6)); + chessGame.move(new Moving(C2, C3)); + chessGame.move(new Moving(C6, B4)); + chessGame.move(new Moving(C3, B4)); + + /* + R.BQKBNR 8 + P.PPPPPP 7 + ........ 6 + .p...... 5 + .p...... 4 + ........ 3 + .p.ppppp 2 + rnbqkbnr 1 + + abcdefgh + */ + + //then + assertAll( + () -> assertThat(chessGame.calculateScore(Camp.WHITE)).isEqualTo(whiteExpected), + () -> assertThat(chessGame.calculateScore(Camp.BLACK)).isEqualTo(blackExpected) + ); + } + + @Test + @DisplayName("getter로 가져온 값을 수정하려고 하면 예외가 발생한다.") + void doNotUpdate() { + //given + final Board board = Board.create(); + final Piece piece = new WhitePawn(); + + //when + final Map pieces = board.getPieces(); + + //then + assertThatThrownBy(() -> pieces.put(F4, piece)) + .isInstanceOf(UnsupportedOperationException.class); + } +} diff --git a/src/test/java/model/ChessGameTest.java b/src/test/java/model/ChessGameTest.java new file mode 100644 index 00000000000..81e0d43b5e0 --- /dev/null +++ b/src/test/java/model/ChessGameTest.java @@ -0,0 +1,266 @@ +package model; + +import static model.Fixtures.A1; +import static model.Fixtures.A2; +import static model.Fixtures.A3; +import static model.Fixtures.A4; +import static model.Fixtures.A5; +import static model.Fixtures.A6; +import static model.Fixtures.A7; +import static model.Fixtures.A8; +import static model.Fixtures.B1; +import static model.Fixtures.B2; +import static model.Fixtures.B5; +import static model.Fixtures.B7; +import static model.Fixtures.B8; +import static model.Fixtures.C1; +import static model.Fixtures.C2; +import static model.Fixtures.C7; +import static model.Fixtures.C8; +import static model.Fixtures.D1; +import static model.Fixtures.D2; +import static model.Fixtures.D7; +import static model.Fixtures.D8; +import static model.Fixtures.E1; +import static model.Fixtures.E2; +import static model.Fixtures.E3; +import static model.Fixtures.E5; +import static model.Fixtures.E7; +import static model.Fixtures.E8; +import static model.Fixtures.F1; +import static model.Fixtures.F2; +import static model.Fixtures.F3; +import static model.Fixtures.F6; +import static model.Fixtures.F7; +import static model.Fixtures.F8; +import static model.Fixtures.G1; +import static model.Fixtures.G2; +import static model.Fixtures.G4; +import static model.Fixtures.G7; +import static model.Fixtures.G8; +import static model.Fixtures.H1; +import static model.Fixtures.H2; +import static model.Fixtures.H3; +import static model.Fixtures.H4; +import static model.Fixtures.H5; +import static model.Fixtures.H6; +import static model.Fixtures.H7; +import static model.Fixtures.H8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import exception.InvalidTurnException; +import exception.KingDeadException; +import exception.PieceDoesNotExistException; +import exception.PieceExistInRouteException; +import java.util.HashMap; +import java.util.Map; +import model.piece.Bishop; +import model.piece.BlackPawn; +import model.piece.King; +import model.piece.Knight; +import model.piece.Piece; +import model.piece.Queen; +import model.piece.Rook; +import model.piece.WhitePawn; +import model.position.Moving; +import model.position.Position; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ChessGameTest { + + @DisplayName("초기에는 32개의 기물이 생성된다.") + @Test + void checkPiecesCount() { + //given && when + final ChessGame chessGame = ChessGame.setupStartingPosition(); + + //then + final Map board = chessGame.getPieces(); + assertThat(board.keySet()).hasSize(32); + } + + @DisplayName("기물들의 시작 위치를 확인한다.") + @Test + void checkStartingPosition() { + //given && when + final ChessGame chessGame = ChessGame.setupStartingPosition(); + + final Map board = chessGame.getPieces(); + + final Map expected = new HashMap<>(); + + // black + expected.put(A8, new Rook(Camp.BLACK)); + expected.put(B8, new Knight(Camp.BLACK)); + expected.put(C8, new Bishop(Camp.BLACK)); + expected.put(D8, new Queen(Camp.BLACK)); + expected.put(E8, new King(Camp.BLACK)); + expected.put(F8, new Bishop(Camp.BLACK)); + expected.put(G8, new Knight(Camp.BLACK)); + expected.put(H8, new Rook(Camp.BLACK)); + expected.put(A7, new BlackPawn()); + expected.put(B7, new BlackPawn()); + expected.put(C7, new BlackPawn()); + expected.put(D7, new BlackPawn()); + expected.put(E7, new BlackPawn()); + expected.put(F7, new BlackPawn()); + expected.put(G7, new BlackPawn()); + expected.put(H7, new BlackPawn()); + + //white + expected.put(A1, new Rook(Camp.WHITE)); + expected.put(B1, new Knight(Camp.WHITE)); + expected.put(C1, new Bishop(Camp.WHITE)); + expected.put(D1, new Queen(Camp.WHITE)); + expected.put(E1, new King(Camp.WHITE)); + expected.put(F1, new Bishop(Camp.WHITE)); + expected.put(G1, new Knight(Camp.WHITE)); + expected.put(H1, new Rook(Camp.WHITE)); + expected.put(A2, new WhitePawn()); + expected.put(B2, new WhitePawn()); + expected.put(C2, new WhitePawn()); + expected.put(D2, new WhitePawn()); + expected.put(E2, new WhitePawn()); + expected.put(F2, new WhitePawn()); + expected.put(G2, new WhitePawn()); + expected.put(H2, new WhitePawn()); + + //then + assertThat(board).isEqualTo(expected); + } + + @DisplayName("해당 위치에 기물이 없는 경우 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"a3", "b3", "c3", "d4", "e4", "f5", "g6", "h6"}) + void failToMoveIfNoPiece(final String currentPosition) { + //given + final ChessGame chessGame = ChessGame.setupStartingPosition(); + + final Position position = Position.from(currentPosition); + + final Moving moving = new Moving(position, E5); + + //when & then + assertThatThrownBy(() -> chessGame.move(moving)) + .isInstanceOf(PieceDoesNotExistException.class); + } + + @Test + @DisplayName("자신의 기물이 아니면 예외가 발생한다.") + void failToMoveIfDifferentCamp() { + //given + final ChessGame chessGame = ChessGame.setupStartingPosition(); + + //when && then + final Moving moving = new Moving(A7, A6); + + assertThatThrownBy(() -> chessGame.move(moving)) + .isInstanceOf(InvalidTurnException.class); + } + + @DisplayName("이동 경로에 기물이 있으면 예외를 발생시킨다.") + @Test + void failToMoveIfContainPieceInRoute() { + //given + final ChessGame chessGame = ChessGame.setupStartingPosition(); + + //when && then + final Moving moving = new Moving(A1, A3); + + assertThatThrownBy(() -> chessGame.move(moving)) + .isInstanceOf(PieceExistInRouteException.class); + } + + @DisplayName("도착 지점에 같은 진영의 기물이 있으면 예외를 발생시킨다.") + @Test + void failToMoveIfContainsPieceInTargetPosition() { + //given + final ChessGame chessGame = ChessGame.setupStartingPosition(); + + //when + chessGame.move(new Moving(A2, A4)); // WHITE + chessGame.move(new Moving(A7, A5)); // BLACK + + //then + final Moving moving = new Moving(A1, A4); + + assertThatThrownBy(() -> chessGame.move(moving)) + .isInstanceOf(PieceExistInRouteException.class); + } + + @DisplayName("기물이 잡히면 체스보드에서 제거된다.") + @Test + void checkRemovePiece() { + //given + final ChessGame chessGame = ChessGame.setupStartingPosition(); + + //when + chessGame.move(new Moving(A2, A4)); + chessGame.move(new Moving(B7, B5)); + chessGame.move(new Moving(A4, B5)); + + //then + assertThat(chessGame.getPieces()).hasSize(31); + } + + @DisplayName("선공은 WHITE이다.") + @Test + void checkFirstAttack() { + final ChessGame chessGame = ChessGame.setupStartingPosition(); + + assertThat(chessGame.getCamp()).isEqualTo(Camp.WHITE); + } + + @DisplayName("후공은 BLACK이다.") + @Test + void checkSecondAttack() { + final ChessGame chessGame = ChessGame.setupStartingPosition(); + + chessGame.move(new Moving(A2, A3)); + + assertThat(chessGame.getCamp()).isEqualTo(Camp.BLACK); + } + + @DisplayName("킹을 잡으면 예외가 발생한다. 블랙 이기는 경우") + @Test + void failToCatchBlackKing() { + //given + final ChessGame chessGame = ChessGame.setupStartingPosition(); + + //when + chessGame.move(new Moving(F2, F3)); + chessGame.move(new Moving(E7, E5)); + chessGame.move(new Moving(G2, G4)); + chessGame.move(new Moving(D8, H4)); + chessGame.move(new Moving(H2, H3)); + + final Moving catchKingMove = new Moving(H4, E1); + + //then + assertThatThrownBy(() -> chessGame.move(catchKingMove)) + .isInstanceOf(KingDeadException.class); + } + + @DisplayName("킹을 잡으면 예외가 발생한다. 화이트 이기는 경우") + @Test + void failToCatchWhiteKing() { + //given + final ChessGame chessGame = ChessGame.setupStartingPosition(); + + //when + chessGame.move(new Moving(E2, E3)); + chessGame.move(new Moving(F7, F6)); + chessGame.move(new Moving(D1, H5)); + chessGame.move(new Moving(G8, H6)); + + final Moving catchKingMove = new Moving(H5, E8); + + //then + assertThatThrownBy(() -> chessGame.move(catchKingMove)) + .isInstanceOf(KingDeadException.class); + } +} diff --git a/src/test/java/model/PieceScoreTest.java b/src/test/java/model/PieceScoreTest.java new file mode 100644 index 00000000000..bf45fdd2607 --- /dev/null +++ b/src/test/java/model/PieceScoreTest.java @@ -0,0 +1,39 @@ +package model; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; +import model.piece.Bishop; +import model.piece.BlackPawn; +import model.piece.King; +import model.piece.Knight; +import model.piece.Piece; +import model.piece.Queen; +import model.piece.Rook; +import model.piece.WhitePawn; +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; + +class PieceScoreTest { + + @DisplayName("해당하는 기물의 점수를 반환한다.") + @ParameterizedTest + @MethodSource("getScoreParameterProvider") + void getScore(final Piece piece, final Score expected) { + assertThat(PieceScore.getScore(piece)).isEqualTo(expected); + } + + static Stream getScoreParameterProvider() { + return Stream.of( + Arguments.of(new King(Camp.BLACK), new Score(0)), + Arguments.of(new Queen(Camp.WHITE), new Score(9)), + Arguments.of(new Rook(Camp.BLACK), new Score(5)), + Arguments.of(new Bishop(Camp.BLACK), new Score(3)), + Arguments.of(new Knight(Camp.WHITE), new Score(2.5F)), + Arguments.of(new WhitePawn(), new Score(1)), + Arguments.of(new BlackPawn(), new Score(1)) + ); + } +} diff --git a/src/test/java/model/ScoreTest.java b/src/test/java/model/ScoreTest.java new file mode 100644 index 00000000000..e4ddf56bf66 --- /dev/null +++ b/src/test/java/model/ScoreTest.java @@ -0,0 +1,31 @@ +package model; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ScoreTest { + + @DisplayName("두개의 score를 더한다.") + @Test + void add() { + final Score one = new Score(1); + final Score two = new Score(2); + + final Score expected = new Score(3); + + assertThat(one.plus(two)).isEqualTo(expected); + } + + @DisplayName("두개의 score를 뺸다.") + @Test + void minus() { + final Score three = new Score(3); + final Score two = new Score(2); + + final Score expected = new Score(1); + + assertThat(three.minus(two)).isEqualTo(expected); + } +} diff --git a/src/test/java/model/command/CommandLineTest.java b/src/test/java/model/command/CommandLineTest.java new file mode 100644 index 00000000000..c5efd8c887d --- /dev/null +++ b/src/test/java/model/command/CommandLineTest.java @@ -0,0 +1,49 @@ +package model.command; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import exception.InvalidCommandException; +import java.util.List; +import java.util.stream.Stream; +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; + +class CommandLineTest { + + @DisplayName("유효하지 않은 커맨드가 입력되면 예외가 발생한다.") + @ParameterizedTest + @MethodSource("invalidInputParameterProvider") + void invalidInput(List input) { + assertThatThrownBy(() -> CommandLine.from(input)) + .isInstanceOf(InvalidCommandException.class); + } + + static Stream invalidInputParameterProvider() { + return Stream.of( + Arguments.of(List.of()), + Arguments.of(List.of("start", "a2", "a3")), + Arguments.of(List.of("start", "end")), + Arguments.of(List.of("move", "a2")), + Arguments.of(List.of("move", "a2", "a3", "a4")), + Arguments.of(List.of("end", "a2", "a3")), + Arguments.of(List.of("end", "end")) + ); + } + + @Test + @DisplayName("getter로 가져온 값을 수정하려고 하면 예외가 발생한다.") + void doNotUpdate() { + //given + final CommandLine commandLine = CommandLine.from(List.of("move", "a1", "a3")); + + //when + final List body = commandLine.getBody(); + + //then + assertThatThrownBy(() -> body.add("b4")) + .isInstanceOf(UnsupportedOperationException.class); + } +} diff --git a/src/test/java/model/command/CommandTest.java b/src/test/java/model/command/CommandTest.java new file mode 100644 index 00000000000..6403fa9478e --- /dev/null +++ b/src/test/java/model/command/CommandTest.java @@ -0,0 +1,43 @@ +package model.command; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import java.util.stream.Stream; +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; + +class CommandTest { + + @DisplayName("올바른 command가 입력되면 예외가 발생하지 않는다.") + @Test + void checkCommand() { + assertAll( + () -> assertThat(Command.from("start")).isEqualTo(Command.START), + () -> assertThat(Command.from("end")).isEqualTo(Command.END), + () -> assertThat(Command.from("move")).isEqualTo(Command.MOVE), + () -> assertThat(Command.from("a1")).isEqualTo(Command.POSITION), + () -> assertThat(Command.from("h8")).isEqualTo(Command.POSITION), + () -> assertThat(Command.from("D8")).isEqualTo(Command.POSITION) + ); + } + + @DisplayName("각 command 의 body 사이즈를 확인한다.") + @ParameterizedTest + @MethodSource("checkBodySizeParameterProvider") + void checkBodySize(Command command, List body) { + assertThat(command.isEqualToBodySize(body.size())).isTrue(); + } + + static Stream checkBodySizeParameterProvider() { + return Stream.of( + Arguments.of(Command.START, List.of()), + Arguments.of(Command.END, List.of()), + Arguments.of(Command.MOVE, List.of("a1", "a2")) + ); + } +} diff --git a/src/test/java/model/piece/PawnTest.java b/src/test/java/model/piece/PawnTest.java index 9530d160953..296e4aacd38 100644 --- a/src/test/java/model/piece/PawnTest.java +++ b/src/test/java/model/piece/PawnTest.java @@ -31,8 +31,7 @@ import exception.InvalidMovingException; import java.util.Set; import java.util.stream.Stream; -import model.Camp; -import model.ChessBoard; +import model.ChessGame; import model.position.Moving; import model.position.Position; import org.junit.jupiter.api.DisplayName; @@ -46,9 +45,7 @@ class PawnTest { @DisplayName("이동할 수 없는 경로면 예외가 발생한다.") @ParameterizedTest @MethodSource("invalidMovingParameterProvider") - void invalidMoving(final Camp camp, final Moving moving) { - final Pawn pawn = new Pawn(camp); - + void invalidMoving(final Pawn pawn, final Moving moving) { assertAll( () -> assertThat(pawn.canMovable(moving)).isFalse(), () -> assertThatThrownBy(() -> pawn.getMoveRoute(moving)) @@ -58,18 +55,16 @@ void invalidMoving(final Camp camp, final Moving moving) { static Stream invalidMovingParameterProvider() { return Stream.of( - Arguments.of(Camp.BLACK, new Moving(A6, A4)), - Arguments.of(Camp.BLACK, new Moving(A7, A4)), - Arguments.of(Camp.WHITE, new Moving(A8, A7)) + Arguments.of(new BlackPawn(), new Moving(A6, A4)), + Arguments.of(new BlackPawn(), new Moving(A7, A4)), + Arguments.of(new WhitePawn(), new Moving(A8, A7)) ); } @DisplayName("이동 경로를 반환한다. 출발지와 도착지는 포함하지 않는다.") @ParameterizedTest @MethodSource("checkRouteParameterProvider") - void checkRoute(final Camp camp, final Moving moving, final Set expected) { - final Pawn pawn = new Pawn(camp); - + void checkRoute(final Pawn pawn, final Moving moving, final Set expected) { assertAll( () -> assertThat(pawn.canMovable(moving)).isTrue(), () -> assertThat(pawn.getMoveRoute(moving)).isEqualTo(expected) @@ -79,10 +74,10 @@ void checkRoute(final Camp camp, final Moving moving, final Set expect static Stream checkRouteParameterProvider() { return Stream.of( - Arguments.of(Camp.BLACK, new Moving(A7, A5), Set.of(A6)), - Arguments.of(Camp.BLACK, new Moving(A6, A5), Set.of()), - Arguments.of(Camp.WHITE, new Moving(B2, B4), Set.of(B3)), - Arguments.of(Camp.WHITE, new Moving(C2, C3), Set.of()) + Arguments.of(new BlackPawn(), new Moving(A7, A5), Set.of(A6)), + Arguments.of(new BlackPawn(), new Moving(A6, A5), Set.of()), + Arguments.of(new WhitePawn(), new Moving(B2, B4), Set.of(B3)), + Arguments.of(new WhitePawn(), new Moving(C2, C3), Set.of()) ); } @@ -90,7 +85,7 @@ static Stream checkRouteParameterProvider() { @DisplayName("앞에 기물이 있을때 전진이 불가하다.") void whenPieceInFrontCanNotMoveForward() { //given - final ChessBoard chessBoard = ChessBoard.setupStartingPosition(); + final ChessGame chessGame = ChessGame.setupStartingPosition(); /* RNBQKBNR 8 @@ -106,13 +101,13 @@ void whenPieceInFrontCanNotMoveForward() { */ //when - chessBoard.move(new Moving(A2, A4)); - chessBoard.move(new Moving(A7, A5)); + chessGame.move(new Moving(A2, A4)); + chessGame.move(new Moving(A7, A5)); final Moving forwadMoving = new Moving(A4, A5); //then - assertThatThrownBy(() -> chessBoard.move(forwadMoving)) + assertThatThrownBy(() -> chessGame.move(forwadMoving)) .isInstanceOf(InvalidMovingException.class); } @@ -120,7 +115,7 @@ void whenPieceInFrontCanNotMoveForward() { @DisplayName("대각선에 기물이 있을 때 대각선 이동이 가능하다. WHITE (위 오른쪽)") void whenPieceInDiagonalCanMove1() { //given - final ChessBoard chessBoard = ChessBoard.setupStartingPosition(); + final ChessGame chessGame = ChessGame.setupStartingPosition(); /* RNBQKBNR 8 @@ -136,11 +131,11 @@ void whenPieceInDiagonalCanMove1() { */ //when - chessBoard.move(new Moving(A2, A4)); - chessBoard.move(new Moving(B7, B5)); + chessGame.move(new Moving(A2, A4)); + chessGame.move(new Moving(B7, B5)); //then - assertThatCode(() -> chessBoard.move(new Moving(A4, B5))) + assertThatCode(() -> chessGame.move(new Moving(A4, B5))) .doesNotThrowAnyException(); } @@ -148,7 +143,7 @@ void whenPieceInDiagonalCanMove1() { @DisplayName("대각선에 기물이 있다면 이동이 가능하다. BLACK (아래 오른쪽)") void whenPieceInDiagonalCanMove2() { //given - final ChessBoard chessBoard = ChessBoard.setupStartingPosition(); + final ChessGame chessGame = ChessGame.setupStartingPosition(); /* RNBQKBNR 8 @@ -164,12 +159,12 @@ void whenPieceInDiagonalCanMove2() { */ //when - chessBoard.move(new Moving(C2, C4)); - chessBoard.move(new Moving(B7, B5)); - chessBoard.move(new Moving(B2, B3)); + chessGame.move(new Moving(C2, C4)); + chessGame.move(new Moving(B7, B5)); + chessGame.move(new Moving(B2, B3)); //then - assertThatCode(() -> chessBoard.move(new Moving(B5, C4))) + assertThatCode(() -> chessGame.move(new Moving(B5, C4))) .doesNotThrowAnyException(); } @@ -177,7 +172,7 @@ void whenPieceInDiagonalCanMove2() { @DisplayName("대각선에 기물이 없다면 대각선 이동이 불가하다.") void whenPieceNotInDiagonalCanNotMove() { //given - final ChessBoard chessBoard = ChessBoard.setupStartingPosition(); + final ChessGame chessGame = ChessGame.setupStartingPosition(); /* RNBQKB.R 8 @@ -193,13 +188,13 @@ void whenPieceNotInDiagonalCanNotMove() { */ //when - chessBoard.move(new Moving(A2, A4)); - chessBoard.move(new Moving(G8, H6)); + chessGame.move(new Moving(A2, A4)); + chessGame.move(new Moving(G8, H6)); final Moving diagonalMoving = new Moving(A4, B5); //then - assertThatThrownBy(() -> chessBoard.move(diagonalMoving)) + assertThatThrownBy(() -> chessGame.move(diagonalMoving)) .isInstanceOf(InvalidMovingException.class); } @@ -207,16 +202,16 @@ void whenPieceNotInDiagonalCanNotMove() { @DisplayName("폰은 후진할 수 없다. WHITE") void failToMoveBackWhitePawn() { //given - final ChessBoard chessBoard = ChessBoard.setupStartingPosition(); + final ChessGame chessGame = ChessGame.setupStartingPosition(); //when - chessBoard.move(new Moving(H2, H4)); - chessBoard.move(new Moving(D7, D5)); + chessGame.move(new Moving(H2, H4)); + chessGame.move(new Moving(D7, D5)); final Moving whiteBackMoving = new Moving(H4, H3); //then - assertThatThrownBy(() -> chessBoard.move(whiteBackMoving)) + assertThatThrownBy(() -> chessGame.move(whiteBackMoving)) .isInstanceOf(InvalidMovingException.class); } @@ -224,17 +219,17 @@ void failToMoveBackWhitePawn() { @DisplayName("폰은 후진할 수 없다. BLACK") void failToMoveBackBlackPawn() { //given - final ChessBoard chessBoard = ChessBoard.setupStartingPosition(); + final ChessGame chessGame = ChessGame.setupStartingPosition(); //when - chessBoard.move(new Moving(H2, H4)); - chessBoard.move(new Moving(D7, D5)); - chessBoard.move(new Moving(H4, H5)); + chessGame.move(new Moving(H2, H4)); + chessGame.move(new Moving(D7, D5)); + chessGame.move(new Moving(H4, H5)); final Moving blackBackMoving = new Moving(D5, D6); //then - assertThatThrownBy(() -> chessBoard.move(blackBackMoving)) + assertThatThrownBy(() -> chessGame.move(blackBackMoving)) .isInstanceOf(InvalidMovingException.class); } } diff --git a/src/test/java/model/piece/PieceTest.java b/src/test/java/model/piece/PieceTest.java index 4950cd63529..bd1fa20faa5 100644 --- a/src/test/java/model/piece/PieceTest.java +++ b/src/test/java/model/piece/PieceTest.java @@ -28,7 +28,7 @@ static Stream invalidMovingParameterProvider() { Arguments.of(new Bishop(Camp.BLACK)), Arguments.of(new King(Camp.BLACK)), Arguments.of(new Knight(Camp.WHITE)), - Arguments.of(new Pawn(Camp.WHITE)), + Arguments.of(new WhitePawn()), Arguments.of(new Queen(Camp.WHITE)), Arguments.of(new Rook(Camp.WHITE)) ); diff --git a/src/test/java/model/status/EndTest.java b/src/test/java/model/status/EndTest.java index f7adb776910..6758223ee69 100644 --- a/src/test/java/model/status/EndTest.java +++ b/src/test/java/model/status/EndTest.java @@ -5,7 +5,8 @@ import exception.InvalidStatusException; import java.util.List; -import model.ChessBoard; +import model.ChessGame; +import model.command.CommandLine; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -15,22 +16,24 @@ class EndTest { @Test void failToPlayIfStatusEnd() { //given - final ChessBoard chessBoard = ChessBoard.setupStartingPosition(); - final GameStatus gameStatus = Initialization.gameSetting(List.of("start")); - final List endCommand = List.of("end"); + final ChessGame chessGame = ChessGame.setupStartingPosition(); + final CommandLine startCommand = CommandLine.from(List.of("start")); + final GameStatus gameStatus = StatusFactory.create(startCommand); + final CommandLine endCommand = CommandLine.from(List.of("end")); //when - final GameStatus play = gameStatus.play(endCommand, chessBoard); + final GameStatus play = gameStatus.play(endCommand, chessGame); //then - assertThatThrownBy(() -> play.play(endCommand, chessBoard)) + assertThatThrownBy(() -> play.play(endCommand, chessGame)) .isInstanceOf(InvalidStatusException.class); } @DisplayName("종료 상태일 때 상태를 확인한다.") @Test void checkRunning() { - final GameStatus gameStatus = Initialization.gameSetting(List.of("end")); + final CommandLine endCommand = CommandLine.from(List.of("end")); + final GameStatus gameStatus = StatusFactory.create(endCommand); assertThat(gameStatus.isRunning()).isFalse(); } diff --git a/src/test/java/model/status/RunningTest.java b/src/test/java/model/status/RunningTest.java index 844e05cf669..0f72f87acb5 100644 --- a/src/test/java/model/status/RunningTest.java +++ b/src/test/java/model/status/RunningTest.java @@ -5,7 +5,8 @@ import exception.InvalidStatusException; import java.util.List; -import model.ChessBoard; +import model.ChessGame; +import model.command.CommandLine; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -14,7 +15,8 @@ class RunningTest { @DisplayName("러닝 상태일때 상태를 확인한다.") @Test void checkRunning() { - final GameStatus gameStatus = Initialization.gameSetting(List.of("start")); + final CommandLine startCommand = CommandLine.from(List.of("start")); + final GameStatus gameStatus = StatusFactory.create(startCommand); assertThat(gameStatus.isRunning()).isTrue(); } @@ -22,35 +24,49 @@ void checkRunning() { @DisplayName("러닝 상태일 때 play 후 상태를 확인한다.") @Test void checkRunningAfterPlay() { - final GameStatus gameStatus = Initialization.gameSetting(List.of("start")); - final ChessBoard chessBoard = ChessBoard.setupStartingPosition(); + final CommandLine startCommand = CommandLine.from(List.of("start")); + final GameStatus gameStatus = StatusFactory.create(startCommand); + final ChessGame chessGame = ChessGame.setupStartingPosition(); - final List moveCommand = List.of("move", "a2", "a3"); - assertThat(gameStatus.play(moveCommand, chessBoard)) + final CommandLine moveCommand = CommandLine.from(List.of("move", "a2", "a3")); + assertThat(gameStatus.play(moveCommand, chessGame)) .isInstanceOf(Running.class); } @DisplayName("러닝 상태에서 end 후 상태를 학인한다.") @Test void checkRunningAfterEnd() { - final GameStatus gameStatus = Initialization.gameSetting(List.of("start")); - final ChessBoard chessBoard = ChessBoard.setupStartingPosition(); + final CommandLine startCommand = CommandLine.from(List.of("start")); + final GameStatus gameStatus = StatusFactory.create(startCommand); + final ChessGame chessGame = ChessGame.setupStartingPosition(); - final List endCommand = List.of("end"); - assertThat(gameStatus.play(endCommand, chessBoard)) + final CommandLine endCommand = CommandLine.from(List.of("end")); + assertThat(gameStatus.play(endCommand, chessGame)) .isInstanceOf(End.class); } + @DisplayName("러닝 상태에서 quit 후 상태를 학인한다.") + @Test + void checkRunningAfterQuit() { + final CommandLine startCommand = CommandLine.from(List.of("start")); + final GameStatus gameStatus = StatusFactory.create(startCommand); + final ChessGame chessGame = ChessGame.setupStartingPosition(); + + final CommandLine quitCommand = CommandLine.from(List.of("quit")); + assertThat(gameStatus.play(quitCommand, chessGame)) + .isInstanceOf(Quit.class); + } + @DisplayName("러닝 상태에서 start 하면 예외가 발생한다.") @Test void failToStartIfAlreadyRunning() { //given - final GameStatus gameStatus = Initialization.gameSetting(List.of("start")); - final ChessBoard chessBoard = ChessBoard.setupStartingPosition(); + final CommandLine startCommand = CommandLine.from(List.of("start")); + final GameStatus gameStatus = StatusFactory.create(startCommand); + final ChessGame chessGame = ChessGame.setupStartingPosition(); //when && then - final List startCommand = List.of("start"); - assertThatThrownBy(() -> gameStatus.play(startCommand, chessBoard)) + assertThatThrownBy(() -> gameStatus.play(startCommand, chessGame)) .isInstanceOf(InvalidStatusException.class); } } diff --git a/src/test/java/model/status/StatusFactoryTest.java b/src/test/java/model/status/StatusFactoryTest.java new file mode 100644 index 00000000000..439ded748c9 --- /dev/null +++ b/src/test/java/model/status/StatusFactoryTest.java @@ -0,0 +1,37 @@ +package model.status; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import exception.InvalidStatusException; +import java.util.List; +import model.command.CommandLine; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class StatusFactoryTest { + + @DisplayName("start를 입력하면 게임이 시작된다.") + @Test + void gameStartWhenCommandIsStart() { + final CommandLine startCommand = CommandLine.from(List.of("start")); + final GameStatus gameStatus = StatusFactory.create(startCommand); + assertThat(gameStatus).isInstanceOf(Running.class); + } + + @DisplayName("end를 입력하면 게임이 종료된다.") + @Test + void gameEndWhenCommandIsEnd() { + final CommandLine endCommand = CommandLine.from(List.of("end")); + final GameStatus gameStatus = StatusFactory.create(endCommand); + assertThat(gameStatus).isInstanceOf(End.class); + } + + @DisplayName("시작시 유효하지 않은 명령어가 오면 예외가 발생한다.") + @Test + void invalidCommand() { + CommandLine moveCommand = CommandLine.from(List.of("move", "a2", "d3")); + assertThatThrownBy(() -> StatusFactory.create(moveCommand)) + .isInstanceOf(InvalidStatusException.class); + } +} diff --git a/src/test/resources/board.sql b/src/test/resources/board.sql new file mode 100644 index 00000000000..4692c4edd38 --- /dev/null +++ b/src/test/resources/board.sql @@ -0,0 +1,9 @@ +use chess_test; + +create table board +( + position varchar(2) not null, + piece_type varchar(6) not null, + camp varchar(5) not null + +); diff --git a/src/test/resources/moving.sql b/src/test/resources/moving.sql new file mode 100644 index 00000000000..b1fbcefe0af --- /dev/null +++ b/src/test/resources/moving.sql @@ -0,0 +1,9 @@ +use chess_test; + +create table moving +( + movement_id INT primary key auto_increment, + camp varchar(5) not null, + start varchar(2) not null, + destination varchar(2) not null +); diff --git a/src/test/resources/turn.sql b/src/test/resources/turn.sql new file mode 100644 index 00000000000..ed9675d1736 --- /dev/null +++ b/src/test/resources/turn.sql @@ -0,0 +1,7 @@ +use chess_test; + +create table turn +( + camp varchar(5) not null, + count int not null +);