diff --git a/README.md b/README.md index 894296bb25e..6421f8a12b9 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,21 @@ - 단, 폰은 이동할 수 있는 위치에 상대 진영의 말이 있는 경우, 그 위치로 이동할 수 없다. 6. 체스 말이 이동할 수 있는 위치에 같은 진영의 말이 있는 경우, 그 위치로 이동할 수 없다. +### 게임 종료 조건 + +1. 킹이 잡히는 팀이 게임에서 진다. 킹이 잡히 게임이 종료된다. + +### 점수 + +1. 각 팀에는 점수가 있다. 팀의 점수는 살아있는 체스 말의 점수의 합이다. +2. 각 말의 점수는 다음과 같다. + - 퀸 : 9점 + - 룩 : 5점 + - 비숍 : 3점 + - 나이트 : 2.5점 + - 폰 : 1점 + - 같은 세로 위치에 같은 색의 폰이 여러개 있는 경우, 그 폰들은 점수가 0.5점이다. + ## 기능 목록 - [x] 체스 말 @@ -48,10 +63,92 @@ - [x] 체스 보드 - [x] 올바른 턴인지 확인한다 - [x] 잡힌 말을 제거한다 +- [x] 종료 조건 + - [x] 킹이 잡히면 게임이 종료되는 기능 +- [x] 점수 + - [x] 각 말의 점수 계산 기능 + - [x] 폰의 점수 보정 기능 + - [x] 각 팀의 점수 계산 기능 - [x] 사용자 입력 기능 - [x] 이동 위치 명령어 입력 기능 - [x] 게임 시작 명령어 입력 기능 - [x] 게임 종료 명령어 입력 기능 + - [x] 점수 조회 명령어 입력 기능 - [x] 출력 기능 - [x] 체스 판 출력 기능 - [x] 안내 문구 출력 기능 + - [x] 점수 출력 기능 +- [x] 데이터 베이스에 저장하는 기능 + - [x] 기물을 저장하는 기능 + - [x] 턴을 저장하는 기능 +- [x] 데이터베이스에 저장된 게임을 불러오는 기능 + +## 데이터베이스 + +### 저장해야 할 데이터 + +1. 각 기물의 위치 +2. 현재 기물을 움직일 수 있는 팀 + +### 테이블 스키마 + +``` SQL +create table piece ( + piece_type varchar(16), + primary key(piece_type) +); + +create table position ( + `name` char(2), + primary key(`name`) +); + +create table team ( + team_name varchar(16), + primary key(team_name) +); + +CREATE TABLE `game` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `current_team_name` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + PRIMARY KEY (`id`), + KEY `fj_team_game` (`current_team_name`), + CONSTRAINT `fj_team_game` FOREIGN KEY (`current_team_name`) REFERENCES `team` (`team_name`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE `pieces_on_board` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `piece_type` varchar(16) NOT NULL, + `team_name` varchar(16) NOT NULL, + `position_name` char(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + PRIMARY KEY (`id`), + KEY `fk-piece-pieces_on_board` (`piece_type`), + KEY `fk-position-pieces_on_board` (`position_name`), + KEY `fk-team-pieces_on_board` (`team_name`), + CONSTRAINT `fk-piece-pieces_on_board` FOREIGN KEY (`piece_type`) REFERENCES `piece` (`piece_type`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `fk-position-pieces_on_board` FOREIGN KEY (`position_name`) REFERENCES `position` (`name`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `fk-team-pieces_on_board` FOREIGN KEY (`team_name`) REFERENCES `team` (`team_name`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +``` + +### 데이터 저장시 고려해야 하는 것 + +1. 데이터가 저장된 적이 없는 경우 혹은 복구를 하지 않겠다고 하는 경우 => 데이터를 모두 지우고, 초기 체스 판을 저장한다. 최초 턴을 white로 지정한다. +2. 말을 움직인 경우, 시작 위치의 말을 지우고, 도착위치의 말을 지운 뒤, 도착 위치로 이동한 기물을 새로 저장한다. 그리고 턴을 바꿔준다. + +## 피드백 반영 예정 목록 + +### DB 관련 + +- [x] 트랜잭션 적용 +- [x] TurnDAO에서 비즈니스 로직 상위로 이동 +- [x] DAO에 boolean을 반환하는 메서드 추가 +- [x] 구체적인 예외를 던지도록 수정 +- [x] DAO 테스트 추가 +- [x] 킹이 잡힌 경우 데이터를 초기화하도록 수정 +- [x] 폰이 데이터베이스에서 로드된 뒤에 폰이 2칸 움직일 수 있는 버그 수정 + +### 도메인 로직 관련 + +- [x] 킹이 잡히는 경우 실제로 킹이 보드에서 삭제되지 않는 부분 수정 + 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/chess_2024-04-01.sql b/chess_2024-04-01.sql new file mode 100644 index 00000000000..956d763bb86 --- /dev/null +++ b/chess_2024-04-01.sql @@ -0,0 +1,195 @@ +# ************************************************************ +# Sequel Ace SQL dump +# Version 20062 +# +# https://sequel-ace.com/ +# https://github.com/Sequel-Ace/Sequel-Ace +# +# Host: localhost (MySQL 8.0.28) +# Database: chess +# Generation Time: 2024-04-01 05:43:10 +0000 +# ************************************************************ + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +SET NAMES utf8mb4; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE='NO_AUTO_VALUE_ON_ZERO', SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + + +# Dump of table game +# ------------------------------------------------------------ + +DROP TABLE IF EXISTS `game`; + +CREATE TABLE `game` ( + `current_team_name` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + KEY `fj_team_game` (`current_team_name`), + CONSTRAINT `fj_team_game` FOREIGN KEY (`current_team_name`) REFERENCES `team` (`team_name`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + + + +# Dump of table piece +# ------------------------------------------------------------ + +DROP TABLE IF EXISTS `piece`; + +CREATE TABLE `piece` ( + `piece_type` varchar(16) NOT NULL, + PRIMARY KEY (`piece_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +LOCK TABLES `piece` WRITE; +/*!40000 ALTER TABLE `piece` DISABLE KEYS */; + +INSERT INTO `piece` (`piece_type`) +VALUES + ('BISHOP'), + ('KING'), + ('KNIGHT'), + ('PAWN'), + ('QUEEN'), + ('ROOK'); + +/*!40000 ALTER TABLE `piece` ENABLE KEYS */; +UNLOCK TABLES; + + +# Dump of table pieces_on_board +# ------------------------------------------------------------ + +DROP TABLE IF EXISTS `pieces_on_board`; + +CREATE TABLE `pieces_on_board` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `piece_type` varchar(16) NOT NULL, + `team_name` varchar(16) NOT NULL, + `position_name` char(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + PRIMARY KEY (`id`), + KEY `fk-piece-pieces_on_board` (`piece_type`), + KEY `fk-position-pieces_on_board` (`position_name`), + KEY `fk-team-pieces_on_board` (`team_name`), + CONSTRAINT `fk-piece-pieces_on_board` FOREIGN KEY (`piece_type`) REFERENCES `piece` (`piece_type`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `fk-position-pieces_on_board` FOREIGN KEY (`position_name`) REFERENCES `position` (`name`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `fk-team-pieces_on_board` FOREIGN KEY (`team_name`) REFERENCES `team` (`team_name`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + + + +# Dump of table position +# ------------------------------------------------------------ + +DROP TABLE IF EXISTS `position`; + +CREATE TABLE `position` ( + `name` char(2) NOT NULL, + PRIMARY KEY (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +LOCK TABLES `position` WRITE; +/*!40000 ALTER TABLE `position` DISABLE KEYS */; + +INSERT INTO `position` (`name`) +VALUES + ('A1'), + ('A2'), + ('A3'), + ('A4'), + ('A5'), + ('A6'), + ('A7'), + ('A8'), + ('B1'), + ('B2'), + ('B3'), + ('B4'), + ('B5'), + ('B6'), + ('B7'), + ('B8'), + ('C1'), + ('C2'), + ('C3'), + ('C4'), + ('C5'), + ('C6'), + ('C7'), + ('C8'), + ('D1'), + ('D2'), + ('D3'), + ('D4'), + ('D5'), + ('D6'), + ('D7'), + ('D8'), + ('E1'), + ('E2'), + ('E3'), + ('E4'), + ('E5'), + ('E6'), + ('E7'), + ('E8'), + ('F1'), + ('F2'), + ('F3'), + ('F4'), + ('F5'), + ('F6'), + ('F7'), + ('F8'), + ('G1'), + ('G2'), + ('G3'), + ('G4'), + ('G5'), + ('G6'), + ('G7'), + ('G8'), + ('H1'), + ('H2'), + ('H3'), + ('H4'), + ('H5'), + ('H6'), + ('H7'), + ('H8'); + +/*!40000 ALTER TABLE `position` ENABLE KEYS */; +UNLOCK TABLES; + + +# Dump of table team +# ------------------------------------------------------------ + +DROP TABLE IF EXISTS `team`; + +CREATE TABLE `team` ( + `team_name` varchar(16) NOT NULL, + PRIMARY KEY (`team_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +LOCK TABLES `team` WRITE; +/*!40000 ALTER TABLE `team` DISABLE KEYS */; + +INSERT INTO `team` (`team_name`) +VALUES + ('black'), + ('white'); + +/*!40000 ALTER TABLE `team` ENABLE KEYS */; +UNLOCK TABLES; + + + +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000000..558a1d5a53f --- /dev/null +++ b/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/src/main/java/chess/Application.java b/src/main/java/chess/Application.java index f23dbba3c1f..fa71945f8a1 100644 --- a/src/main/java/chess/Application.java +++ b/src/main/java/chess/Application.java @@ -1,35 +1,51 @@ package chess; -import static chess.domain.game.EndCommand.END_COMMAND; +import static chess.domain.game.command.EndCommand.END_COMMAND; +import static chess.domain.piece.PieceMoveResult.FAILURE; +import static chess.domain.piece.Team.BLACK; +import static chess.domain.piece.Team.WHITE; -import chess.domain.Position; +import chess.dao.ChessPersistence; import chess.domain.game.ChessGame; -import chess.domain.game.Command; +import chess.domain.game.command.Command; +import chess.domain.game.command.MoveCommand; +import chess.domain.game.command.StatusCommand; import chess.domain.piece.Piece; +import chess.domain.piece.PieceMoveResult; import chess.domain.piece.PieceType; import chess.domain.piece.Team; import chess.dto.PieceDTO; import chess.view.InputView; import chess.view.OutputView; import java.util.List; +import java.util.Map; public class Application { + private static final ChessPersistence CHESS_PERSISTENCE_SERVICE = new ChessPersistence(); + public static void main(String[] args) { OutputView.printGuide(); Command startOrEnd = InputView.readStartOrEnd(); if (isEndCommand(startOrEnd)) { return; } - ChessGame chessGame = new ChessGame(); + ChessGame chessGame = CHESS_PERSISTENCE_SERVICE.loadChessGame(); + if (!CHESS_PERSISTENCE_SERVICE.isSaveDataExist()) { + CHESS_PERSISTENCE_SERVICE.saveChessGame(new ChessGame()); + } + printPiecesOnChessBoard(chessGame); + + playChess(chessGame); + } + + private static boolean isEndCommand(Command command) { + return command.equals(END_COMMAND); + } + + private static void printPiecesOnChessBoard(ChessGame chessGame) { List piecesOnBoard = chessGame.getPiecesOnBoard(); List pieceDTOS = piecesToDTO(piecesOnBoard); OutputView.printChessBoard(pieceDTOS); - - Command endOrMove = InputView.readEndOrMove(); - while (!isEndCommand(endOrMove)) { - playGame(endOrMove, chessGame); - endOrMove = InputView.readEndOrMove(); - } } private static List piecesToDTO(List piecesOnBoard) { @@ -42,20 +58,61 @@ private static List piecesToDTO(List piecesOnBoard) { }).toList(); } - private static boolean isEndCommand(Command command) { - return command.equals(END_COMMAND); + private static void playChess(ChessGame chessGame) { + Command endOrMoveOrStatus; + PieceMoveResult pieceMoveResult; + do { + endOrMoveOrStatus = InputView.readEndOrMoveOrStatus(); + pieceMoveResult = playGameOrPrintStatus(endOrMoveOrStatus, chessGame); + } while (!isEndCommand(endOrMoveOrStatus) && !pieceMoveResult.isEnd()); } - private static void playGame(Command moveCommand, ChessGame chessGame) { - List options = moveCommand.getOptions(); - Position from = options.get(0); - Position to = options.get(1); - boolean moveSuccess = chessGame.move(from, to); - List piecesOnBoard = chessGame.getPiecesOnBoard(); - List pieceDTOS = piecesToDTO(piecesOnBoard); - OutputView.printChessBoard(pieceDTOS); - if (!moveSuccess) { + private static PieceMoveResult playGameOrPrintStatus(Command endOrMoveOrStatus, ChessGame chessGame) { + if (isStatusCommand(endOrMoveOrStatus)) { + printStatus(chessGame); + return FAILURE; + } + if (isEndCommand(endOrMoveOrStatus)) { + return FAILURE; + } + return playGame((MoveCommand) endOrMoveOrStatus, chessGame); + } + + private static boolean isStatusCommand(Command endOrMoveOrStatus) { + return endOrMoveOrStatus.equals(StatusCommand.STATUS_COMMAND); + } + + private static void printStatus(ChessGame chessGame) { + Map scoresGroupingByTeam = chessGame.calculateScores(); + scoresGroupingByTeam.forEach(OutputView::printStatus); + double whiteTeamPoint = scoresGroupingByTeam.get(WHITE); + double blackTeamPoint = scoresGroupingByTeam.get(BLACK); + OutputView.currentWinner(whiteTeamPoint, blackTeamPoint); + } + + private static PieceMoveResult playGame(MoveCommand moveCommand, ChessGame chessGame) { + PieceMoveResult moveResult = chessGame.move(moveCommand); + if (!moveResult.equals(FAILURE)) { + CHESS_PERSISTENCE_SERVICE.updateChessGame(chessGame, moveCommand); + } + if (moveResult.isEnd()) { + CHESS_PERSISTENCE_SERVICE.deleteAll(); + } + printPiecesOnChessBoard(chessGame); + printReInputGuideIfNeed(moveResult); + printWinnerIfNeed(moveResult); + return moveResult; + } + + private static void printReInputGuideIfNeed(PieceMoveResult moveResult) { + if (moveResult.equals(FAILURE)) { OutputView.printReInputGuide(); } } + + private static void printWinnerIfNeed(PieceMoveResult moveResult) { + if (moveResult.isEnd()) { + OutputView.printWinner(moveResult); + } + } } diff --git a/src/main/java/chess/dao/ChessPersistence.java b/src/main/java/chess/dao/ChessPersistence.java new file mode 100644 index 00000000000..7f82339e7a5 --- /dev/null +++ b/src/main/java/chess/dao/ChessPersistence.java @@ -0,0 +1,119 @@ +package chess.dao; + +import chess.dao.exception.DBException; +import chess.domain.Position; +import chess.domain.game.ChessGame; +import chess.domain.game.command.MoveCommand; +import chess.domain.piece.Piece; +import chess.domain.piece.Team; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +public class ChessPersistence { + private final PiecesOnChessBoardDAO piecesOnChessBoardDAO; + private final TurnDAO turnDAO; + private final DBConnectionCache dbConnectionCache; + + public ChessPersistence() { + this(new PiecesOnChessBoardDAOForMysql(), new TurnDAOForMysql(), new MysqlDBConnectionCache()); + } + + ChessPersistence(PiecesOnChessBoardDAO piecesOnChessBoardDAO, TurnDAO turnDAO, + DBConnectionCache dbConnectionCache) { + this.piecesOnChessBoardDAO = piecesOnChessBoardDAO; + this.turnDAO = turnDAO; + this.dbConnectionCache = dbConnectionCache; + } + + public ChessGame loadChessGame() { + Connection connection = dbConnectionCache.getConnection(); + startTransaction(connection); + if (isSaveDataExist()) { + List pieces = piecesOnChessBoardDAO.selectAll(connection); + Team currentTeam = turnDAO.select(connection).get(); + commitTransaction(connection); + return new ChessGame(pieces, currentTeam); + } + piecesOnChessBoardDAO.deleteAll(connection); + turnDAO.delete(connection); + commitTransaction(connection); + return new ChessGame(); + } + + private void startTransaction(Connection connection) { + try { + connection.setAutoCommit(false); + } catch (SQLException e) { + throw new DBException("DB 접근에 오류가 발생했습니다.", e); + } + } + + public boolean isSaveDataExist() { + Connection connection = dbConnectionCache.getConnection(); + boolean turnNotEmpty = turnDAO.isNotEmpty(connection); + boolean piecesNotEmpty = piecesOnChessBoardDAO.isNotEmpty(connection); + return turnNotEmpty && piecesNotEmpty; + } + + private void commitTransaction(Connection connection) { + try { + connection.commit(); + connection.setAutoCommit(true); + } catch (SQLException e) { + throw new DBException("DB 접근에 오류가 발생했습니다.", e); + } + } + + public boolean saveChessGame(ChessGame chessGame) { + Connection connection = dbConnectionCache.getConnection(); + List piecesOnBoard = chessGame.getPiecesOnBoard(); + startTransaction(connection); + if (canNotSave(connection)) { + return false; + } + boolean saveAllPiecesSuccess = piecesOnChessBoardDAO.saveAll(piecesOnBoard, connection); + Team currentTeam = chessGame.currentTeam(); + boolean saveTurnSuccess = turnDAO.save(currentTeam, connection); + commitTransaction(connection); + return saveAllPiecesSuccess && saveTurnSuccess; + } + + private boolean canNotSave(Connection connection) { + boolean piecesNotEmpty = piecesOnChessBoardDAO.isNotEmpty(connection); + boolean turnNotEmpty = turnDAO.isNotEmpty(connection); + return piecesNotEmpty || turnNotEmpty; + } + + public boolean updateChessGame(ChessGame chessGame, MoveCommand moveCommand) { + Connection connection = dbConnectionCache.getConnection(); + List positions = moveCommand.getOptions(); + Position from = positions.get(0); + Position to = positions.get(1); + startTransaction(connection); + boolean deleteFromSuccess = piecesOnChessBoardDAO.delete(from, connection); + piecesOnChessBoardDAO.delete(to, connection); + Piece movedPiece = findMovedPiece(chessGame, to); + boolean saveMovedPieceSuccess = piecesOnChessBoardDAO.save(movedPiece, connection); + Team team = turnDAO.select(connection) + .orElseThrow(() -> new DBException("데이터가 잘못되었습니다.", null)); + boolean updateTurnSuccess = turnDAO.update(team, team.otherTeam(), connection); + commitTransaction(connection); + return deleteFromSuccess && saveMovedPieceSuccess && updateTurnSuccess; + } + + private Piece findMovedPiece(ChessGame chessGame, Position to) { + return chessGame.getPiecesOnBoard().stream() + .filter(piece -> piece.isOn(to)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("움직인 말이 없습니다.", null)); + } + + public void deleteAll() { + Connection connection = dbConnectionCache.getConnection(); + startTransaction(connection); + piecesOnChessBoardDAO.deleteAll(connection); + turnDAO.delete(connection); + commitTransaction(connection); + } +} diff --git a/src/main/java/chess/dao/DBConnectionCache.java b/src/main/java/chess/dao/DBConnectionCache.java new file mode 100644 index 00000000000..0d6a736da12 --- /dev/null +++ b/src/main/java/chess/dao/DBConnectionCache.java @@ -0,0 +1,7 @@ +package chess.dao; + +import java.sql.Connection; + +public interface DBConnectionCache { + Connection getConnection(); +} diff --git a/src/main/java/chess/dao/MysqlDBConnectionCache.java b/src/main/java/chess/dao/MysqlDBConnectionCache.java new file mode 100644 index 00000000000..4e4c78521e6 --- /dev/null +++ b/src/main/java/chess/dao/MysqlDBConnectionCache.java @@ -0,0 +1,52 @@ +package chess.dao; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class MysqlDBConnectionCache implements DBConnectionCache { + private static final String DB_URL = "jdbc:mysql://localhost:13306/chess?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"; + private static final String DB_USER_NAME = "user"; + private static final String DB_USER_PASSWORD = "password"; + private final DBConnectionParameters dbConnectionParameters; + private Connection connection; + + public MysqlDBConnectionCache() { + this(DB_URL, DB_USER_NAME, DB_USER_PASSWORD); + } + + public MysqlDBConnectionCache(String url, String userName, String password) { + dbConnectionParameters = new DBConnectionParameters(url, userName, password); + connection = createConnection(dbConnectionParameters); + } + + private Connection createConnection(DBConnectionParameters dbConnectionParameters) { + String url = dbConnectionParameters.url; + String userName = dbConnectionParameters.userName; + String password = dbConnectionParameters.password; + try { + return DriverManager.getConnection(url, userName, password); + } catch (SQLException e) { + throw new RuntimeException("DB 접속 에러", e); + } + } + + @Override + public Connection getConnection() { + try { + return getConnectionCanThrow(); + } catch (SQLException e) { + throw new RuntimeException("DB 접속 에러"); + } + } + + private Connection getConnectionCanThrow() throws SQLException { + if (!connection.isValid(1)) { + connection = createConnection(dbConnectionParameters); + } + return connection; + } + + private record DBConnectionParameters(String url, String userName, String password) { + } +} diff --git a/src/main/java/chess/dao/PiecesOnChessBoardDAO.java b/src/main/java/chess/dao/PiecesOnChessBoardDAO.java new file mode 100644 index 00000000000..571dbadbb0f --- /dev/null +++ b/src/main/java/chess/dao/PiecesOnChessBoardDAO.java @@ -0,0 +1,20 @@ +package chess.dao; + +import chess.domain.Position; +import chess.domain.piece.Piece; +import java.sql.Connection; +import java.util.List; + +public interface PiecesOnChessBoardDAO { + boolean save(Piece piece, Connection connection); + + boolean saveAll(List pieces, Connection connection); + + List selectAll(Connection connection); + + boolean isNotEmpty(Connection connection); + + boolean delete(Position targetPosition, Connection connection); + + void deleteAll(Connection connection); +} diff --git a/src/main/java/chess/dao/PiecesOnChessBoardDAOForMysql.java b/src/main/java/chess/dao/PiecesOnChessBoardDAOForMysql.java new file mode 100644 index 00000000000..08e2fc3d092 --- /dev/null +++ b/src/main/java/chess/dao/PiecesOnChessBoardDAOForMysql.java @@ -0,0 +1,136 @@ +package chess.dao; + +import chess.domain.Position; +import chess.domain.piece.Bishop; +import chess.domain.piece.King; +import chess.domain.piece.Knight; +import chess.domain.piece.Pawn; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceType; +import chess.domain.piece.Queen; +import chess.domain.piece.Rook; +import chess.domain.piece.Team; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class PiecesOnChessBoardDAOForMysql implements PiecesOnChessBoardDAO { + + @Override + public boolean save(Piece piece, Connection connection) { + try { + PreparedStatement preparedStatement = connection.prepareStatement( + "insert into pieces_on_board (piece_type, team_name, position_name) values ( ?, ? ,? )"); + setPieceToPreparedStatement(List.of(piece), preparedStatement); + return preparedStatement.executeUpdate() == 1; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean saveAll(List pieces, Connection connection) { + String sql = pieces.stream().map(piece -> "( ?, ? ,? )") + .collect(Collectors.joining(",")); + sql = "insert into pieces_on_board (piece_type, team_name, position_name) values " + sql; + try { + PreparedStatement preparedStatement = connection.prepareStatement(sql); + setPieceToPreparedStatement(pieces, preparedStatement); + return preparedStatement.executeUpdate() == pieces.size(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public List selectAll(Connection connection) { + try { + PreparedStatement preparedStatement = connection.prepareStatement( + "select piece_type, team_name, position_name from pieces_on_board"); + ResultSet resultSet = preparedStatement.executeQuery(); + return parsingResultSet(resultSet); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean isNotEmpty(Connection connection) { + try { + PreparedStatement preparedStatement = connection.prepareStatement( + "select 1 from pieces_on_board where exists(select 1 from pieces_on_board) limit 1"); + ResultSet resultSet = preparedStatement.executeQuery(); + return resultSet.next(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean delete(Position targetPosition, Connection connection) { + try { + PreparedStatement preparedStatement = connection.prepareStatement( + "delete from pieces_on_board where position_name = ?"); + preparedStatement.setString(1, targetPosition.name()); + return preparedStatement.executeUpdate() == 1; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public void deleteAll(Connection connection) { + try { + PreparedStatement preparedStatement = connection.prepareStatement("delete from pieces_on_board"); + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private List parsingResultSet(ResultSet resultSet) throws SQLException { + List selected = new ArrayList<>(); + while (resultSet.next()) { + String piece_type = resultSet.getString(1); + String team_name = resultSet.getString(2); + String position_name = resultSet.getString(3); + PieceType pieceType = PieceType.valueOf(piece_type); + Team team = Team.valueOf(team_name); + Position position = Position.valueOf(position_name); + Piece piece = mapToPiece(pieceType, team, position); + selected.add(piece); + } + return Collections.unmodifiableList(selected); + } + + private Piece mapToPiece(PieceType pieceType, Team team, Position position) { + return switch (pieceType) { + case BISHOP -> new Bishop(position, team); + case KING -> new King(position, team); + case KNIGHT -> new Knight(position, team); + case PAWN -> new Pawn(position, team); + case QUEEN -> new Queen(position, team); + case ROOK -> new Rook(position, team); + }; + } + + private static void setPieceToPreparedStatement(List pieces, PreparedStatement preparedStatement) + throws SQLException { + for (int index = 0; index < pieces.size(); index++) { + Piece piece = pieces.get(index); + PieceType pieceType = piece.getPieceType(); + Team team = piece.getTeam(); + int row = piece.getRow(); + int column = piece.getColumn(); + Position position = Position.getInstance(row, column); + preparedStatement.setString((index * 3) + 1, pieceType.name()); + preparedStatement.setString((index * 3) + 2, team.name()); + preparedStatement.setString((index * 3) + 3, position.name()); + } + } +} diff --git a/src/main/java/chess/dao/TurnDAO.java b/src/main/java/chess/dao/TurnDAO.java new file mode 100644 index 00000000000..5d945a990c1 --- /dev/null +++ b/src/main/java/chess/dao/TurnDAO.java @@ -0,0 +1,17 @@ +package chess.dao; + +import chess.domain.piece.Team; +import java.sql.Connection; +import java.util.Optional; + +public interface TurnDAO { + Optional select(Connection connection); + + boolean isNotEmpty(Connection connection); + + boolean save(Team team, Connection connection); + + boolean update(Team targetTeam, Team updatedTeam, Connection connection); + + void delete(Connection connection); +} diff --git a/src/main/java/chess/dao/TurnDAOForMysql.java b/src/main/java/chess/dao/TurnDAOForMysql.java new file mode 100644 index 00000000000..756ae84f629 --- /dev/null +++ b/src/main/java/chess/dao/TurnDAOForMysql.java @@ -0,0 +1,74 @@ +package chess.dao; + +import chess.domain.piece.Team; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; + +public class TurnDAOForMysql implements TurnDAO { + + @Override + public Optional select(Connection connection) { + try { + PreparedStatement preparedStatement = connection.prepareStatement("select current_team_name from game"); + ResultSet resultSet = preparedStatement.executeQuery(); + resultSet.next(); + if (!(resultSet.isFirst() && resultSet.isLast())) { + return Optional.empty(); + } + String teamName = resultSet.getString(1); + return Optional.of(Team.valueOf(teamName)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean isNotEmpty(Connection connection) { + try { + PreparedStatement preparedStatement = connection.prepareStatement( + "select 1 from game where exists(select 1 from game) limit 1"); + ResultSet resultSet = preparedStatement.executeQuery(); + return resultSet.next(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean save(Team team, Connection connection) { + try { + PreparedStatement preparedStatement = connection.prepareStatement( + "INSERT INTO game (current_team_name) values ( ? )"); + preparedStatement.setString(1, team.name()); + return preparedStatement.executeUpdate() == 1; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean update(Team targetTeam, Team updatedTeam, Connection connection) { + try { + PreparedStatement preparedStatement = connection.prepareStatement( + "update game set current_team_name = ? where current_team_name = ?"); + preparedStatement.setString(1, updatedTeam.name()); + preparedStatement.setString(2, targetTeam.name()); + return preparedStatement.executeUpdate() == 1; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public void delete(Connection connection) { + try { + PreparedStatement preparedStatement = connection.prepareStatement("delete from game"); + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/chess/dao/exception/DBException.java b/src/main/java/chess/dao/exception/DBException.java new file mode 100644 index 00000000000..c6e90521515 --- /dev/null +++ b/src/main/java/chess/dao/exception/DBException.java @@ -0,0 +1,7 @@ +package chess.dao.exception; + +public class DBException extends RuntimeException { + public DBException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/chess/domain/Position.java b/src/main/java/chess/domain/Position.java index 1fa07cd4bf6..513d4014f5a 100644 --- a/src/main/java/chess/domain/Position.java +++ b/src/main/java/chess/domain/Position.java @@ -24,7 +24,7 @@ public enum Position { this.column = column; } - static Position getInstance(int row, int column) { + public static Position getInstance(int row, int column) { return Arrays.stream(values()) .filter(position -> position.row == row) .filter(position -> position.column == column) diff --git a/src/main/java/chess/domain/board/ChessBoard.java b/src/main/java/chess/domain/board/ChessBoard.java index 418ce4de306..23bb5db7d3a 100644 --- a/src/main/java/chess/domain/board/ChessBoard.java +++ b/src/main/java/chess/domain/board/ChessBoard.java @@ -1,10 +1,12 @@ package chess.domain.board; import static chess.domain.board.InitialPieces.INITIAL_PIECES; +import static chess.domain.piece.Team.WHITE; import chess.domain.Position; import chess.domain.piece.Piece; import chess.domain.piece.PieceMoveResult; +import chess.domain.piece.PieceType; import chess.domain.piece.Team; import java.util.ArrayList; import java.util.Collections; @@ -13,34 +15,62 @@ public class ChessBoard { private final List piecesOnBoard; - private Team currentTeam = Team.WHITE; + private Team currentTeam; public ChessBoard() { this(INITIAL_PIECES); } public ChessBoard(List pieces) { - piecesOnBoard = new ArrayList<>(pieces); + this(pieces, WHITE); + } + + public ChessBoard(List pieces, Team currentTeam) { + this.piecesOnBoard = new ArrayList<>(pieces); + this.currentTeam = currentTeam; } public ChessBoard(Piece... pieces) { - this(List.of(pieces)); + this(List.of(pieces), WHITE); } - boolean move(Position from, Position to) { + PieceMoveResult move(Position from, Position to) { if (isEmptyPosition(from) || isOtherTeamTurn(from)) { - return false; + return PieceMoveResult.FAILURE; } Piece piece = findPiece(from); PieceMoveResult moveResult = piece.move(to, this); removePieceIfCaught(to, moveResult); + moveResult = fixMoveResultWhenGameEnd(to, moveResult); changeCurrentTeamIfNotFail(moveResult); - return moveResult.toBoolean(); + return moveResult; + } + + private PieceMoveResult fixMoveResultWhenGameEnd(Position to, PieceMoveResult moveResult) { + boolean gameEnd = isGameEnd(to); + if (gameEnd && currentTeam.equals(WHITE)) { + return PieceMoveResult.WHITE_WIN; + } + if (gameEnd && currentTeam.equals(Team.BLACK)) { + return PieceMoveResult.BLACK_WIN; + } + return moveResult; + } + + private boolean isGameEnd(Position to) { + return piecesOnBoard.stream() + .filter(this::isKing) + .filter(this::isOtherTeam) + .allMatch(piece -> piece.isOn(to)); + } + + private boolean isKing(Piece piece) { + return piece.getPieceType().equals(PieceType.KING); } private boolean isEmptyPosition(Position from) { Optional optionalPiece = piecesOnBoard.stream() - .filter(piece1 -> piece1.isOn(from)) + .filter(piece -> piece.isOn(from)) .findFirst(); return optionalPiece.isEmpty(); } @@ -67,15 +97,15 @@ private void removePieceIfCaught(Position to, PieceMoveResult moveResult) { private void removeDeadPiece(Position to) { Piece needToRemovePiece = piecesOnBoard.stream() .filter(piece -> piece.isOn(to)) - .filter(piece -> { - Team pieceTeam = piece.getTeam(); - Team otherTeam = currentTeam.otherTeam(); - return pieceTeam.equals(otherTeam); - }) + .filter(this::isOtherTeam) .findFirst().orElseThrow(); piecesOnBoard.remove(needToRemovePiece); } + private boolean isOtherTeam(Piece piece) { + return piece.isTeamWith(currentTeam.otherTeam()); + } + private void changeCurrentTeamIfNotFail(PieceMoveResult moveResult) { if (!moveResult.equals(PieceMoveResult.FAILURE)) { currentTeam = currentTeam.otherTeam(); @@ -91,4 +121,12 @@ public Optional whichTeam(Position position) { List getPiecesOnBoard() { return Collections.unmodifiableList(piecesOnBoard); } + + double calculatePoint(Team team) { + return PointCalculator.calculatePoint(team, piecesOnBoard); + } + + Team getCurrentTeam() { + return currentTeam; + } } diff --git a/src/main/java/chess/domain/board/ChessBoardAdaptor.java b/src/main/java/chess/domain/board/ChessBoardAdaptor.java new file mode 100644 index 00000000000..30e0878be18 --- /dev/null +++ b/src/main/java/chess/domain/board/ChessBoardAdaptor.java @@ -0,0 +1,46 @@ +package chess.domain.board; + +import static chess.domain.piece.Team.BLACK; +import static chess.domain.piece.Team.WHITE; + +import chess.domain.Position; +import chess.domain.game.ChessBoardForChessGame; +import chess.domain.game.command.MoveCommand; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceMoveResult; +import chess.domain.piece.Team; +import java.util.List; +import java.util.Map; + +public class ChessBoardAdaptor implements ChessBoardForChessGame { + private final ChessBoard chessBoard; + + public ChessBoardAdaptor(ChessBoard chessBoard) { + this.chessBoard = chessBoard; + } + + @Override + public PieceMoveResult move(MoveCommand moveCommand) { + List positions = moveCommand.getOptions(); + Position from = positions.get(0); + Position to = positions.get(1); + return chessBoard.move(from, to); + } + + @Override + public List getPiecesOnBoard() { + return chessBoard.getPiecesOnBoard(); + } + + @Override + public Map calculateScores() { + double blackTeamScore = chessBoard.calculatePoint(BLACK); + double whiteTeamScore = chessBoard.calculatePoint(WHITE); + return Map.of(BLACK, blackTeamScore, WHITE, whiteTeamScore); + } + + @Override + public Team getCurrentTeam() { + return chessBoard.getCurrentTeam(); + } +} diff --git a/src/main/java/chess/domain/board/ChessBoardWrapper.java b/src/main/java/chess/domain/board/ChessBoardWrapper.java deleted file mode 100644 index 192da8a099a..00000000000 --- a/src/main/java/chess/domain/board/ChessBoardWrapper.java +++ /dev/null @@ -1,21 +0,0 @@ -package chess.domain.board; - -import chess.domain.Position; -import chess.domain.piece.Piece; -import java.util.List; - -public class ChessBoardWrapper { - private final ChessBoard chessBoard; - - public ChessBoardWrapper(ChessBoard chessBoard) { - this.chessBoard = chessBoard; - } - - public boolean move(Position from, Position to) { - return chessBoard.move(from, to); - } - - public List getPiecesOnBoard() { - return chessBoard.getPiecesOnBoard(); - } -} diff --git a/src/main/java/chess/domain/board/PointCalculator.java b/src/main/java/chess/domain/board/PointCalculator.java new file mode 100644 index 00000000000..ce51cbaeda6 --- /dev/null +++ b/src/main/java/chess/domain/board/PointCalculator.java @@ -0,0 +1,46 @@ +package chess.domain.board; + +import chess.domain.piece.Piece; +import chess.domain.piece.PieceType; +import chess.domain.piece.Team; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +class PointCalculator { + static double calculatePoint(Team team, List piecesOnBoard) { + piecesOnBoard = Collections.unmodifiableList(piecesOnBoard); + double totalPoint = calculateWithOutSameColumnPawn(team, piecesOnBoard); + Map> pawnsAtSameColumn = pawnGroupingByColumn(team, piecesOnBoard); + double correctionPoint = calculatePawnCorrectionPoint(pawnsAtSameColumn); + return totalPoint - correctionPoint; + } + + private static double calculateWithOutSameColumnPawn(Team team, List piecesOnBoard) { + return piecesOnBoard.stream() + .filter(piece -> piece.isTeamWith(team)) + .mapToDouble(Piece::getPoint) + .reduce(0.0, Double::sum); + } + + private static Map> pawnGroupingByColumn(Team team, List piecesOnBoard) { + return piecesOnBoard.stream() + .filter(piece -> piece.isTeamWith(team)) + .filter(PointCalculator::isPawn) + .collect(Collectors.groupingBy(Piece::getColumn, Collectors.toUnmodifiableList())); + } + + private static boolean isPawn(Piece piece) { + PieceType pieceType = piece.getPieceType(); + return pieceType.equals(PieceType.PAWN); + } + + private static double calculatePawnCorrectionPoint(Map> pawnsAtSameColumn) { + final double correctionPointPerPawn = 0.5; + return pawnsAtSameColumn.values().stream() + .filter(pieces -> pieces.size() > 1) + .mapToDouble(pieces -> pieces.size() * correctionPointPerPawn) + .reduce(0.0, Double::sum); + } +} diff --git a/src/main/java/chess/domain/game/ChessBoardForChessGame.java b/src/main/java/chess/domain/game/ChessBoardForChessGame.java new file mode 100644 index 00000000000..e511458853b --- /dev/null +++ b/src/main/java/chess/domain/game/ChessBoardForChessGame.java @@ -0,0 +1,18 @@ +package chess.domain.game; + +import chess.domain.game.command.MoveCommand; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceMoveResult; +import chess.domain.piece.Team; +import java.util.List; +import java.util.Map; + +public interface ChessBoardForChessGame { + PieceMoveResult move(MoveCommand moveCommand); + + List getPiecesOnBoard(); + + Map calculateScores(); + + Team getCurrentTeam(); +} diff --git a/src/main/java/chess/domain/game/ChessGame.java b/src/main/java/chess/domain/game/ChessGame.java index 929ef2c2e46..1fec0d55913 100644 --- a/src/main/java/chess/domain/game/ChessGame.java +++ b/src/main/java/chess/domain/game/ChessGame.java @@ -1,23 +1,38 @@ package chess.domain.game; -import chess.domain.Position; import chess.domain.board.ChessBoard; -import chess.domain.board.ChessBoardWrapper; +import chess.domain.board.ChessBoardAdaptor; +import chess.domain.game.command.MoveCommand; import chess.domain.piece.Piece; +import chess.domain.piece.PieceMoveResult; +import chess.domain.piece.Team; import java.util.List; +import java.util.Map; public class ChessGame { - private final ChessBoardWrapper chessBoardWrapper; + private final ChessBoardForChessGame chessBoard; + + public ChessGame(List pieces, Team currentTeam) { + this.chessBoard = new ChessBoardAdaptor(new ChessBoard(pieces, currentTeam)); + } public ChessGame() { - this.chessBoardWrapper = new ChessBoardWrapper(new ChessBoard()); + this.chessBoard = new ChessBoardAdaptor(new ChessBoard()); + } + + public PieceMoveResult move(MoveCommand moveCommand) { + return chessBoard.move(moveCommand); } - public boolean move(Position from, Position to) { - return chessBoardWrapper.move(from, to); + public Map calculateScores() { + return chessBoard.calculateScores(); } public List getPiecesOnBoard() { - return chessBoardWrapper.getPiecesOnBoard(); + return chessBoard.getPiecesOnBoard(); + } + + public Team currentTeam() { + return chessBoard.getCurrentTeam(); } } diff --git a/src/main/java/chess/domain/game/Command.java b/src/main/java/chess/domain/game/command/Command.java similarity index 86% rename from src/main/java/chess/domain/game/Command.java rename to src/main/java/chess/domain/game/command/Command.java index 4496d31d256..aec10e6e0aa 100644 --- a/src/main/java/chess/domain/game/Command.java +++ b/src/main/java/chess/domain/game/command/Command.java @@ -1,4 +1,4 @@ -package chess.domain.game; +package chess.domain.game.command; import java.util.List; diff --git a/src/main/java/chess/domain/game/EndCommand.java b/src/main/java/chess/domain/game/command/EndCommand.java similarity index 91% rename from src/main/java/chess/domain/game/EndCommand.java rename to src/main/java/chess/domain/game/command/EndCommand.java index d9935e49852..a4d4dc46d86 100644 --- a/src/main/java/chess/domain/game/EndCommand.java +++ b/src/main/java/chess/domain/game/command/EndCommand.java @@ -1,4 +1,4 @@ -package chess.domain.game; +package chess.domain.game.command; import java.util.List; diff --git a/src/main/java/chess/domain/game/MoveCommand.java b/src/main/java/chess/domain/game/command/MoveCommand.java similarity index 92% rename from src/main/java/chess/domain/game/MoveCommand.java rename to src/main/java/chess/domain/game/command/MoveCommand.java index 58f8216507d..87749c15c03 100644 --- a/src/main/java/chess/domain/game/MoveCommand.java +++ b/src/main/java/chess/domain/game/command/MoveCommand.java @@ -1,4 +1,4 @@ -package chess.domain.game; +package chess.domain.game.command; import chess.domain.Position; import java.util.Arrays; diff --git a/src/main/java/chess/domain/game/StartCommand.java b/src/main/java/chess/domain/game/command/StartCommand.java similarity index 91% rename from src/main/java/chess/domain/game/StartCommand.java rename to src/main/java/chess/domain/game/command/StartCommand.java index 5bee41f3299..8671a7427fa 100644 --- a/src/main/java/chess/domain/game/StartCommand.java +++ b/src/main/java/chess/domain/game/command/StartCommand.java @@ -1,4 +1,4 @@ -package chess.domain.game; +package chess.domain.game.command; import java.util.List; diff --git a/src/main/java/chess/domain/game/command/StatusCommand.java b/src/main/java/chess/domain/game/command/StatusCommand.java new file mode 100644 index 00000000000..257cb39181f --- /dev/null +++ b/src/main/java/chess/domain/game/command/StatusCommand.java @@ -0,0 +1,20 @@ +package chess.domain.game.command; + +import java.util.List; + +public class StatusCommand extends Command { + public static final StatusCommand STATUS_COMMAND = new StatusCommand(); + + private StatusCommand() { + this(null); + } + + private StatusCommand(String[] options) { + super(options); + } + + @Override + public List getOptions() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/chess/domain/piece/AbstractCatchOnMovePiece.java b/src/main/java/chess/domain/piece/AbstractCatchOnMovePiece.java index c0ef9c8b4e0..2d2c6e1d6a4 100644 --- a/src/main/java/chess/domain/piece/AbstractCatchOnMovePiece.java +++ b/src/main/java/chess/domain/piece/AbstractCatchOnMovePiece.java @@ -33,16 +33,22 @@ protected abstract Optional tryMoveAssumeAloneAndCheckRoute(Pos private boolean isMyTeam(Position targetPosition, ChessBoard chessBoard) { return chessBoard.whichTeam(targetPosition) - .filter(team -> team.equals(getTeam())) + .filter(this::isMyTeam) .isPresent(); } + private boolean isMyTeam(Team team) { + return team.equals(getTeam()); + } + private boolean isOtherTeam(Position targetPosition, ChessBoard chessBoard) { return chessBoard.whichTeam(targetPosition) - .filter(team -> { - Team otherTeam = getTeam().otherTeam(); - return team.equals(otherTeam); - }) + .filter(this::isOtherTeam) .isPresent(); } + + private boolean isOtherTeam(Team team) { + Team otherTeam = getTeam().otherTeam(); + return team.equals(otherTeam); + } } diff --git a/src/main/java/chess/domain/piece/AbstractPiece.java b/src/main/java/chess/domain/piece/AbstractPiece.java index 10aa97fc999..59d61fc00fa 100644 --- a/src/main/java/chess/domain/piece/AbstractPiece.java +++ b/src/main/java/chess/domain/piece/AbstractPiece.java @@ -46,7 +46,36 @@ public int getRow() { return position.getRow(); } + @Override + public boolean isTeamWith(Team team) { + return this.team.equals(team); + } + protected Position getPosition() { return position; } + + @Override + public int hashCode() { + int result = team != null ? team.hashCode() : 0; + result = 31 * result + (position != null ? position.hashCode() : 0); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + AbstractPiece that = (AbstractPiece) o; + + if (team != that.team) { + return false; + } + return position == that.position; + } } diff --git a/src/main/java/chess/domain/piece/Bishop.java b/src/main/java/chess/domain/piece/Bishop.java index 4fed465d93b..16ba67ebe26 100644 --- a/src/main/java/chess/domain/piece/Bishop.java +++ b/src/main/java/chess/domain/piece/Bishop.java @@ -24,4 +24,9 @@ public Optional tryMoveAssumeAlone(Position targetPosition, Che public PieceType getPieceType() { return PieceType.BISHOP; } + + @Override + public double getPoint() { + return 3.0; + } } diff --git a/src/main/java/chess/domain/piece/King.java b/src/main/java/chess/domain/piece/King.java index cc55f36e05a..dba4ecff778 100644 --- a/src/main/java/chess/domain/piece/King.java +++ b/src/main/java/chess/domain/piece/King.java @@ -24,4 +24,9 @@ public Optional tryMoveAssumeAloneAndCheckRoute(Position target public PieceType getPieceType() { return PieceType.KING; } + + @Override + public double getPoint() { + return 0.0; + } } diff --git a/src/main/java/chess/domain/piece/Knight.java b/src/main/java/chess/domain/piece/Knight.java index 207c260cf14..8816c91c966 100644 --- a/src/main/java/chess/domain/piece/Knight.java +++ b/src/main/java/chess/domain/piece/Knight.java @@ -29,4 +29,9 @@ private boolean isMovablePosition(Position targetPosition) { public PieceType getPieceType() { return PieceType.KNIGHT; } + + @Override + public double getPoint() { + return 2.5; + } } diff --git a/src/main/java/chess/domain/piece/Pawn.java b/src/main/java/chess/domain/piece/Pawn.java index ccb63d82448..2f3eecce9d7 100644 --- a/src/main/java/chess/domain/piece/Pawn.java +++ b/src/main/java/chess/domain/piece/Pawn.java @@ -1,17 +1,39 @@ package chess.domain.piece; +import static chess.domain.Position.A2; +import static chess.domain.Position.A7; +import static chess.domain.Position.B2; +import static chess.domain.Position.B7; +import static chess.domain.Position.C2; +import static chess.domain.Position.C7; +import static chess.domain.Position.D2; +import static chess.domain.Position.D7; +import static chess.domain.Position.E2; +import static chess.domain.Position.E7; +import static chess.domain.Position.F2; +import static chess.domain.Position.F7; +import static chess.domain.Position.G2; +import static chess.domain.Position.G7; +import static chess.domain.Position.H2; +import static chess.domain.Position.H7; +import static chess.domain.piece.Team.BLACK; +import static chess.domain.piece.Team.WHITE; + import chess.domain.Position; import chess.domain.board.ChessBoard; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; public final class Pawn extends AbstractPiece { - - private final Position initialPosition; + private static final Map> MOVE_FORWARD_TWO_ABLE_POSITIONS = Map.of( + WHITE, Set.of(A2, B2, C2, D2, E2, F2, G2, H2), + BLACK, Set.of(A7, B7, C7, D7, E7, F7, G7, H7) + ); public Pawn(Position position, Team team) { super(position, team); - this.initialPosition = position; } @Override @@ -37,7 +59,7 @@ private boolean isMoveForward(Position targetPosition) { private int forwardDirection() { Team team = getTeam(); - if (team.equals(Team.WHITE)) { + if (team.equals(WHITE)) { return 1; } return -1; @@ -53,7 +75,7 @@ private boolean isMoveForwardTwo(Position targetPosition, ChessBoard chessBoard) List route = nowPosition.route(targetPosition); boolean rightDirection = nowPosition.rowDistance(targetPosition) == 2 * forwardDirection(); boolean sameColumn = nowPosition.isSameColumn(targetPosition); - boolean firstMove = nowPosition.equals(initialPosition); + boolean firstMove = isFirstMove(); boolean allPieceOnRouteIsEmpty = isAllPieceOnRouteIsEmpty(chessBoard, route); return rightDirection && sameColumn && firstMove && allPieceOnRouteIsEmpty; } @@ -64,6 +86,12 @@ private boolean isAllPieceOnRouteIsEmpty(ChessBoard chessBoard, List r .allMatch(Optional::isEmpty); } + private boolean isFirstMove() { + Team team = getTeam(); + Set positions = MOVE_FORWARD_TWO_ABLE_POSITIONS.get(team); + return positions.contains(getPosition()); + } + private boolean isMoveDiagonal(Position targetPosition) { Position nowPosition = getPosition(); int rowDistance = nowPosition.rowDistance(targetPosition); @@ -84,4 +112,9 @@ private boolean isOtherTeam(Position targetPosition, ChessBoard chessBoard) { public PieceType getPieceType() { return PieceType.PAWN; } + + @Override + public double getPoint() { + return 1.0; + } } diff --git a/src/main/java/chess/domain/piece/Piece.java b/src/main/java/chess/domain/piece/Piece.java index a83e97dbb20..4182e5d4bc3 100644 --- a/src/main/java/chess/domain/piece/Piece.java +++ b/src/main/java/chess/domain/piece/Piece.java @@ -15,4 +15,8 @@ public interface Piece { int getColumn(); int getRow(); + + double getPoint(); + + boolean isTeamWith(Team team); } diff --git a/src/main/java/chess/domain/piece/PieceMoveResult.java b/src/main/java/chess/domain/piece/PieceMoveResult.java index 751faa59d26..a4303354949 100644 --- a/src/main/java/chess/domain/piece/PieceMoveResult.java +++ b/src/main/java/chess/domain/piece/PieceMoveResult.java @@ -1,9 +1,9 @@ package chess.domain.piece; public enum PieceMoveResult { - SUCCESS, FAILURE, CATCH; + SUCCESS, FAILURE, CATCH, WHITE_WIN, BLACK_WIN; - public boolean toBoolean() { - return !this.equals(FAILURE); + public boolean isEnd() { + return this.equals(BLACK_WIN) || this.equals(WHITE_WIN); } } diff --git a/src/main/java/chess/domain/piece/Queen.java b/src/main/java/chess/domain/piece/Queen.java index 33f72372707..100aaf3c219 100644 --- a/src/main/java/chess/domain/piece/Queen.java +++ b/src/main/java/chess/domain/piece/Queen.java @@ -28,4 +28,9 @@ public Optional tryMoveAssumeAlone(Position targetPosition, Che public PieceType getPieceType() { return QUEEN; } + + @Override + public double getPoint() { + return 9.0; + } } diff --git a/src/main/java/chess/domain/piece/Rook.java b/src/main/java/chess/domain/piece/Rook.java index 976842a9349..2b09226f107 100644 --- a/src/main/java/chess/domain/piece/Rook.java +++ b/src/main/java/chess/domain/piece/Rook.java @@ -22,4 +22,9 @@ public Optional tryMoveAssumeAlone(Position targetPosition, Che public PieceType getPieceType() { return PieceType.ROOK; } + + @Override + public double getPoint() { + return 5.0; + } } diff --git a/src/main/java/chess/view/InputView.java b/src/main/java/chess/view/InputView.java index ffe811b905c..e36e9d53a97 100644 --- a/src/main/java/chess/view/InputView.java +++ b/src/main/java/chess/view/InputView.java @@ -1,9 +1,10 @@ package chess.view; -import chess.domain.game.Command; -import chess.domain.game.EndCommand; -import chess.domain.game.MoveCommand; -import chess.domain.game.StartCommand; +import chess.domain.game.command.Command; +import chess.domain.game.command.EndCommand; +import chess.domain.game.command.MoveCommand; +import chess.domain.game.command.StartCommand; +import chess.domain.game.command.StatusCommand; import java.util.Scanner; public class InputView { @@ -32,18 +33,25 @@ private static boolean isEndCommand(String input) { return input.equals("end"); } - public static Command readEndOrMove() { + public static Command readEndOrMoveOrStatus() { Scanner scanner = new Scanner(System.in); String input = scanner.nextLine(); - while (!input.matches("move [a-h][1-8] [a-h][1-8]") && !isEndCommand(input)) { + while (!input.matches("move [a-h][1-8] [a-h][1-8]") && !isEndCommand(input) && !isStatusCommand(input)) { System.out.println("다시 입력해 주세요"); input = scanner.nextLine(); } if (isEndCommand(input)) { return EndCommand.END_COMMAND; } + if (isStatusCommand(input)) { + return StatusCommand.STATUS_COMMAND; + } String options = input.substring(OPTION_BEGIN_INDEX); String[] splitOptions = options.split(SEPARATOR); return new MoveCommand(splitOptions); } + + private static boolean isStatusCommand(String input) { + return input.equals("status"); + } } diff --git a/src/main/java/chess/view/OutputView.java b/src/main/java/chess/view/OutputView.java index 84d561d2264..b7fb07c4c0a 100644 --- a/src/main/java/chess/view/OutputView.java +++ b/src/main/java/chess/view/OutputView.java @@ -1,5 +1,6 @@ package chess.view; +import chess.domain.piece.PieceMoveResult; import chess.domain.piece.PieceType; import chess.domain.piece.Team; import chess.dto.PieceDTO; @@ -11,7 +12,8 @@ public static void printGuide() { System.out.printf("> 체스 게임을 시작합니다.%n" + "> 게임 시작 : start%n" + "> 게임 종료 : end%n" - + "> 게임 이동 : move source위치 target위치 - 예. move b2 b3%n"); + + "> 게임 이동 : move source위치 target위치 - 예. move b2 b3%n" + + "> 현재 점수 확인 : status%n"); } public static void printChessBoard(List pieceDTOS) { @@ -44,6 +46,32 @@ public static void printReInputGuide() { System.out.println("다시 입력해 주세요"); } + public static void printStatus(Team team, double point) { + System.out.printf("%s: %f%n", team.name(), point); + } + + public static void printWinner(PieceMoveResult pieceMoveResult) { + if (pieceMoveResult.equals(PieceMoveResult.BLACK_WIN)) { + System.out.println("BLACK 승리"); + return; + } + if (pieceMoveResult.equals(PieceMoveResult.WHITE_WIN)) { + System.out.println("WHITE 승리"); + } + } + + public static void currentWinner(double whitePoint, double blackPoint) { + if (blackPoint > whitePoint) { + System.out.println("BLACK이 이기는 중"); + return; + } + if (whitePoint > blackPoint) { + System.out.println("WHITE가 이기는 중"); + return; + } + System.out.println("비기는 중"); + } + enum PieceAsset { BLACK_KING('K'), BLACK_QUEEN('Q'), diff --git a/src/test/java/chess/dao/ChessPersistenceTest.java b/src/test/java/chess/dao/ChessPersistenceTest.java new file mode 100644 index 00000000000..9bcfd2176bc --- /dev/null +++ b/src/test/java/chess/dao/ChessPersistenceTest.java @@ -0,0 +1,108 @@ +package chess.dao; + +import static chess.domain.Position.A1; +import static chess.domain.piece.Team.BLACK; +import static chess.domain.piece.Team.WHITE; +import static org.junit.jupiter.api.Assertions.assertAll; + +import chess.domain.game.ChessGame; +import chess.domain.game.command.MoveCommand; +import chess.domain.piece.Pawn; +import chess.domain.piece.Piece; +import chess.domain.piece.Team; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ChessPersistenceTest { + private final FakeDBConnectionCache dbConnectionCache = new FakeDBConnectionCache(); + + @Test + @DisplayName("저장된 게임이 있는 경우, 잘 불러오는지 확인") + void loadChessGame() { + FakePiecesOnChessBoardDAO piecesOnChessBoardDAO = new FakePiecesOnChessBoardDAO(); + FakeTurnDAO turnDAO = new FakeTurnDAO(); + ChessPersistence persistenceService = new ChessPersistence(piecesOnChessBoardDAO, turnDAO, dbConnectionCache); + List pieces = List.of(new Pawn(A1, WHITE)); + Team currentTeam = BLACK; + ChessGame chessGame = new ChessGame(pieces, currentTeam); + persistenceService.saveChessGame(chessGame); + + ChessGame loadedChessGame = persistenceService.loadChessGame(); + List loadedPieces = loadedChessGame.getPiecesOnBoard(); + Team loadedCurrentTeam = loadedChessGame.currentTeam(); + + assertAll( + () -> Assertions.assertThat(loadedPieces).containsExactlyInAnyOrderElementsOf(pieces), + () -> Assertions.assertThat(loadedCurrentTeam).isEqualTo(currentTeam) + ); + } + + @Test + @DisplayName("저장된 게임이 있는 경우 저장된 게임이 있다고 하는지 검증") + void isSaveDataExistWhenExist() { + FakePiecesOnChessBoardDAO piecesOnChessBoardDAO = new FakePiecesOnChessBoardDAO(); + FakeTurnDAO turnDAO = new FakeTurnDAO(); + ChessPersistence persistenceService = new ChessPersistence(piecesOnChessBoardDAO, turnDAO, dbConnectionCache); + ChessGame chessGame = new ChessGame(); + persistenceService.saveChessGame(chessGame); + + boolean saveDataExist = persistenceService.isSaveDataExist(); + + Assertions.assertThat(saveDataExist) + .isTrue(); + } + + @Test + @DisplayName("저장된 게임이 없는 경우 저장된 게임이 없다고 하는지 검증") + void isSaveDataExistWhenNotExist() { + FakePiecesOnChessBoardDAO piecesOnChessBoardDAO = new FakePiecesOnChessBoardDAO(); + FakeTurnDAO turnDAO = new FakeTurnDAO(); + ChessPersistence persistenceService = new ChessPersistence(piecesOnChessBoardDAO, turnDAO, dbConnectionCache); + + boolean saveDataExist = persistenceService.isSaveDataExist(); + + Assertions.assertThat(saveDataExist) + .isFalse(); + } + + @Test + @DisplayName("게임이 잘 저장이 되는지 검증") + void saveChessGame() { + FakePiecesOnChessBoardDAO piecesOnChessBoardDAO = new FakePiecesOnChessBoardDAO(); + FakeTurnDAO turnDAO = new FakeTurnDAO(); + ChessPersistence persistenceService = new ChessPersistence(piecesOnChessBoardDAO, turnDAO, dbConnectionCache); + boolean saveChessGameSuccess = persistenceService.saveChessGame(new ChessGame()); + Assertions.assertThat(saveChessGameSuccess) + .isTrue(); + } + + @Test + @DisplayName("이미 저장된 게임이 있으면 게임이 저장되지 않는지 검증") + void saveChessGameFail() { + FakePiecesOnChessBoardDAO piecesOnChessBoardDAO = new FakePiecesOnChessBoardDAO(); + FakeTurnDAO turnDAO = new FakeTurnDAO(); + ChessPersistence persistenceService = new ChessPersistence(piecesOnChessBoardDAO, turnDAO, dbConnectionCache); + persistenceService.saveChessGame(new ChessGame()); + + boolean saveChessGameSuccess = persistenceService.saveChessGame(new ChessGame()); + Assertions.assertThat(saveChessGameSuccess) + .isFalse(); + } + + @Test + @DisplayName("게임의 진행 상황을 저장할 수 있는지 검증") + void updateChessGame() { + FakePiecesOnChessBoardDAO piecesOnChessBoardDAO = new FakePiecesOnChessBoardDAO(); + FakeTurnDAO turnDAO = new FakeTurnDAO(); + ChessPersistence persistenceService = new ChessPersistence(piecesOnChessBoardDAO, turnDAO, dbConnectionCache); + ChessGame chessGame = new ChessGame(); + persistenceService.saveChessGame(chessGame); + MoveCommand moveCommand = new MoveCommand("a2", "a4"); + chessGame.move(moveCommand); + boolean updateChessGameSuccess = persistenceService.updateChessGame(chessGame, moveCommand); + Assertions.assertThat(updateChessGameSuccess) + .isTrue(); + } +} diff --git a/src/test/java/chess/dao/FakeDBConnectionCache.java b/src/test/java/chess/dao/FakeDBConnectionCache.java new file mode 100644 index 00000000000..ff6189d6fd8 --- /dev/null +++ b/src/test/java/chess/dao/FakeDBConnectionCache.java @@ -0,0 +1,305 @@ +package chess.dao; + +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; + +public class FakeDBConnectionCache implements DBConnectionCache { + @Override + public Connection getConnection() { + return new FakeConnection(); + } + + private static class FakeConnection implements Connection { + + @Override + public Statement createStatement() throws SQLException { + return null; + } + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + return null; + } + + @Override + public CallableStatement prepareCall(String sql) throws SQLException { + return null; + } + + @Override + public String nativeSQL(String sql) throws SQLException { + return null; + } + + @Override + public T unwrap(Class iface) throws SQLException { + return null; + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + + } + + + + @Override + public boolean getAutoCommit() throws SQLException { + return false; + } + + @Override + public void commit() throws SQLException { + + } + + @Override + public void rollback() throws SQLException { + + } + + @Override + public void close() throws SQLException { + + } + + @Override + public boolean isClosed() throws SQLException { + return false; + } + + @Override + public DatabaseMetaData getMetaData() throws SQLException { + return null; + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + + } + + @Override + public boolean isReadOnly() throws SQLException { + return false; + } + + @Override + public void setCatalog(String catalog) throws SQLException { + + } + + @Override + public String getCatalog() throws SQLException { + return null; + } + + @Override + public void setTransactionIsolation(int level) throws SQLException { + + } + + @Override + public int getTransactionIsolation() throws SQLException { + return 0; + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + @Override + public void clearWarnings() throws SQLException { + + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + return null; + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException { + return null; + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException { + return null; + } + + @Override + public Map> getTypeMap() throws SQLException { + return null; + } + + @Override + public void setTypeMap(Map> map) throws SQLException { + + } + + @Override + public void setHoldability(int holdability) throws SQLException { + + } + + @Override + public int getHoldability() throws SQLException { + return 0; + } + + @Override + public Savepoint setSavepoint() throws SQLException { + return null; + } + + @Override + public Savepoint setSavepoint(String name) throws SQLException { + return null; + } + + @Override + public void rollback(Savepoint savepoint) throws SQLException { + + } + + @Override + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + return null; + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + return null; + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + return null; + } + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + return null; + } + + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + return null; + } + + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + return null; + } + + @Override + public Clob createClob() throws SQLException { + return null; + } + + @Override + public Blob createBlob() throws SQLException { + return null; + } + + @Override + public NClob createNClob() throws SQLException { + return null; + } + + @Override + public SQLXML createSQLXML() throws SQLException { + return null; + } + + @Override + public boolean isValid(int timeout) throws SQLException { + return false; + } + + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException { + + } + + @Override + public void setClientInfo(Properties properties) throws SQLClientInfoException { + + } + + @Override + public String getClientInfo(String name) throws SQLException { + return null; + } + + @Override + public Properties getClientInfo() throws SQLException { + return null; + } + + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + return null; + } + + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + return null; + } + + @Override + public void setSchema(String schema) throws SQLException { + + } + + @Override + public String getSchema() throws SQLException { + return null; + } + + @Override + public void abort(Executor executor) throws SQLException { + + } + + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + + } + + @Override + public int getNetworkTimeout() throws SQLException { + return 0; + } + } +} diff --git a/src/test/java/chess/dao/FakePiecesOnChessBoardDAO.java b/src/test/java/chess/dao/FakePiecesOnChessBoardDAO.java new file mode 100644 index 00000000000..14f3adfda73 --- /dev/null +++ b/src/test/java/chess/dao/FakePiecesOnChessBoardDAO.java @@ -0,0 +1,72 @@ +package chess.dao; + +import chess.domain.Position; +import chess.domain.piece.Bishop; +import chess.domain.piece.King; +import chess.domain.piece.Knight; +import chess.domain.piece.Pawn; +import chess.domain.piece.Piece; +import chess.domain.piece.Queen; +import chess.domain.piece.Rook; +import chess.domain.piece.Team; +import java.sql.Connection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class FakePiecesOnChessBoardDAO implements PiecesOnChessBoardDAO { + private final Set pieces = new HashSet<>(); + + @Override + public boolean save(Piece piece, Connection connection) { + return pieces.add(copy(piece)); + } + + private Piece copy(Piece piece) { + int row = piece.getRow(); + int column = piece.getColumn(); + Position position = Position.getInstance(row, column); + Team team = piece.getTeam(); + return switch (piece.getPieceType()) { + case ROOK -> new Rook(position, team); + case BISHOP -> new Bishop(position, team); + case KING -> new King(position, team); + case PAWN -> new Pawn(position, team); + case KNIGHT -> new Knight(position, team); + case QUEEN -> new Queen(position, team); + }; + } + + @Override + public boolean saveAll(List pieces, Connection connection) { + for (Piece piece : pieces) { + if (this.pieces.contains(piece)) { + return false; + } + } + for (Piece piece : pieces) { + save(piece, connection); + } + return true; + } + + @Override + public List selectAll(Connection connection) { + return pieces.stream().toList(); + } + + @Override + public boolean isNotEmpty(Connection connection) { + return !selectAll(connection).isEmpty(); + } + + @Override + public boolean delete(Position targetPosition, Connection connection) { + return pieces.removeIf(piece -> piece.isOn(targetPosition)); + } + + @Override + public void deleteAll(Connection connection) { + pieces.clear(); + } +} diff --git a/src/test/java/chess/dao/FakeTurnDAO.java b/src/test/java/chess/dao/FakeTurnDAO.java new file mode 100644 index 00000000000..ae550f2a641 --- /dev/null +++ b/src/test/java/chess/dao/FakeTurnDAO.java @@ -0,0 +1,42 @@ +package chess.dao; + +import chess.domain.piece.Team; +import java.sql.Connection; +import java.util.Optional; + +public class FakeTurnDAO implements TurnDAO { + private Team team; + + @Override + public Optional select(Connection connection) { + return Optional.ofNullable(team); + } + + @Override + public boolean isNotEmpty(Connection connection) { + return select(connection).isPresent(); + } + + @Override + public boolean save(Team team, Connection connection) { + if (this.team == null) { + this.team = team; + return true; + } + return false; + } + + @Override + public boolean update(Team targetTeam, Team updatedTeam, Connection connection) { + if (targetTeam.equals(team)) { + team = updatedTeam; + return true; + } + return false; + } + + @Override + public void delete(Connection connection) { + team = null; + } +} diff --git a/src/test/java/chess/dao/PiecesOnChessBoardDAOForMysqlTest.java b/src/test/java/chess/dao/PiecesOnChessBoardDAOForMysqlTest.java new file mode 100644 index 00000000000..4341627daab --- /dev/null +++ b/src/test/java/chess/dao/PiecesOnChessBoardDAOForMysqlTest.java @@ -0,0 +1,118 @@ +package chess.dao; + +import static chess.domain.Position.A2; +import static chess.domain.Position.B2; +import static chess.domain.piece.Team.WHITE; +import static org.junit.jupiter.api.Assertions.assertAll; + +import chess.dao.exception.DBException; +import chess.domain.piece.Pawn; +import chess.domain.piece.Piece; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PiecesOnChessBoardDAOForMysqlTest { + private static final String DB_URL = "jdbc:mysql://localhost:13306/chess2?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"; + private static final String DB_USER_NAME = "user"; + private static final String DB_USER_PASSWORD = "password"; + private static final DBConnectionCache DB_CONNECTION_CACHE = new MysqlDBConnectionCache(DB_URL, DB_USER_NAME, + DB_USER_PASSWORD); + private static final PiecesOnChessBoardDAOForMysql DAO_FOR_MYSQL = new PiecesOnChessBoardDAOForMysql(); + + @BeforeEach + void setUp() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + try { + connection.setAutoCommit(false); + } catch (SQLException e) { + throw new DBException("DB 접속 오류", e); + } + } + + @AfterEach + void tearDown() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + try { + connection.rollback(); + connection.setAutoCommit(true); + } catch (SQLException e) { + throw new DBException("DB 접속 오류", e); + } + } + + @Test + @DisplayName("체스 보드 위의 기물 정보가 잘 저장되는지 검증") + void save() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + boolean saveResult = DAO_FOR_MYSQL.save(new Pawn(A2, WHITE), connection); + Assertions.assertThat(saveResult) + .isTrue(); + } + + @Test + @DisplayName("체스 보드 위의 여러 기물 정보가 잘 저장되는지 검증") + void saveAll() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + boolean saveResult = DAO_FOR_MYSQL.saveAll(List.of(new Pawn(A2, WHITE), new Pawn(B2, WHITE)), connection); + Assertions.assertThat(saveResult) + .isTrue(); + } + + @Test + @DisplayName("저장된 체스 보드 위의 여러 기물 정보를 잘 불러오는지 검증") + void selectAll() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + List pieces = List.of(new Pawn(A2, WHITE), new Pawn(B2, WHITE)); + DAO_FOR_MYSQL.saveAll(pieces, connection); + + List selected = DAO_FOR_MYSQL.selectAll(connection); + + Assertions.assertThat(selected) + .containsExactlyInAnyOrderElementsOf(pieces); + } + + @Test + @DisplayName("체스 보드 위에 말이 있는지 잘 판단하는지 검증") + void isNotEmpty() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + boolean notEmptyWhenBoardIsEmpty = DAO_FOR_MYSQL.isNotEmpty(connection); + DAO_FOR_MYSQL.save(new Pawn(A2, WHITE), connection); + boolean notEmptyWhenBoardIsNotEmpty = DAO_FOR_MYSQL.isNotEmpty(connection); + assertAll( + () -> Assertions.assertThat(notEmptyWhenBoardIsEmpty).isFalse(), + () -> Assertions.assertThat(notEmptyWhenBoardIsNotEmpty).isTrue() + ); + } + + @Test + @DisplayName("체스 보드 위의 말을 잘 지우는지 검증") + void delete() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + DAO_FOR_MYSQL.save(new Pawn(A2, WHITE), connection); + + boolean deleteResult = DAO_FOR_MYSQL.delete(A2, connection); + + Assertions.assertThat(deleteResult) + .isTrue(); + } + + @Test + @DisplayName("체스 보드 위의 모든 말을 잘 지우는지 검증") + void deleteAll() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + List pieces = List.of(new Pawn(A2, WHITE), new Pawn(B2, WHITE)); + DAO_FOR_MYSQL.saveAll(pieces, connection); + + DAO_FOR_MYSQL.deleteAll(connection); + + List selected = DAO_FOR_MYSQL.selectAll(connection); + Assertions.assertThat(selected) + .isEmpty(); + } +} diff --git a/src/test/java/chess/dao/TurnDAOForMysqlTest.java b/src/test/java/chess/dao/TurnDAOForMysqlTest.java new file mode 100644 index 00000000000..2f1e34acbbd --- /dev/null +++ b/src/test/java/chess/dao/TurnDAOForMysqlTest.java @@ -0,0 +1,103 @@ +package chess.dao; + +import static chess.domain.piece.Team.BLACK; +import static chess.domain.piece.Team.WHITE; +import static org.junit.jupiter.api.Assertions.assertAll; + +import chess.dao.exception.DBException; +import chess.domain.piece.Team; +import java.sql.Connection; +import java.sql.SQLException; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class TurnDAOForMysqlTest { + private static final String DB_URL = "jdbc:mysql://localhost:13306/chess2?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"; + private static final String DB_USER_NAME = "user"; + private static final String DB_USER_PASSWORD = "password"; + private static final DBConnectionCache DB_CONNECTION_CACHE = new MysqlDBConnectionCache(DB_URL, DB_USER_NAME, + DB_USER_PASSWORD); + private static final TurnDAOForMysql TURN_DAO_FOR_MYSQL = new TurnDAOForMysql(); + + @BeforeEach + void setUp() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + try { + connection.setAutoCommit(false); + } catch (SQLException e) { + throw new DBException("DB 접속 오류", e); + } + } + + @AfterEach + void tearDown() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + try { + connection.rollback(); + connection.setAutoCommit(true); + } catch (SQLException e) { + throw new DBException("DB 접속 오류", e); + } + } + + @Test + @DisplayName("현재 기물을 움직일 차례를 잘 불러오는지 검증") + void select() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + TURN_DAO_FOR_MYSQL.save(WHITE, connection); + + Team team = TURN_DAO_FOR_MYSQL.select(connection).get(); + Assertions.assertThat(team) + .isEqualTo(WHITE); + } + + @Test + @DisplayName("현재 기물을 움직일 차례가 저징되어있는지 잘 판단하는지 검증") + void isNotEmpty() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + boolean notEmptyWhenTurnEmpty = TURN_DAO_FOR_MYSQL.isNotEmpty(connection); + TURN_DAO_FOR_MYSQL.save(WHITE, connection); + boolean notEmptyWhenTurnNotEmpty = TURN_DAO_FOR_MYSQL.isNotEmpty(connection); + + assertAll( + () -> Assertions.assertThat(notEmptyWhenTurnEmpty).isFalse(), + () -> Assertions.assertThat(notEmptyWhenTurnNotEmpty).isTrue() + ); + } + + @Test + @DisplayName("현재 기물을 움직일 차례를 잘 저장하는지 검증") + void save() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + boolean saveResult = TURN_DAO_FOR_MYSQL.save(WHITE, connection); + + Assertions.assertThat(saveResult) + .isTrue(); + } + + @Test + @DisplayName("현재 기물을 움직일 차례를 잘 수정하는지 검증") + void update() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + TURN_DAO_FOR_MYSQL.save(WHITE, connection); + + boolean updateResult = TURN_DAO_FOR_MYSQL.update(WHITE, BLACK, connection); + + Assertions.assertThat(updateResult) + .isTrue(); + } + + @Test + @DisplayName("현재 기물을 움직일 차례를 잘 지우는지 검증") + void delete() { + Connection connection = DB_CONNECTION_CACHE.getConnection(); + TURN_DAO_FOR_MYSQL.delete(connection); + + boolean notEmpty = TURN_DAO_FOR_MYSQL.isNotEmpty(connection); + Assertions.assertThat(notEmpty) + .isFalse(); + } +} diff --git a/src/test/java/chess/domain/board/ChessBoardTest.java b/src/test/java/chess/domain/board/ChessBoardTest.java index 43776ae928d..36f74288a74 100644 --- a/src/test/java/chess/domain/board/ChessBoardTest.java +++ b/src/test/java/chess/domain/board/ChessBoardTest.java @@ -4,6 +4,7 @@ import static chess.domain.Position.A3; import static chess.domain.Position.A4; import static chess.domain.Position.A6; +import static chess.domain.Position.A8; import static chess.domain.Position.B1; import static chess.domain.Position.B2; import static chess.domain.Position.B6; @@ -22,6 +23,9 @@ import static chess.domain.Position.E3; import static chess.domain.Position.F2; import static chess.domain.Position.H1; +import static chess.domain.Position.H2; +import static chess.domain.piece.PieceMoveResult.FAILURE; +import static chess.domain.piece.PieceMoveResult.SUCCESS; import static chess.domain.piece.Team.BLACK; import static chess.domain.piece.Team.WHITE; @@ -31,6 +35,7 @@ import chess.domain.piece.Knight; import chess.domain.piece.Pawn; import chess.domain.piece.Piece; +import chess.domain.piece.PieceMoveResult; import chess.domain.piece.Queen; import chess.domain.piece.Rook; import chess.domain.piece.Team; @@ -55,7 +60,7 @@ public static Stream moveSuccessParameters() { public static Stream moveFailureCauseInvalidMoveParameters() { return Stream.of( - Arguments.of(D2, D5), Arguments.of(D2, C3), Arguments.of(A1, H1), Arguments.of(A1, B6), + Arguments.of(D2, D5), Arguments.of(D2, C3), Arguments.of(A1, H2), Arguments.of(A1, B6), Arguments.of(B1, B2), Arguments.of(C1, C6), Arguments.of(D1, D3), Arguments.of(D1, C3), Arguments.of(E1, E3) ); @@ -72,67 +77,74 @@ public static Stream whichTeamParameters() { @MethodSource("moveSuccessParameters") @DisplayName("정상적인 경우에 이동이 성공하는지 검증") void moveSuccess(Position from, Position to) { - ChessBoard chessBoard = new ChessBoard(new Pawn(D2, WHITE), new Rook(A1, WHITE), new King(E1, WHITE), + ChessBoard chessBoard = new ChessBoard(new King(A8, WHITE), new King(H1, BLACK), new Pawn(D2, WHITE), + new Rook(A1, WHITE), new King(E1, WHITE), new Knight(B1, WHITE), new Bishop(C1, WHITE), new Queen(D1, WHITE)); - boolean moveSuccess = chessBoard.move(from, to); - Assertions.assertThat(moveSuccess).isTrue(); + PieceMoveResult moveResult = chessBoard.move(from, to); + Assertions.assertThat(moveResult).isEqualTo(SUCCESS); } @ParameterizedTest @MethodSource("moveFailureCauseInvalidMoveParameters") @DisplayName("해당 위치가 이동이 불가능한 위치라서 이동이 실패하는지 검증") void moveFailureCauseInvalidMove(Position from, Position to) { - ChessBoard chessBoard = new ChessBoard(new Pawn(D2, WHITE), new Rook(A1, WHITE), new King(E1, WHITE), + ChessBoard chessBoard = new ChessBoard(new King(A8, WHITE), new King(H1, BLACK), new Pawn(D2, WHITE), + new Rook(A1, WHITE), new King(E1, WHITE), new Knight(B1, WHITE), new Bishop(C1, WHITE), new Queen(D1, WHITE)); - boolean moveSuccess = chessBoard.move(from, to); - Assertions.assertThat(moveSuccess).isFalse(); + PieceMoveResult moveResult = chessBoard.move(from, to); + Assertions.assertThat(moveResult).isEqualTo(FAILURE); } @Test @DisplayName("해당 위치에 말이 없어서 이동이 실패하는지 검증") void moveFailureCausePieceNotFound() { - ChessBoard chessBoard = new ChessBoard(new Pawn(D2, WHITE)); - boolean moveSuccess = chessBoard.move(D3, D4); - Assertions.assertThat(moveSuccess).isFalse(); + ChessBoard chessBoard = new ChessBoard(new King(A8, WHITE), new King(H1, BLACK), new Pawn(D2, WHITE)); + PieceMoveResult moveResult = chessBoard.move(D3, D4); + Assertions.assertThat(moveResult).isEqualTo(FAILURE); } @Test @DisplayName("팀의 이동 차례가 아니라서 이동이 실패하는지 검증") void moveFailureCauseInvalidTurn() { - ChessBoard chessBoard = new ChessBoard(new Pawn(D2, WHITE), new Pawn(D7, BLACK)); + ChessBoard chessBoard = new ChessBoard(new King(A8, WHITE), new King(H1, BLACK), new Pawn(D2, WHITE), + new Pawn(D7, BLACK)); chessBoard.move(D2, D4); - boolean moveSuccess = chessBoard.move(D4, D5); - Assertions.assertThat(moveSuccess).isFalse(); + PieceMoveResult moveResult = chessBoard.move(D4, D5); + Assertions.assertThat(moveResult).isEqualTo(FAILURE); } @Test @DisplayName("상대 말을 잡으면 그 말이 보드에서 사라지는지 검증") void catchSuccess() { Pawn survivor = new Pawn(D2, WHITE); - ChessBoard chessBoard = new ChessBoard(survivor, new Pawn(C3, BLACK)); + King whiteKing = new King(A8, WHITE); + King blackKing = new King(H1, BLACK); + ChessBoard chessBoard = new ChessBoard(whiteKing, blackKing, survivor, new Pawn(C3, BLACK)); chessBoard.move(D2, C3); List piecesOnBoard = chessBoard.getPiecesOnBoard(); Assertions.assertThat(piecesOnBoard) - .containsExactly(survivor); + .containsExactly(whiteKing, blackKing, survivor); } @Test @DisplayName("잘못된 이동을 시도했을 때 차례가 넘어가지 않는지 검증") void validateTurnNotChangeWhenInvalidMove() { - ChessBoard chessBoard = new ChessBoard(new Pawn(D2, WHITE), new Pawn(C3, BLACK)); + ChessBoard chessBoard = new ChessBoard(new King(A8, WHITE), new King(H1, BLACK), new Pawn(D2, WHITE), + new Pawn(C3, BLACK)); chessBoard.move(D2, D8); - boolean blackMoveSuccess = chessBoard.move(C3, C2); - Assertions.assertThat(blackMoveSuccess).isFalse(); + PieceMoveResult moveResult = chessBoard.move(C3, C2); + Assertions.assertThat(moveResult).isEqualTo(FAILURE); } @Test @DisplayName("이동이 성공했을 때 차례가 넘어가는지 검증") void validateTurnChangeWhenValidMove() { - ChessBoard chessBoard = new ChessBoard(new Pawn(D2, WHITE), new Pawn(C3, BLACK)); + ChessBoard chessBoard = new ChessBoard(new King(A8, WHITE), new King(H1, BLACK), new Pawn(D2, WHITE), + new Pawn(C3, BLACK)); chessBoard.move(D2, D3); - boolean blackMoveSuccess = chessBoard.move(C3, C2); - Assertions.assertThat(blackMoveSuccess).isTrue(); + PieceMoveResult moveResult = chessBoard.move(C3, C2); + Assertions.assertThat(moveResult).isEqualTo(SUCCESS); } @ParameterizedTest @@ -149,7 +161,7 @@ void whichTeam(Team team) { @Test @DisplayName("해당 위치에 말이 없는 경우 팀을 판단할 수 없는지 검증") void whichTeamWhenEmpty() { - ChessBoard chessBoard = new ChessBoard(List.of()); + ChessBoard chessBoard = new ChessBoard(new King(A8, WHITE), new King(H1, BLACK)); Optional actual = chessBoard.whichTeam(D2); Assertions.assertThat(actual) .isEmpty(); diff --git a/src/test/java/chess/domain/board/PointCalculatorTest.java b/src/test/java/chess/domain/board/PointCalculatorTest.java new file mode 100644 index 00000000000..ccf10763d90 --- /dev/null +++ b/src/test/java/chess/domain/board/PointCalculatorTest.java @@ -0,0 +1,58 @@ +package chess.domain.board; + +import static chess.domain.Position.A2; +import static chess.domain.Position.A3; +import static chess.domain.Position.A4; +import static chess.domain.Position.A5; +import static chess.domain.Position.A6; +import static chess.domain.Position.A8; +import static chess.domain.Position.B2; +import static chess.domain.Position.H1; +import static chess.domain.piece.Team.BLACK; +import static chess.domain.piece.Team.WHITE; +import static org.junit.jupiter.api.Assertions.assertAll; + +import chess.domain.piece.King; +import chess.domain.piece.Pawn; +import chess.domain.piece.Piece; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PointCalculatorTest { + @Test + @DisplayName("각 팀의 점수가 잘 계산되는지 검증") + void calculatePoint() { + ChessBoard chessBoard = new ChessBoard(); + List piecesOnBoard = chessBoard.getPiecesOnBoard(); + + double whiteTeamPoint = PointCalculator.calculatePoint(WHITE, piecesOnBoard); + double blackTeamPoint = PointCalculator.calculatePoint(WHITE, piecesOnBoard); + Offset offset = Offset.offset(0.01); + + assertAll( + () -> Assertions.assertThat(whiteTeamPoint).isCloseTo(38.0, offset), + () -> Assertions.assertThat(blackTeamPoint).isCloseTo(38.0, offset) + ); + } + + @Test + @DisplayName("같은 열의 폰이 포함된 각 팀의 점수가 잘 계산되는지 검증") + void calculatePointWithPawn() { + ChessBoard chessBoard = new ChessBoard(new King(A8, WHITE), new King(H1, BLACK), new Pawn(A2, BLACK), + new Pawn(B2, BLACK), new Pawn(A3, BLACK), + new Pawn(A4, WHITE), new Pawn(A5, WHITE), new Pawn(A6, WHITE)); + List piecesOnBoard = chessBoard.getPiecesOnBoard(); + + double whiteTeamPoint = PointCalculator.calculatePoint(WHITE, piecesOnBoard); + double blackTeamPoint = PointCalculator.calculatePoint(BLACK, piecesOnBoard); + Offset offset = Offset.offset(0.01); + + assertAll( + () -> Assertions.assertThat(blackTeamPoint).isCloseTo(2.0, offset), + () -> Assertions.assertThat(whiteTeamPoint).isCloseTo(1.5, offset) + ); + } +} diff --git a/src/test/java/chess/domain/game/EndCommandTest.java b/src/test/java/chess/domain/game/EndCommandTest.java index 5c5737e0353..5fba07b874a 100644 --- a/src/test/java/chess/domain/game/EndCommandTest.java +++ b/src/test/java/chess/domain/game/EndCommandTest.java @@ -1,6 +1,6 @@ package chess.domain.game; -import static chess.domain.game.EndCommand.END_COMMAND; +import static chess.domain.game.command.EndCommand.END_COMMAND; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/chess/domain/game/MoveCommandTest.java b/src/test/java/chess/domain/game/MoveCommandTest.java index d910ff16d2d..39db70c6cc6 100644 --- a/src/test/java/chess/domain/game/MoveCommandTest.java +++ b/src/test/java/chess/domain/game/MoveCommandTest.java @@ -4,6 +4,7 @@ import static chess.domain.Position.A2; import chess.domain.Position; +import chess.domain.game.command.MoveCommand; import java.util.List; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/chess/domain/game/StartCommandTest.java b/src/test/java/chess/domain/game/StartCommandTest.java index ee581d0f161..455eb8b1774 100644 --- a/src/test/java/chess/domain/game/StartCommandTest.java +++ b/src/test/java/chess/domain/game/StartCommandTest.java @@ -1,6 +1,6 @@ package chess.domain.game; -import static chess.domain.game.StartCommand.START_COMMAND; +import static chess.domain.game.command.StartCommand.START_COMMAND; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName;