diff --git a/.gitignore b/.gitignore index e2b01389a6b..839f4174067 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,4 @@ out/ .vscode/ ### Docker ### -/docker/ +/docker/db 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 1c07272734b..a16dd7c1367 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,3 +1,39 @@ +## 실행 방법 +1. MySQL 컨테이너 실행 + ```zsh + cd ./docker && docker-compose -p chess up -d + ``` + +2. 데이터베이스, 테이블 생성 + ```mysql + CREATE DATABASE chess DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; + use chess; + + CREATE TABLE `game` ( + `gameId` int NOT NULL AUTO_INCREMENT, + `turn` varchar(10) DEFAULT NULL, + PRIMARY KEY (`gameId`) + ); + + CREATE TABLE `piece` ( + `file` int NOT NULL, + `rank` int NOT NULL, + `pieceColor` varchar(50) NOT NULL, + `pieceType` varchar(50) NOT NULL, + `gameId` int NOT NULL, + PRIMARY KEY (`file`,`rank`,`gameId`), + KEY `gameId` (`gameId`), + CONSTRAINT `gameId` FOREIGN KEY (`gameId`) REFERENCES `game` (`gameId`) + ); + ``` + +3. `Application` 실행 +4. 프로그램 실행 후 `start` 를 입력하여 새로운 게임을 시작하거나 `load` 를 입력하여 이전에 저장된 게임을 불러온다. +5. 게임이 시작되면 `move {source} {target}` 을 입력하여 체스 말을 움직인다. +6. 게임 진행 중 `save` 명령으로 현재 상태를 저장할 수 있다. +7. 한 팀의 킹이 잡히면 게임은 종료되고, 이때 `status` 명령으로 각 팀의 점수와 승자를 확인할 수 있다. +8. 어디서든 `end` 명령으로 게임을 종료할 수 있다. + ## 기능 흐름 - [x] 사용자로부터 게임 시작 여부 입력 @@ -21,6 +57,7 @@ ### Piece - [x] 타입이 다른 기물들과 자신을 구별할 수 있는`PieceType`을 가진다. + - [x] `PieceType` 에는 각 기물의 평가치(점수)가 정의되어 있다. - [x] 이동 가능한 위치인지 여부를 확인한다. - [x] 모든 말은 체스보드 안에서만 움직일 수 있다. - [x] 목적지까지 가는 경로에 다른 기물이 존재하면 이동할 수 없다. @@ -44,9 +81,25 @@ - [x] 체스의 규칙에 맞게 각 기물들을 시작 위치로 배치한다. - [x] 기물의 도착지에 현재 이동하는 기물과 같은 색의 기물이 존재하면 이동할 수 없다. - [x] 도착지에 다른 색의 기물이 존재하는 경우, 해당 기물을 제거하고 배치한다. +- [x] 현재 보드에 남아 있는 기물들을 기반으로 각 팀의 점수를 계산할 수 있다. + - 각 팀의 점수는 남아 있는 팀의 기물 평가치의 합으로 계산한다. + - 같은 세로줄에 같은 색의 폰이 있는 경우, 각 폰들은 원래 평가치의 절반으로 계산된다. + +### ChessGame +- [x] `흰 팀 차례`, `검은 팀 차례`, `게임 종료` 의 상태를 가진다. +- [x] 게임이 진행중인 상태에서는 전달받은 출발지와 목적지에 따라 말을 이동시킨다. +- [x] 한 팀의 킹이 잡히면 게임을 종료한다. +- [x] 게임이 종료된 경우 승자를 판단할 수 있다. +- [x] 상태와 무관하게 각 팀별 현재 점수를 계산할 수 있다. +- [x] 현재 게임 상태를 DB에 저장할 수 있다. +- [x] DB에 저장된 게임 상태를 다시 불러올 수 있다. ### InputView - [x] `start` 명령과 `end`, `move` 명령을 입력받는다. +- [x] 게임 종료 후 `status` 명령을 입력받을 수 있다. +- [x] 현재 게임 상태를 저장하기 위한 `save` 명령을 입력받을 수 있다. +- [x] 저장된 게임 상태를 불러오기 위한 `load` 명령을 입력받을 수 있다. ### OutputView - [x] 현재 체스판의 상태를 출력한다. +- [x] 각 진영의 점수를 출력하고 어느 진영이 이겼는지 출력한다. diff --git a/src/main/java/Application.java b/src/main/java/Application.java index a852778d7e2..b8e9ed946a0 100644 --- a/src/main/java/Application.java +++ b/src/main/java/Application.java @@ -1,4 +1,5 @@ import controller.ChessController; +import service.DBService; import view.InputView; import view.OutputView; @@ -6,7 +7,8 @@ public class Application { public static void main(String[] args) { InputView inputView = new InputView(); OutputView outputView = new OutputView(); - ChessController chessController = new ChessController(inputView, outputView); + DBService dbService = new DBService(); + ChessController chessController = new ChessController(inputView, outputView, dbService); chessController.run(); } diff --git a/src/main/java/controller/ChessController.java b/src/main/java/controller/ChessController.java index 6330f6fd32f..aabdf0f6526 100644 --- a/src/main/java/controller/ChessController.java +++ b/src/main/java/controller/ChessController.java @@ -1,11 +1,13 @@ package controller; -import domain.GameCommand; -import domain.game.Board; -import domain.game.BoardInitializer; -import domain.game.Turn; +import domain.game.ChessGame; +import domain.game.GameRequest; +import domain.game.Piece; +import domain.game.TeamColor; +import domain.position.Position; +import service.DBService; import dto.BoardDto; -import dto.RequestDto; +import java.util.Map; import java.util.function.Supplier; import view.InputView; import view.OutputView; @@ -13,17 +15,21 @@ public class ChessController { private final InputView inputView; private final OutputView outputView; + private final DBService dbService; - public ChessController(final InputView inputView, final OutputView outputView) { + public ChessController(final InputView inputView, final OutputView outputView, final DBService dbService) { this.inputView = inputView; this.outputView = outputView; + this.dbService = dbService; } public void run() { outputView.printWelcomeMessage(); - GameCommand command = readUserInput(inputView::inputGameStart); - if (command.isStart()) { - startGame(); + GameRequest gameRequest = readUserInput(inputView::inputGameCommand).asRequest(); + while (gameRequest.isStart() || gameRequest.isLoad()) { + startGame(gameRequest); + outputView.printRestartMessage(); + gameRequest = readUserInput(inputView::inputGameCommand).asRequest(); } } @@ -32,35 +38,84 @@ private T readUserInput(Supplier inputSupplier) { try { return inputSupplier.get(); } catch (IllegalArgumentException e) { - System.out.println(e.getMessage()); + outputView.printErrorMessage(e.getMessage()); } } } - private void startGame() { - Board board = BoardInitializer.init(); - printStatus(board); + private void startGame(GameRequest gameRequest) { + ChessGame chessGame = createGame(gameRequest); + printBoardStatus(chessGame.getPositionsOfPieces()); - Turn turn = new Turn(); - RequestDto requestDto = readUserInput(inputView::inputGameCommand); - while (requestDto.command().isContinuable()) { - doTurn(board, turn, requestDto); - printStatus(board); - requestDto = readUserInput(inputView::inputGameCommand); + while (shouldProceedGame(gameRequest, chessGame)) { + outputView.printCurrentTurn(chessGame.currentPlayingTeam()); + gameRequest = readUserInput(inputView::inputGameCommand).asRequest(); + processRequest(gameRequest, chessGame); } + finishGame(gameRequest, chessGame); } - private void doTurn(Board board, Turn turn, RequestDto requestDto) { - try { - board.movePiece(turn.current(), requestDto.source(), requestDto.destination()); - turn.next(); - } catch (IllegalArgumentException e) { - System.out.println("[오류] " + e.getMessage()); + private ChessGame createGame(GameRequest gameRequest) { + if (gameRequest.isStart()) { + return new ChessGame(); } + int gameId = readUserInput(inputView::inputGameId); + return dbService.loadGame(gameId); } - private void printStatus(Board board) { - BoardDto boardDto = BoardDto.from(board); + private void printBoardStatus(Map positionOfPieces) { + BoardDto boardDto = BoardDto.from(positionOfPieces); outputView.printBoard(boardDto); } + + private boolean shouldProceedGame(GameRequest gameRequest, ChessGame chessGame) { + return gameRequest.isContinuable() && !chessGame.isGameEnd(); + } + + private void processRequest(GameRequest gameRequest, ChessGame chessGame) { + if (gameRequest.isSave()) { + saveCurrentStatus(chessGame); + return; + } + if (gameRequest.isContinuable()) { + playRound(gameRequest, chessGame); + } + } + + private void saveCurrentStatus(ChessGame chessGame) { + int gameId = dbService.saveGame(chessGame); + outputView.printSaveResult(gameId); + } + + private void playRound(GameRequest gameRequest, ChessGame chessGame) { + try { + chessGame.move(gameRequest.source(), gameRequest.destination()); + printBoardStatus(chessGame.getPositionsOfPieces()); + } catch (IllegalArgumentException | IllegalStateException e) { + outputView.printErrorMessage(e.getMessage()); + } + } + + private void finishGame(GameRequest gameRequest, ChessGame chessGame) { + outputView.printGameEndMessage(); + if (gameRequest.isEnd()) { + return; + } + + outputView.printStatusInputMessage(); + gameRequest = readUserInput(inputView::inputGameCommand).asRequest(); + if (gameRequest.isStatus()) { + printGameResult(chessGame); + } + } + + private void printGameResult(ChessGame chessGame) { + TeamColor winner = chessGame.getWinner(); + double whiteScore = chessGame.currentScoreOf(TeamColor.WHITE); + double blackScore = chessGame.currentScoreOf(TeamColor.BLACK); + + outputView.printWinner(winner); + outputView.printScore(TeamColor.WHITE, whiteScore); + outputView.printScore(TeamColor.BLACK, blackScore); + } } diff --git a/src/main/java/dao/DBConnector.java b/src/main/java/dao/DBConnector.java new file mode 100644 index 00000000000..facbb17e06a --- /dev/null +++ b/src/main/java/dao/DBConnector.java @@ -0,0 +1,33 @@ +package dao; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class DBConnector { + private static final String SERVER = "localhost:13306"; + private static final String DATABASE = "chess"; + private static final String OPTION = "?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"; + private static final String USERNAME = "root"; + private static final String PASSWORD = "root"; + + private static DBConnector instance = null; + + private DBConnector() { + } + + public static DBConnector getInstance() { + if (instance == null) { + instance = new DBConnector(); + } + return instance; + } + + public Connection getConnection() { + try { + return DriverManager.getConnection("jdbc:mysql://" + SERVER + "/" + DATABASE + OPTION, USERNAME, PASSWORD); + } catch (final SQLException e) { + throw new DBException("DB 연결 오류", e); + } + } +} diff --git a/src/main/java/dao/DBException.java b/src/main/java/dao/DBException.java new file mode 100644 index 00000000000..cccdd21aa39 --- /dev/null +++ b/src/main/java/dao/DBException.java @@ -0,0 +1,17 @@ +package dao; + +public class DBException extends RuntimeException { + private static final String DEFAULT_ERROR_MESSAGE = "쿼리 실행 중 오류가 발생했습니다."; + + public DBException(String message) { + super(message); + } + + public DBException(Throwable cause) { + super(DEFAULT_ERROR_MESSAGE, cause); + } + + public DBException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/dao/GameDao.java b/src/main/java/dao/GameDao.java new file mode 100644 index 00000000000..be0fcd27af4 --- /dev/null +++ b/src/main/java/dao/GameDao.java @@ -0,0 +1,12 @@ +package dao; + +import domain.game.TeamColor; +import java.sql.Connection; + +public interface GameDao { + int addGame(Connection connection); + + TeamColor findTurn(Connection connection, int gameId); + + void updateTurn(Connection connection, int gameId, TeamColor teamColor); +} diff --git a/src/main/java/dao/GameDaoImpl.java b/src/main/java/dao/GameDaoImpl.java new file mode 100644 index 00000000000..bbcd3875e09 --- /dev/null +++ b/src/main/java/dao/GameDaoImpl.java @@ -0,0 +1,56 @@ +package dao; + +import domain.game.TeamColor; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class GameDaoImpl implements GameDao { + private static final String TABLE_NAME = "game"; + + @Override + public int addGame(Connection connection) { + final String query = String.format("INSERT INTO %s(turn) VALUE(?);", TABLE_NAME); + try (PreparedStatement preparedStatement = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) { + preparedStatement.setString(1, TeamColor.WHITE.name()); + preparedStatement.executeUpdate(); + ResultSet resultSet = preparedStatement.getGeneratedKeys(); + if (resultSet.next()) { + return resultSet.getInt(1); + } + } catch (final SQLException e) { + throw new DBException(e); + } + throw new DBException("게임 생성 중 오류가 발생했습니다."); + } + + @Override + public TeamColor findTurn(Connection connection, int gameId) { + final String query = String.format("SELECT turn FROM %s WHERE `gameId` = ?", TABLE_NAME); + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setInt(1, gameId); + final ResultSet resultSet = preparedStatement.executeQuery(); + if (resultSet.next()) { + String turn = resultSet.getString("turn"); + return TeamColor.valueOf(turn); + } + } catch (SQLException e) { + throw new DBException(e); + } + throw new DBException(gameId + " 에 해당하는 차례를 찾을 수 없습니다."); + } + + @Override + public void updateTurn(Connection connection, int gameId, TeamColor teamColor) { + final var query = String.format("UPDATE %s SET turn = ? WHERE gameId = ?", TABLE_NAME); + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setString(1, teamColor.name()); + preparedStatement.setInt(2, gameId); + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw new DBException(e); + } + } +} diff --git a/src/main/java/dao/PieceDao.java b/src/main/java/dao/PieceDao.java new file mode 100644 index 00000000000..518d62ea284 --- /dev/null +++ b/src/main/java/dao/PieceDao.java @@ -0,0 +1,11 @@ +package dao; + +import dto.PieceDto; +import java.sql.Connection; +import java.util.List; + +public interface PieceDao { + void addAll(Connection connection, List pieceDtos, int gameId); + + List findAllPieces(Connection connection, int gameId); +} diff --git a/src/main/java/dao/PieceDaoImpl.java b/src/main/java/dao/PieceDaoImpl.java new file mode 100644 index 00000000000..41d6f61bbc5 --- /dev/null +++ b/src/main/java/dao/PieceDaoImpl.java @@ -0,0 +1,51 @@ +package dao; + +import dto.PieceDto; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class PieceDaoImpl implements PieceDao { + private static final String TABLE_NAME = "piece"; + + @Override + public void addAll(Connection connection, List pieceDtos, int gameId) { + final String query = String.format("INSERT INTO %s VALUES(?, ?, ?, ?, ?)", TABLE_NAME); + try (PreparedStatement pstmt = connection.prepareStatement(query)) { + for (PieceDto pieceDto : pieceDtos) { + pstmt.setInt(1, pieceDto.fileIndex()); + pstmt.setInt(2, pieceDto.rankIndex()); + pstmt.setString(3, pieceDto.color()); + pstmt.setString(4, pieceDto.type()); + pstmt.setInt(5, gameId); + pstmt.addBatch(); + } + pstmt.executeBatch(); + } catch (SQLException e) { + throw new DBException(e); + } + } + + @Override + public List findAllPieces(Connection connection, int gameId) { + final List pieces = new ArrayList<>(); + final String query = "SELECT * FROM " + TABLE_NAME + " WHERE gameId = ?"; + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setInt(1, gameId); + final ResultSet resultSet = preparedStatement.executeQuery(); + while (resultSet.next()) { + int fileIndex = resultSet.getInt("file"); + int rankIndex = resultSet.getInt("rank"); + String color = resultSet.getString("pieceColor"); + String type = resultSet.getString("pieceType"); + pieces.add(new PieceDto(fileIndex, rankIndex, color, type)); + } + } catch (final SQLException e) { + throw new DBException(e); + } + return pieces; + } +} diff --git a/src/main/java/domain/GameCommand.java b/src/main/java/domain/GameCommand.java index 3d38ad62863..ec9e4ebcc4b 100644 --- a/src/main/java/domain/GameCommand.java +++ b/src/main/java/domain/GameCommand.java @@ -1,7 +1,7 @@ package domain; public enum GameCommand { - START, MOVE, END; + START, MOVE, STATUS, SAVE, LOAD, END; public boolean isContinuable() { return this != END; @@ -10,4 +10,20 @@ public boolean isContinuable() { public boolean isStart() { return this == START; } + + public boolean isStatus() { + return this == STATUS; + } + + public boolean isSave() { + return this == SAVE; + } + + public boolean isEnd() { + return this == END; + } + + public boolean isLoad() { + return this == LOAD; + } } diff --git a/src/main/java/domain/game/Board.java b/src/main/java/domain/game/Board.java index 73ddcacc8ce..ac4b38bf49b 100644 --- a/src/main/java/domain/game/Board.java +++ b/src/main/java/domain/game/Board.java @@ -1,30 +1,35 @@ package domain.game; +import domain.position.File; import domain.position.Position; - import java.util.Collections; import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; public class Board { + public static final double DUPLICATED_PAWN_PENALTY_RATE = 0.5; + private final Map chessBoard; public Board(final Map chessBoard) { this.chessBoard = chessBoard; } - public Map getChessBoard() { - return Collections.unmodifiableMap(chessBoard); - } - - public void movePiece(final TeamColor teamColor, final Position source, final Position destination) { + public MoveResponse movePiece(final TeamColor teamColor, final Position source, final Position destination) { validateMoveRequest(teamColor, source, destination); - boolean caughtEnemy = isPieceExist(destination); Piece piece = chessBoard.get(source); - chessBoard.put(destination, piece); chessBoard.remove(source); + Optional caughtPiece = getCaughtPiece(destination); + chessBoard.put(destination, piece); + + return caughtPiece + .map(Piece::getPieceType) + .map(CaughtMoveResponse::new) + .orElseGet(NormalMoveResponse::new); } private void validateMoveRequest(TeamColor teamColor, Position source, Position destination) { @@ -72,7 +77,43 @@ private boolean isAllyPieceExistOnDestination(TeamColor teamColor, Position dest return isPieceExist(destination) && (chessBoard.get(destination).hasColor(teamColor)); } + private Optional getCaughtPiece(Position destination) { + if (!chessBoard.containsKey(destination)) { + return Optional.empty(); + } + return Optional.of(chessBoard.get(destination)); + } + private boolean isPieceExist(Position position) { return chessBoard.containsKey(position); } + + public double calculateScoreOf(TeamColor teamColor) { + Map teamPieces = chessBoard.entrySet().stream() + .filter(entry -> entry.getValue().hasColor(teamColor)) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + double totalPieceValue = teamPieces.values().stream() + .mapToDouble(Piece::value) + .sum(); + + return totalPieceValue - calcDuplicatedPawnPenalty(teamPieces); + } + + private double calcDuplicatedPawnPenalty(Map teamPieces) { + Map pawnCounts = teamPieces.entrySet().stream() + .filter(entry -> entry.getValue().isPawn()) + .collect(Collectors.groupingBy(entry -> entry.getKey().file(), Collectors.counting())); + + int duplicatedPawnCount = pawnCounts.values().stream() + .filter(count -> count > 1) + .mapToInt(Long::intValue) + .sum(); + + return duplicatedPawnCount * DUPLICATED_PAWN_PENALTY_RATE; + } + + public Map getPositionsOfPieces() { + return Collections.unmodifiableMap(chessBoard); + } } diff --git a/src/main/java/domain/game/CaughtMoveResponse.java b/src/main/java/domain/game/CaughtMoveResponse.java new file mode 100644 index 00000000000..81b8d0f0205 --- /dev/null +++ b/src/main/java/domain/game/CaughtMoveResponse.java @@ -0,0 +1,19 @@ +package domain.game; + +public final class CaughtMoveResponse extends MoveResponse { + private final PieceType caughtPieceType; + + public CaughtMoveResponse(PieceType caughtPieceType) { + this.caughtPieceType = caughtPieceType; + } + + @Override + public boolean hasCaught() { + return true; + } + + @Override + public PieceType caughtPieceType() { + return caughtPieceType; + } +} diff --git a/src/main/java/domain/game/ChessGame.java b/src/main/java/domain/game/ChessGame.java new file mode 100644 index 00000000000..4e8a6d33735 --- /dev/null +++ b/src/main/java/domain/game/ChessGame.java @@ -0,0 +1,95 @@ +package domain.game; + +import domain.game.state.BlackTurn; +import domain.game.state.GameEnd; +import domain.game.state.GameState; +import domain.game.state.WhiteTurn; +import domain.position.Position; +import java.util.Map; +import java.util.Set; + +public class ChessGame { + private static final Set GAME_END_WHEN_CAUGHT = Set.of(PieceType.WHITE_KING, PieceType.BLACK_KING); + + private GameState state; + private final Board board; + + protected ChessGame(GameState state, Board board) { + this.state = state; + this.board = board; + } + + public ChessGame() { + this(WhiteTurn.getInstance(), BoardInitializer.init()); + } + + public static ChessGame of(TeamColor savedTurn, Map piecePositions) { + GameState state = getGameStateOf(savedTurn); + return new ChessGame(state, new Board(piecePositions)); + } + + private static GameState getGameStateOf(TeamColor savedTurn) { + if (savedTurn == TeamColor.WHITE) { + return WhiteTurn.getInstance(); + } + return BlackTurn.getInstance(); + } + + public void move(Position source, Position destination) { + checkMovableState(); + MoveResponse moveResponse = board.movePiece(state.currentTurn(), source, destination); + state = changeState(moveResponse); + } + + private void checkMovableState() { + if (isGameEnd()) { + throw new IllegalStateException("게임이 종료되어 더 이상 움직일 수 없습니다."); + } + } + + private GameState changeState(MoveResponse moveResponse) { + if (shouldGameEnd(moveResponse)) { + return winStateOf(state); + } + return nextPlayingStateOf(state); + } + + private boolean shouldGameEnd(MoveResponse moveResponse) { + if (!moveResponse.hasCaught()) { + return false; + } + PieceType caughtPieceType = moveResponse.caughtPieceType(); + return GAME_END_WHEN_CAUGHT.contains(caughtPieceType); + } + + private GameState winStateOf(GameState state) { + return GameEnd.getInstance(state.currentTurn()); + } + + private GameState nextPlayingStateOf(GameState state) { + if (state.isTurnOf(TeamColor.WHITE)) { + return BlackTurn.getInstance(); + } + return WhiteTurn.getInstance(); + } + + public boolean isGameEnd() { + return !state.isContinuable(); + } + + public TeamColor getWinner() { + return state.getWinner(); + } + + public double currentScoreOf(TeamColor teamColor) { + return board.calculateScoreOf(teamColor); + } + + public TeamColor currentPlayingTeam() { + return state.currentTurn(); + } + + public Map getPositionsOfPieces() { + return board.getPositionsOfPieces(); + } +} diff --git a/src/main/java/domain/game/GameRequest.java b/src/main/java/domain/game/GameRequest.java new file mode 100644 index 00000000000..2792a43a3f7 --- /dev/null +++ b/src/main/java/domain/game/GameRequest.java @@ -0,0 +1,63 @@ +package domain.game; + +import domain.GameCommand; +import domain.position.Position; +import java.util.Collections; +import java.util.List; + +public class GameRequest { + private static final int MOVE_SOURCE_INDEX = 0; + private static final int MOVE_DESTINATION_INDEX = 1; + + private final GameCommand commandType; + private final List arguments; + + public GameRequest(GameCommand commandType, List arguments) { + this.commandType = commandType; + this.arguments = arguments; + } + + public static GameRequest ofNoArgument(GameCommand commandType) { + return new GameRequest(commandType, Collections.emptyList()); + } + + public Position source() { + checkIsMove(); + return arguments.get(MOVE_SOURCE_INDEX); + } + + public Position destination() { + checkIsMove(); + return arguments.get(MOVE_DESTINATION_INDEX); + } + + private void checkIsMove() { + if (!commandType.equals(GameCommand.MOVE)) { + throw new IllegalArgumentException("유효하지 않은 커멘드 타입입니다."); + } + } + + public boolean isContinuable() { + return commandType.isContinuable(); + } + + public boolean isStart() { + return commandType.isStart(); + } + + public boolean isStatus() { + return commandType.isStatus(); + } + + public boolean isSave() { + return commandType.isSave(); + } + + public boolean isEnd() { + return commandType.isEnd(); + } + + public boolean isLoad() { + return commandType.isLoad(); + } +} diff --git a/src/main/java/domain/game/MoveResponse.java b/src/main/java/domain/game/MoveResponse.java new file mode 100644 index 00000000000..4200a10bfbd --- /dev/null +++ b/src/main/java/domain/game/MoveResponse.java @@ -0,0 +1,7 @@ +package domain.game; + +public abstract class MoveResponse { + public abstract boolean hasCaught(); + + public abstract PieceType caughtPieceType(); +} diff --git a/src/main/java/domain/game/NormalMoveResponse.java b/src/main/java/domain/game/NormalMoveResponse.java new file mode 100644 index 00000000000..6cf1d0c98cc --- /dev/null +++ b/src/main/java/domain/game/NormalMoveResponse.java @@ -0,0 +1,13 @@ +package domain.game; + +public final class NormalMoveResponse extends MoveResponse { + @Override + public boolean hasCaught() { + return false; + } + + @Override + public PieceType caughtPieceType() { + throw new IllegalStateException("상대의 말이 잡히지 않았습니다."); + } +} diff --git a/src/main/java/domain/game/Piece.java b/src/main/java/domain/game/Piece.java index a9577006295..6e6b16c409f 100644 --- a/src/main/java/domain/game/Piece.java +++ b/src/main/java/domain/game/Piece.java @@ -14,15 +14,30 @@ public Piece(final PieceType pieceType, final MoveStrategy moveStrategy) { this.moveStrategy = moveStrategy; } - public PieceType getPieceType() { - return pieceType; - } - public boolean hasColor(final TeamColor teamColor) { return teamColor.contains(pieceType); } + public TeamColor color() { + if (hasColor(TeamColor.WHITE)) { + return TeamColor.WHITE; + } + return TeamColor.BLACK; + } + public boolean isMovable(final Position source, final Position destination, final Set piecePositions) { return moveStrategy.isMovable(source, destination, piecePositions); } + + public double value() { + return pieceType.value(); + } + + public boolean isPawn() { + return pieceType.isPawn(); + } + + public PieceType getPieceType() { + return pieceType; + } } diff --git a/src/main/java/domain/game/PieceType.java b/src/main/java/domain/game/PieceType.java index efb7c999b23..fe7f72dedc6 100644 --- a/src/main/java/domain/game/PieceType.java +++ b/src/main/java/domain/game/PieceType.java @@ -1,6 +1,30 @@ package domain.game; +import java.util.Arrays; + public enum PieceType { - BLACK_PAWN, BLACK_ROOK, BLACK_KNIGHT, BLACK_BISHOP, BLACK_QUEEN, BLACK_KING, - WHITE_PAWN, WHITE_ROOK, WHITE_KNIGHT, WHITE_BISHOP, WHITE_QUEEN, WHITE_KING + BLACK_PAWN(1), BLACK_KNIGHT(2.5), BLACK_BISHOP(3), BLACK_ROOK(5), BLACK_QUEEN(9), BLACK_KING(0), + WHITE_PAWN(1), WHITE_KNIGHT(2.5), WHITE_BISHOP(3), WHITE_ROOK(5), WHITE_QUEEN(9), WHITE_KING(0); + + private final double value; + + PieceType(double value) { + this.value = value; + } + + public static PieceType fromColorAndType(TeamColor teamColor, String type) { + return Arrays.stream(values()) + .filter(teamColor::contains) + .filter(pieceType -> pieceType.name().equals(type)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("해당 기물이 없습니다.")); + } + + public double value() { + return value; + } + + public boolean isPawn() { + return this == BLACK_PAWN || this == WHITE_PAWN; + } } diff --git a/src/main/java/domain/game/TeamColor.java b/src/main/java/domain/game/TeamColor.java index 1d6780a537e..760790004e4 100644 --- a/src/main/java/domain/game/TeamColor.java +++ b/src/main/java/domain/game/TeamColor.java @@ -17,11 +17,4 @@ public enum TeamColor { public boolean contains(PieceType pieceType) { return includedPieces.contains(pieceType); } - - public TeamColor toggle() { - if (this == BLACK) { - return WHITE; - } - return BLACK; - } } diff --git a/src/main/java/domain/game/Turn.java b/src/main/java/domain/game/Turn.java deleted file mode 100644 index d8be17d3b97..00000000000 --- a/src/main/java/domain/game/Turn.java +++ /dev/null @@ -1,17 +0,0 @@ -package domain.game; - -public class Turn { - private TeamColor currentTurn; - - public Turn() { - this.currentTurn = TeamColor.WHITE; - } - - public void next() { - currentTurn = currentTurn.toggle(); - } - - public TeamColor current() { - return currentTurn; - } -} diff --git a/src/main/java/domain/game/state/BlackTurn.java b/src/main/java/domain/game/state/BlackTurn.java new file mode 100644 index 00000000000..07ca789012c --- /dev/null +++ b/src/main/java/domain/game/state/BlackTurn.java @@ -0,0 +1,19 @@ +package domain.game.state; + +import domain.game.TeamColor; + +public final class BlackTurn extends GamePlaying { + private static final BlackTurn instance = new BlackTurn(); + + private BlackTurn() { + } + + public static BlackTurn getInstance() { + return instance; + } + + @Override + public TeamColor currentTurn() { + return TeamColor.BLACK; + } +} diff --git a/src/main/java/domain/game/state/GameEnd.java b/src/main/java/domain/game/state/GameEnd.java new file mode 100644 index 00000000000..62f5e2fa7b5 --- /dev/null +++ b/src/main/java/domain/game/state/GameEnd.java @@ -0,0 +1,40 @@ +package domain.game.state; + +import domain.game.TeamColor; +import java.util.EnumMap; + +public final class GameEnd implements GameState { + private static final EnumMap CACHE = new EnumMap<>(TeamColor.class); + + private static final String GAME_END_MESSAGE = "게임이 종료되었습니다."; + + private final TeamColor winner; + + private GameEnd(TeamColor winner) { + this.winner = winner; + } + + public static GameEnd getInstance(TeamColor winner) { + return CACHE.computeIfAbsent(winner, GameEnd::new); + } + + @Override + public boolean isContinuable() { + return false; + } + + @Override + public boolean isTurnOf(TeamColor teamColor) { + throw new IllegalStateException(GAME_END_MESSAGE); + } + + @Override + public TeamColor currentTurn() { + throw new IllegalStateException(GAME_END_MESSAGE); + } + + @Override + public TeamColor getWinner() { + return winner; + } +} diff --git a/src/main/java/domain/game/state/GamePlaying.java b/src/main/java/domain/game/state/GamePlaying.java new file mode 100644 index 00000000000..13b28635a37 --- /dev/null +++ b/src/main/java/domain/game/state/GamePlaying.java @@ -0,0 +1,20 @@ +package domain.game.state; + +import domain.game.TeamColor; + +public abstract sealed class GamePlaying implements GameState permits WhiteTurn, BlackTurn { + @Override + public boolean isContinuable() { + return true; + } + + @Override + public boolean isTurnOf(TeamColor teamColor) { + return teamColor.equals(currentTurn()); + } + + @Override + public TeamColor getWinner() { + throw new IllegalStateException("아직 게임이 진행중입니다."); + } +} diff --git a/src/main/java/domain/game/state/GameState.java b/src/main/java/domain/game/state/GameState.java new file mode 100644 index 00000000000..1997ee42ae0 --- /dev/null +++ b/src/main/java/domain/game/state/GameState.java @@ -0,0 +1,13 @@ +package domain.game.state; + +import domain.game.TeamColor; + +public interface GameState { + boolean isContinuable(); + + boolean isTurnOf(TeamColor teamColor); + + TeamColor currentTurn(); + + TeamColor getWinner(); +} diff --git a/src/main/java/domain/game/state/WhiteTurn.java b/src/main/java/domain/game/state/WhiteTurn.java new file mode 100644 index 00000000000..ceb89ac4b83 --- /dev/null +++ b/src/main/java/domain/game/state/WhiteTurn.java @@ -0,0 +1,19 @@ +package domain.game.state; + +import domain.game.TeamColor; + +public final class WhiteTurn extends GamePlaying { + private static final WhiteTurn instance = new WhiteTurn(); + + private WhiteTurn() { + } + + public static WhiteTurn getInstance() { + return instance; + } + + @Override + public TeamColor currentTurn() { + return TeamColor.WHITE; + } +} diff --git a/src/main/java/domain/strategy/ContinuousMoveStrategy.java b/src/main/java/domain/strategy/ContinuousMoveStrategy.java index 6dbff3e67ac..1807f03cf10 100644 --- a/src/main/java/domain/strategy/ContinuousMoveStrategy.java +++ b/src/main/java/domain/strategy/ContinuousMoveStrategy.java @@ -33,7 +33,7 @@ public boolean isMovable(final Position source, final Position destination, fina return isReachable(destination, optimalVector, movePaths); } - private static boolean isContinuable(final Position current, final Position destination, final Set piecePositions) { + private boolean isContinuable(final Position current, final Position destination, final Set piecePositions) { boolean isReachedDestination = current.equals(destination); boolean isOtherPieceExist = piecePositions.contains(current); return !isReachedDestination && !isOtherPieceExist; diff --git a/src/main/java/dto/BoardDto.java b/src/main/java/dto/BoardDto.java index dedb06e29c9..ab22b2b6472 100644 --- a/src/main/java/dto/BoardDto.java +++ b/src/main/java/dto/BoardDto.java @@ -1,17 +1,15 @@ package dto; -import domain.game.Board; +import domain.game.Piece; import domain.game.PieceType; import domain.position.Position; - import java.util.Collections; import java.util.Map; import java.util.stream.Collectors; public record BoardDto(Map piecePositions) { - public static BoardDto from(final Board board) { - Map piecePositions = board.getChessBoard() - .entrySet() + public static BoardDto from(final Map positionsOfPieces) { + Map piecePositions = positionsOfPieces.entrySet() .stream() .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().getPieceType())); diff --git a/src/main/java/dto/CommandDto.java b/src/main/java/dto/CommandDto.java new file mode 100644 index 00000000000..3c2bebcb89e --- /dev/null +++ b/src/main/java/dto/CommandDto.java @@ -0,0 +1,39 @@ +package dto; + +import domain.GameCommand; +import domain.game.GameRequest; +import domain.position.Position; +import java.util.List; + +public record CommandDto(GameCommand command, MovePositionDto movePositionDto) { + private static final String NO_POSITION_DATA = "이동 위치 정보가 없습니다."; + + public static CommandDto of(GameCommand command) { + return new CommandDto(command, MovePositionDto.noPosition()); + } + + public static CommandDto of(GameCommand command, Position source, Position destination) { + return new CommandDto(command, MovePositionDto.of(source, destination)); + } + + public Position source() { + if (movePositionDto.doesNotHavePosition()) { + throw new IllegalArgumentException(NO_POSITION_DATA); + } + return movePositionDto.source(); + } + + public Position destination() { + if (movePositionDto.doesNotHavePosition()) { + throw new IllegalArgumentException(NO_POSITION_DATA); + } + return movePositionDto.destination(); + } + + public GameRequest asRequest() { + if (movePositionDto.doesNotHavePosition()) { + return GameRequest.ofNoArgument(command); + } + return new GameRequest(command, List.of(source(), destination())); + } +} diff --git a/src/main/java/dto/PieceDto.java b/src/main/java/dto/PieceDto.java new file mode 100644 index 00000000000..04ac363145d --- /dev/null +++ b/src/main/java/dto/PieceDto.java @@ -0,0 +1,31 @@ +package dto; + +import domain.game.Piece; +import domain.game.PieceType; +import domain.game.TeamColor; +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; + +public record PieceDto( + int fileIndex, int rankIndex, + String color, String type +) { + public static PieceDto of(Position position, Piece piece) { + return new PieceDto( + position.columnIndex(), + position.rowIndex(), + piece.color().name(), + piece.getPieceType().name() + ); + } + + public Position getPosition() { + return new Position(File.of(fileIndex), Rank.of(rankIndex)); + } + + public PieceType getPieceType() { + TeamColor teamColor = TeamColor.valueOf(color); + return PieceType.fromColorAndType(teamColor, type); + } +} diff --git a/src/main/java/dto/RequestDto.java b/src/main/java/dto/RequestDto.java deleted file mode 100644 index 3a0ff97ba4b..00000000000 --- a/src/main/java/dto/RequestDto.java +++ /dev/null @@ -1,28 +0,0 @@ -package dto; - -import domain.GameCommand; -import domain.position.Position; - -public record RequestDto(GameCommand command, MovePositionDto movePositionDto) { - public static RequestDto of(GameCommand command) { - return new RequestDto(command, MovePositionDto.noPosition()); - } - - public static RequestDto of(GameCommand command, Position source, Position destination) { - return new RequestDto(command, MovePositionDto.of(source, destination)); - } - - public Position source() { - if (movePositionDto.doesNotHavePosition()) { - throw new IllegalArgumentException("이동 위치 정보가 없습니다."); - } - return movePositionDto.source(); - } - - public Position destination() { - if (movePositionDto.doesNotHavePosition()) { - throw new IllegalArgumentException("이동 위치 정보가 없습니다."); - } - return movePositionDto.destination(); - } -} diff --git a/src/main/java/resource/game.sql b/src/main/java/resource/game.sql new file mode 100644 index 00000000000..94e1bfb04d9 --- /dev/null +++ b/src/main/java/resource/game.sql @@ -0,0 +1,7 @@ +use chess; + +CREATE TABLE `game` ( + `gameId` int NOT NULL AUTO_INCREMENT, + `turn` varchar(10) DEFAULT NULL, + PRIMARY KEY (`gameId`) +); diff --git a/src/main/java/resource/piece.sql b/src/main/java/resource/piece.sql new file mode 100644 index 00000000000..334434f24ad --- /dev/null +++ b/src/main/java/resource/piece.sql @@ -0,0 +1,12 @@ +use chess; + +CREATE TABLE `piece` ( + `file` int NOT NULL, + `rank` int NOT NULL, + `pieceColor` varchar(50) NOT NULL, + `pieceType` varchar(50) NOT NULL, + `gameId` int NOT NULL, + PRIMARY KEY (`file`,`rank`,`gameId`), + KEY `gameId` (`gameId`), + CONSTRAINT `gameId` FOREIGN KEY (`gameId`) REFERENCES `game` (`gameId`) +); diff --git a/src/main/java/service/DBService.java b/src/main/java/service/DBService.java new file mode 100644 index 00000000000..1a879fbd12a --- /dev/null +++ b/src/main/java/service/DBService.java @@ -0,0 +1,82 @@ +package service; + +import dao.DBConnector; +import dao.DBException; +import dao.GameDao; +import dao.GameDaoImpl; +import dao.PieceDao; +import dao.PieceDaoImpl; +import domain.game.ChessGame; +import domain.game.Piece; +import domain.game.PieceFactory; +import domain.game.TeamColor; +import domain.position.Position; +import dto.PieceDto; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class DBService { + private final GameDao gameDao; + private final PieceDao pieceDao; + + protected DBService(GameDao gameDao, PieceDao pieceDao) { + this.gameDao = gameDao; + this.pieceDao = pieceDao; + } + + public DBService() { + this(new GameDaoImpl(), new PieceDaoImpl()); + } + + private Connection getConnectionOfAutoCommit(boolean autoCommit) { + Connection connection = DBConnector.getInstance().getConnection(); + try { + connection.setAutoCommit(autoCommit); + } catch (SQLException e) { + throw new DBException(e); + } + return connection; + } + + public ChessGame loadGame(int gameId) { + Connection connection = getConnectionOfAutoCommit(true); + TeamColor savedTurn = gameDao.findTurn(connection, gameId); + List savedPieces = pieceDao.findAllPieces(connection, gameId); + return ChessGame.of(savedTurn, separatePositionAndPiece(savedPieces)); + } + + private Map separatePositionAndPiece(List savedPieces) { + return savedPieces.stream() + .collect(Collectors.toMap( + PieceDto::getPosition, + dto -> PieceFactory.create(dto.getPieceType()) + )); + } + + public int saveGame(ChessGame chessGame) { + Connection connection = getConnectionOfAutoCommit(false); + try (connection) { + int gameId = gameDao.addGame(connection); + gameDao.updateTurn(connection, gameId, chessGame.currentPlayingTeam()); + pieceDao.addAll(connection, collectPositionOfPieces(chessGame), gameId); + connection.commit(); + return gameId; + } catch (SQLException | DBException e) { + try { + connection.rollback(); + } catch (SQLException rollbackException) { + throw new IllegalStateException("트랜잭션 롤백 중 오류가 발생했습니다.", rollbackException); + } + throw new DBException("게임 저장 중 오류가 발생했습니다.", e); + } + } + + private List collectPositionOfPieces(ChessGame chessGame) { + return chessGame.getPositionsOfPieces().entrySet().stream() + .map(entry -> PieceDto.of(entry.getKey(), entry.getValue())) + .toList(); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java index a91291d5d0b..8904321a5cf 100644 --- a/src/main/java/view/InputView.java +++ b/src/main/java/view/InputView.java @@ -2,7 +2,7 @@ import domain.GameCommand; import domain.position.Position; -import dto.RequestDto; +import dto.CommandDto; import java.util.Arrays; import java.util.List; @@ -12,26 +12,28 @@ public class InputView { private static final String START_COMMAND = "start"; private static final String MOVE_COMMAND = "move"; + private static final String STATUS_COMMAND = "status"; + private static final String SAVE_COMMAND = "save"; + private static final String LOAD_COMMAND = "load"; private static final String END_COMMAND = "end"; private static final Map gameCommands = Map.of( START_COMMAND, GameCommand.START, MOVE_COMMAND, GameCommand.MOVE, + STATUS_COMMAND, GameCommand.STATUS, + SAVE_COMMAND, GameCommand.SAVE, + LOAD_COMMAND, GameCommand.LOAD, END_COMMAND, GameCommand.END ); + private static final String COMMAND_DELIMITER = " "; + private static final int COMMAND_TYPE_POSITION = 0; + private static final int MOVE_COMMAND_SOURCE_POSITION = 1; + private static final int MOVE_COMMAND_DESTINATION_POSITION = 2; private final Scanner sc = new Scanner(System.in); - public GameCommand inputGameStart() { - String input = sc.nextLine(); - if (!gameCommands.containsKey(input)) { - throw new IllegalArgumentException("잘못된 명령입니다."); - } - return gameCommands.get(input); - } - - public RequestDto inputGameCommand() { - List input = Arrays.stream(sc.nextLine().split(" ")).toList(); - String commandType = input.get(0); + public CommandDto inputGameCommand() { + List input = Arrays.stream(sc.nextLine().split(COMMAND_DELIMITER)).toList(); + String commandType = input.get(COMMAND_TYPE_POSITION); if (!gameCommands.containsKey(commandType)) { throw new IllegalArgumentException("유효하지 않은 명령입니다."); @@ -39,17 +41,22 @@ public RequestDto inputGameCommand() { GameCommand command = gameCommands.get(commandType); if (input.size() == 3) { - return createRequestDtoFromInput(input, command); + return includePositionToCommand(input, command); } - return RequestDto.of(command); + return CommandDto.of(command); } - private RequestDto createRequestDtoFromInput(List input, GameCommand command) { - String sourcePosition = input.get(1); - String destinationPosition = input.get(2); + private CommandDto includePositionToCommand(List input, GameCommand command) { + String sourcePosition = input.get(MOVE_COMMAND_SOURCE_POSITION); + String destinationPosition = input.get(MOVE_COMMAND_DESTINATION_POSITION); Position source = PositionConvertor.convertPosition(sourcePosition); Position destination = PositionConvertor.convertPosition(destinationPosition); - return RequestDto.of(command, source, destination); + return CommandDto.of(command, source, destination); + } + + public int inputGameId() { + System.out.print("로드할 게임 ID를 입력하세요 >> "); + return Integer.parseInt(sc.nextLine()); } } diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java index 07e5681dd8f..26f8d2af475 100644 --- a/src/main/java/view/OutputView.java +++ b/src/main/java/view/OutputView.java @@ -1,6 +1,7 @@ package view; import domain.game.PieceType; +import domain.game.TeamColor; import domain.position.Position; import dto.BoardDto; @@ -16,7 +17,11 @@ public class OutputView { private static final String RANK_DESCRIPTION_SEPARATOR = "|"; private static final String PAD_CHAR = " "; private static final int PIECE_CHAR_PAD_SIZE = 1; - public static final String EMPTY_PIECE = "."; + private static final String EMPTY_PIECE = "."; + private static final Map TEAM_NAME= Map.of( + TeamColor.WHITE, "흰색 팀", + TeamColor.BLACK, "검은색 팀" + ); private static final String NEW_LINE = System.lineSeparator(); public void printWelcomeMessage() { @@ -24,6 +29,8 @@ public void printWelcomeMessage() { > 체스 게임을 시작합니다. > 게임 시작: start > 게임 종료: end + > 게임 저장: save + > 게임 로드: load > 게임 이동: move source위치 target위치 - 예. move b2 b3""" ); } @@ -32,7 +39,7 @@ public void printBoard(final BoardDto boardDto) { List boardStatus = convertBoardStatus(boardDto.piecePositions()); StringBuilder sb = new StringBuilder(); - sb.append(pad(FILE_DESCRIPTION, FILE_DESCRIPTION_PAD_SIZE)).append(NEW_LINE); + sb.append(NEW_LINE).append(pad(FILE_DESCRIPTION, FILE_DESCRIPTION_PAD_SIZE)).append(NEW_LINE); sb.append(pad(FILE_DESCRIPTION_SEPARATOR, FILE_DESCRIPTION_PAD_SIZE)).append(NEW_LINE); for (int rank = boardStatus.size(); rank > 0; rank--) { sb.append(rank).append(PAD_CHAR).append(RANK_DESCRIPTION_SEPARATOR) @@ -72,4 +79,43 @@ private void buildStrings(final String[][] strings, final Position position, fin private String pad(String origin, int padSize) { return PAD_CHAR.repeat(padSize) + origin + PAD_CHAR.repeat(padSize); } + + public void printCurrentTurn(TeamColor teamColor) { + System.out.printf("(%s) 차례 >> ", getTeamName(teamColor)); + } + + public void printWinner(TeamColor winner) { + System.out.printf("우승자는 %s 입니다.%n", getTeamName(winner)); + } + + public void printScore(TeamColor teamColor, double score) { + System.out.printf("%s 의 점수: %.1f%n", getTeamName(teamColor), score); + } + + private String getTeamName(TeamColor teamColor) { + if (!TEAM_NAME.containsKey(teamColor)) { + throw new IllegalArgumentException("팀 정보가 존재하지 않습니다."); + } + return TEAM_NAME.get(teamColor); + } + + public void printRestartMessage() { + System.out.println("다시 시작하려면 start 를, 다른 게임을 불러오려면 load 를 입력하세요."); + } + + public void printErrorMessage(String message) { + System.out.println("[오류] " + message); + } + + public void printStatusInputMessage() { + System.out.println("결과를 확인하려면 status 를 입력하세요."); + } + + public void printGameEndMessage() { + System.out.println("게임이 종료되었습니다."); + } + + public void printSaveResult(int gameId) { + System.out.println("게임 진행 상황이 저장되었습니다. ID: " + gameId); + } } diff --git a/src/test/java/dao/FakeGameDao.java b/src/test/java/dao/FakeGameDao.java new file mode 100644 index 00000000000..50d5f0b32bd --- /dev/null +++ b/src/test/java/dao/FakeGameDao.java @@ -0,0 +1,32 @@ +package dao; + +import domain.game.TeamColor; +import java.sql.Connection; +import java.util.HashMap; +import java.util.Map; + +public class FakeGameDao implements GameDao { + private static final Map data = new HashMap<>(); + + static { + data.put(1, TeamColor.BLACK); + data.put(2, TeamColor.WHITE); + } + + @Override + public int addGame(Connection notUsed) { + int generatedKey = data.size() + 1; + data.put(generatedKey, TeamColor.WHITE); + return generatedKey; + } + + @Override + public TeamColor findTurn(Connection notUsed, int gameId) { + return data.get(gameId); + } + + @Override + public void updateTurn(Connection notUsed, int gameId, TeamColor teamColor) { + data.put(gameId, teamColor); + } +} diff --git a/src/test/java/dao/FakePieceDao.java b/src/test/java/dao/FakePieceDao.java new file mode 100644 index 00000000000..5fdad5fbd9c --- /dev/null +++ b/src/test/java/dao/FakePieceDao.java @@ -0,0 +1,33 @@ +package dao; + +import dto.PieceDto; +import java.sql.Connection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FakePieceDao implements PieceDao { + private static final Map> data = new HashMap<>(); + + static { + data.put(1, List.of( + new PieceDto(1, 1, "BLACK", "BLACK_PAWN"), + new PieceDto(5, 3, "WHITE", "WHITE_PAWN") + )); + data.put(2, List.of( + new PieceDto(2, 2, "BLACK", "BLACK_KING"), + new PieceDto(4, 7, "WHITE", "WHITE_QUEEN") + )); + } + + @Override + public void addAll(Connection notUsed, List pieceDtos, int gameId) { + data.put(gameId, new ArrayList<>(pieceDtos)); + } + + @Override + public List findAllPieces(Connection notUsed, int gameId) { + return new ArrayList<>(data.get(gameId)); + } +} diff --git a/src/test/java/dao/GameDaoImplTest.java b/src/test/java/dao/GameDaoImplTest.java new file mode 100644 index 00000000000..ffede788ec8 --- /dev/null +++ b/src/test/java/dao/GameDaoImplTest.java @@ -0,0 +1,52 @@ +package dao; + +import static org.assertj.core.api.Assertions.assertThat; + +import domain.game.TeamColor; +import java.sql.Connection; +import java.sql.SQLException; +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 GameDaoImplTest { + final GameDao gameDao = new GameDaoImpl(); + Connection connection; + + @BeforeEach + void setUp() throws SQLException { + connection = DBConnector.getInstance().getConnection(); + connection.setAutoCommit(false); + } + + @AfterEach + void tearDown() throws SQLException { + connection.rollback(); + connection.close(); + } + + @Test + @DisplayName("게임 정보를 추가하면 자동 생성된 게임 ID를 반환한다.") + void addGameTest() { + int gameId = gameDao.addGame(connection); + assertThat(gameId).isPositive(); + } + + @Test + @DisplayName("게임 ID로 저장된 차례를 조회한다.") + void findTurnTest() { + int gameId = gameDao.addGame(connection); + TeamColor turn = gameDao.findTurn(connection, gameId); + assertThat(turn).isEqualTo(TeamColor.WHITE); + } + + @Test + @DisplayName("저장된 게임에 대한 차례를 변경한다.") + void updateTurnTest() { + int gameId = gameDao.addGame(connection); + gameDao.updateTurn(connection, gameId, TeamColor.BLACK); + TeamColor turn = gameDao.findTurn(connection, gameId); + assertThat(turn).isEqualTo(TeamColor.BLACK); + } +} diff --git a/src/test/java/dao/PieceDaoImplTest.java b/src/test/java/dao/PieceDaoImplTest.java new file mode 100644 index 00000000000..d91515cb6ae --- /dev/null +++ b/src/test/java/dao/PieceDaoImplTest.java @@ -0,0 +1,48 @@ +package dao; + +import static org.assertj.core.api.Assertions.assertThat; +import static domain.Fixture.Positions.*; + +import domain.game.PieceFactory; +import domain.game.PieceType; +import dto.PieceDto; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +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 PieceDaoImplTest { + private final GameDao gameDao = new GameDaoImpl(); + private final PieceDao pieceDao = new PieceDaoImpl(); + private Connection connection; + + @BeforeEach + void setUp() throws SQLException { + connection = DBConnector.getInstance().getConnection(); + connection.setAutoCommit(false); + } + + @AfterEach + void tearDown() throws SQLException { + connection.rollback(); + connection.close(); + } + + @Test + @DisplayName("여러 Piece 를 한 번에 저장하고 불러올 수 있다.") + void addPieceBulkTest() { + List bulk = List.of( + PieceDto.of(A1, PieceFactory.create(PieceType.BLACK_PAWN)), + PieceDto.of(B2, PieceFactory.create(PieceType.WHITE_QUEEN)) + ); + int gameId = gameDao.addGame(connection); + pieceDao.addAll(connection, bulk, gameId); + + List result = pieceDao.findAllPieces(connection, gameId); + + assertThat(result).containsAll(bulk); + } +} diff --git a/src/test/java/domain/Fixture.java b/src/test/java/domain/Fixture.java index 0cc33304293..78b3b35e2e9 100644 --- a/src/test/java/domain/Fixture.java +++ b/src/test/java/domain/Fixture.java @@ -1,14 +1,12 @@ package domain; -import static domain.position.UnitVector.DOWN; -import static domain.position.UnitVector.DOWN_LEFT; -import static domain.position.UnitVector.DOWN_RIGHT; -import static domain.position.UnitVector.LEFT; -import static domain.position.UnitVector.RIGHT; -import static domain.position.UnitVector.UP; -import static domain.position.UnitVector.UP_LEFT; -import static domain.position.UnitVector.UP_RIGHT; +import static domain.Fixture.Pieces.*; +import static domain.Fixture.Positions.*; +import static domain.position.UnitVector.*; +import domain.game.Piece; +import domain.game.PieceFactory; +import domain.game.PieceType; import domain.game.TeamColor; import domain.position.File; import domain.position.Position; @@ -20,6 +18,7 @@ import domain.strategy.MoveStrategy; import domain.strategy.PawnMoveStrategy; import domain.strategy.WhitePawnMoveStrategy; +import java.util.Map; import java.util.Set; @SuppressWarnings("unused") @@ -108,6 +107,22 @@ public static class Vectors { public static final Set OMNIDIRECTIONAL_VECTORS = Set.of(UP, RIGHT, DOWN, LEFT, UP_RIGHT, DOWN_RIGHT, DOWN_LEFT, UP_LEFT); } + public static class Pieces { + public static final Piece BLACK_PAWN_PIECE = PieceFactory.create(PieceType.BLACK_PAWN); + public static final Piece BLACK_KNIGHT_PIECE = PieceFactory.create(PieceType.BLACK_KNIGHT); + public static final Piece BLACK_BISHOP_PIECE = PieceFactory.create(PieceType.BLACK_BISHOP); + public static final Piece BLACK_ROOK_PIECE = PieceFactory.create(PieceType.BLACK_ROOK); + public static final Piece BLACK_QUEEN_PIECE = PieceFactory.create(PieceType.BLACK_QUEEN); + public static final Piece BLACK_KING_PIECE = PieceFactory.create(PieceType.BLACK_KING); + public static final Piece WHITE_PAWN_PIECE = PieceFactory.create(PieceType.WHITE_PAWN); + public static final Piece WHITE_KNIGHT_PIECE = PieceFactory.create(PieceType.WHITE_KNIGHT); + public static final Piece WHITE_BISHOP_PIECE = PieceFactory.create(PieceType.WHITE_BISHOP); + public static final Piece WHITE_ROOK_PIECE = PieceFactory.create(PieceType.WHITE_ROOK); + public static final Piece WHITE_QUEEN_PIECE = PieceFactory.create(PieceType.WHITE_QUEEN); + public static final Piece WHITE_KING_PIECE = PieceFactory.create(PieceType.WHITE_KING); + + } + public static class Strategies { public static final MoveStrategy KING_MOVE_STRATEGY = new ContinuousMoveStrategy(Vectors.OMNIDIRECTIONAL_VECTORS, 1); public static final MoveStrategy QUEEN_MOVE_STRATEGY = new ContinuousMoveStrategy(Vectors.OMNIDIRECTIONAL_VECTORS, 8); @@ -124,4 +139,63 @@ public static PawnMoveStrategy pawnMoveStrategyOf(TeamColor teamColor) { } return new BlackPawnMoveStrategy(); } + + public static class PredefinedBoardsOfEachScore { + public static final Map BOARD_WHITE_20_5_BLACK_20 = Map.ofEntries( + Map.entry(A7, BLACK_PAWN_PIECE), + Map.entry(B6, BLACK_PAWN_PIECE), + Map.entry(B8, BLACK_KING_PIECE), + Map.entry(C7, BLACK_PAWN_PIECE), + Map.entry(C8, BLACK_ROOK_PIECE), + Map.entry(D7, BLACK_BISHOP_PIECE), + Map.entry(E6, BLACK_QUEEN_PIECE), + + Map.entry(E1, WHITE_ROOK_PIECE), + Map.entry(E3, WHITE_PAWN_PIECE), + Map.entry(F1, WHITE_KING_PIECE), + Map.entry(F2, WHITE_PAWN_PIECE), + Map.entry(F4, WHITE_KNIGHT_PIECE), + Map.entry(G2, WHITE_PAWN_PIECE), + Map.entry(G4, WHITE_QUEEN_PIECE), + Map.entry(H3, WHITE_PAWN_PIECE) + ); + + public static final Map BOARD_WHITE_19_5_BLACK_20 = Map.ofEntries( + Map.entry(E1, WHITE_ROOK_PIECE), + Map.entry(F1, WHITE_KING_PIECE), + Map.entry(F2, WHITE_PAWN_PIECE), // 0.5 + Map.entry(F3, WHITE_PAWN_PIECE), // 0.5 + Map.entry(F4, WHITE_KNIGHT_PIECE), + Map.entry(G2, WHITE_PAWN_PIECE), + Map.entry(G4, WHITE_QUEEN_PIECE), + Map.entry(H3, WHITE_PAWN_PIECE), + Map.entry(A7, BLACK_PAWN_PIECE), + Map.entry(B6, BLACK_PAWN_PIECE), + Map.entry(B8, BLACK_KING_PIECE), + Map.entry(C7, BLACK_PAWN_PIECE), + Map.entry(C8, BLACK_ROOK_PIECE), + Map.entry(D7, BLACK_BISHOP_PIECE), + Map.entry(E6, BLACK_QUEEN_PIECE) + ); + + public static final Map BOARD_WHITE_2_BLACK_2_5 = Map.ofEntries( + Map.entry(A2, WHITE_PAWN_PIECE), // 0.5 + Map.entry(A3, WHITE_PAWN_PIECE), // 0.5 + Map.entry(C2, WHITE_PAWN_PIECE), + Map.entry(D7, BLACK_PAWN_PIECE), // 0.5 + Map.entry(D6, BLACK_PAWN_PIECE), // 0.5 + Map.entry(D5, BLACK_PAWN_PIECE), // 0.5 + Map.entry(C7, BLACK_PAWN_PIECE) + ); + + public static final Map BOARD_WHITE_0_BLACK_3_5 = Map.ofEntries( + Map.entry(A2, BLACK_PAWN_PIECE), + Map.entry(A3, BLACK_PAWN_PIECE), + Map.entry(A4, BLACK_PAWN_PIECE), + Map.entry(A5, BLACK_PAWN_PIECE), + Map.entry(A6, BLACK_PAWN_PIECE), + Map.entry(A7, BLACK_PAWN_PIECE), + Map.entry(A8, BLACK_PAWN_PIECE) + ); + } } diff --git a/src/test/java/domain/game/BoardTest.java b/src/test/java/domain/game/BoardTest.java index 16727e18951..1b674d115c7 100644 --- a/src/test/java/domain/game/BoardTest.java +++ b/src/test/java/domain/game/BoardTest.java @@ -1,18 +1,24 @@ package domain.game; +import static domain.Fixture.Positions.*; +import static domain.Fixture.PredefinedBoardsOfEachScore.*; +import static domain.game.TeamColor.*; +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 domain.position.File; import domain.position.Position; import domain.position.Rank; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - import java.util.HashMap; import java.util.Map; - -import static domain.Fixture.Positions.*; -import static domain.game.TeamColor.WHITE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class BoardTest { @DisplayName("기물의 시작 위치를 배치한 Board 인스턴스를 생성한다.") @@ -25,61 +31,129 @@ void createBoard() { assertThat(board).isNotNull(); } - @DisplayName("기물의 이동 목적지가 비어있으면 이동시킬 수 있다.") - @Test - void movePieceToEmptySpaceTest() { - // Given - Piece piece = PieceFactory.create(PieceType.WHITE_BISHOP); - Position source = B2; - Position destination = Position.of(File.D, Rank.FOUR); - Map piecePositions = new HashMap<>(Map.of(source, piece)); - Board board = new Board(piecePositions); + @DisplayName("각 기물의 전략에 따라 기물을 이동한다.") + @Nested + class MovePieceTest { + @DisplayName("기물의 이동 목적지가 비어있으면 이동시킬 수 있다.") + @Test + void movePieceToEmptySpaceTest() { + // Given + Piece piece = PieceFactory.create(PieceType.WHITE_BISHOP); + Position source = B2; + Position destination = Position.of(File.D, Rank.FOUR); + Map piecePositions = new HashMap<>(Map.of(source, piece)); + Board board = new Board(piecePositions); - // When - board.movePiece(WHITE, source, destination); + // When + board.movePiece(WHITE, source, destination); - // Then - assertThat(piecePositions).doesNotContainKey(source).containsKey(destination); - } + // Then + assertThat(piecePositions).doesNotContainKey(source).containsKey(destination); + } - @DisplayName("기물의 이동 목적지에 다른 색의 기물이 있으면 이동시킬 수 있다.") - @Test - void movePieceToEnemySpaceTest() { - // Given - Piece piece = PieceFactory.create(PieceType.WHITE_KNIGHT); - Piece enemy = PieceFactory.create(PieceType.BLACK_KING); - Position source = B2; - Position destination = C4; - Map piecePositions = new HashMap<>(Map.of( - source, piece, - destination, enemy - )); - Board board = new Board(piecePositions); + @DisplayName("기물의 이동 목적지에 다른 색의 기물이 있으면 이동시킬 수 있다.") + @Test + void movePieceToEnemySpaceTest() { + // Given + Piece piece = PieceFactory.create(PieceType.WHITE_KNIGHT); + Piece enemy = PieceFactory.create(PieceType.BLACK_KING); + Position source = B2; + Position destination = C4; + Map piecePositions = new HashMap<>(Map.of( + source, piece, + destination, enemy + )); + Board board = new Board(piecePositions); - // When - board.movePiece(WHITE, source, destination); + // When + board.movePiece(WHITE, source, destination); - // Then - assertThat(piecePositions).doesNotContainKey(source).containsKey(destination); + // Then + assertThat(piecePositions).doesNotContainKey(source).containsKey(destination); + } + + @DisplayName("기물의 이동 목적지에 같은 색의 기물이 있으면 이동시킬 수 없다.") + @Test + void notMovePieceTest() { + // Given + Piece piece = PieceFactory.create(PieceType.WHITE_KNIGHT); + Piece other = PieceFactory.create(PieceType.WHITE_ROOK); + Position source = B2; + Position destination = C4; + Map piecePositions = new HashMap<>(Map.of( + source, piece, + destination, other + )); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() -> board.movePiece(WHITE, source, destination)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동 위치에 아군 기물이 존재합니다."); + } } - @DisplayName("기물의 이동 목적지에 같은 색의 기물이 있으면 이동시킬 수 없다.") - @Test - void notMovePieceTest() { - // Given - Piece piece = PieceFactory.create(PieceType.WHITE_KNIGHT); - Piece other = PieceFactory.create(PieceType.WHITE_ROOK); - Position source = B2; - Position destination = C4; - Map piecePositions = new HashMap<>(Map.of( - source, piece, - destination, other - )); - Board board = new Board(piecePositions); - - // When & Then - assertThatThrownBy(() -> board.movePiece(WHITE, source, destination)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("이동 위치에 아군 기물이 존재합니다."); + @DisplayName("점수 계산 테스트") + @Nested + class CalculateScoreTest { + @Test + @DisplayName("초기 상태의 점수는 흑/백 모두 38점이다.") + void initialScoreTest() { + // Given + Board board = BoardInitializer.init(); + + // When + double whiteScore = board.calculateScoreOf(WHITE); + double blackScore = board.calculateScoreOf(BLACK); + + // Then + double expected = 9 + 2 * (5 + 3 + 2.5) + (1 * 8); + assertThat(whiteScore).isEqualTo(blackScore).isEqualTo(expected); + } + + @Test + @DisplayName("각 폰이 모두 각자의 세로줄에 존재하는 경우, 모든 기물들은 각자의 평가치에 맞게 평가된다.") + void calculateScoreTest() { + // Given + Board board = new Board(BOARD_WHITE_20_5_BLACK_20); + + // When + double whiteScore = board.calculateScoreOf(WHITE); + double blackScore = board.calculateScoreOf(BLACK); + + // Then + double expectedWhiteScore = 5 + 2.5 + 9 + (1 * 4); // 20.5 + double expectedBlackScore = 5 + 3 + 9 + (1 * 3) + 0; // 20 + assertAll( + () -> assertThat(whiteScore).isEqualTo(expectedWhiteScore), + () -> assertThat(blackScore).isEqualTo(expectedBlackScore) + ); + } + + @ParameterizedTest + @MethodSource("pawnOnSameFileCase") + @DisplayName("같은 세로줄에 같은 색의 폰이 존재하는 경우, 해당 폰은 원래 평가치의 절반으로 평가된다.") + void pawnOnSameFileTest(Map piecePositions, double expectedWhiteScore, double expectedBlackScore) { + // Given + Board board = new Board(piecePositions); + + // When + double whiteScore = board.calculateScoreOf(WHITE); + double blackScore = board.calculateScoreOf(BLACK); + + // Then + assertAll( + () -> assertThat(whiteScore).isEqualTo(expectedWhiteScore), + () -> assertThat(blackScore).isEqualTo(expectedBlackScore) + ); + } + + static Stream pawnOnSameFileCase() { + return Stream.of( + Arguments.of(BOARD_WHITE_19_5_BLACK_20, 19.5, 20), + Arguments.of(BOARD_WHITE_2_BLACK_2_5, 2, 2.5), + Arguments.of(BOARD_WHITE_0_BLACK_3_5, 0, 3.5) + ); + } } } diff --git a/src/test/java/domain/game/ChessGameTest.java b/src/test/java/domain/game/ChessGameTest.java new file mode 100644 index 00000000000..d86d43d4263 --- /dev/null +++ b/src/test/java/domain/game/ChessGameTest.java @@ -0,0 +1,116 @@ +package domain.game; + +import static domain.Fixture.Pieces.*; +import static domain.Fixture.Positions.*; +import static domain.Fixture.PredefinedBoardsOfEachScore.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +import domain.position.Position; +import java.util.HashMap; +import java.util.Map; +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 ChessGameTest { + @Test + @DisplayName("게임은 흰색 팀의 차례로 시작한다.") + void initialTeamEqualsWhiteTest() { + // Given + ChessGame chessGame = new ChessGame(); + + // When + TeamColor currentTeam = chessGame.currentPlayingTeam(); + + // Then + assertThat(currentTeam).isEqualTo(TeamColor.WHITE); + } + + @Test + @DisplayName("한쪽 팀이 기물을 이동하면 다음 팀으로 차례가 넘어간다.") + void toggleTeamTest() { + // Given + ChessGame chessGame = new ChessGame(); + + // When + chessGame.move(A2, A4); + + // Then + assertThat(chessGame.currentPlayingTeam()).isEqualTo(TeamColor.BLACK); + } + + @Test + @DisplayName("게임이 진행중인 상태에서는 전달받은 출발지와 목적지에 따라 말을 이동시킨다.") + void movePieceTest() { + // Given + ChessGame chessGame = new ChessGame(); + + // When + Position source = A2; + Position destination = A4; + + // Then + assertThatCode(() -> chessGame.move(source, destination)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @MethodSource("kingCaughtCase") + @DisplayName("한 팀의 킹이 잡히면 게임을 종료하고 승자를 판단할 수 있다.") + void gameEndTest(Map piecePositions, Position source, Position destination, TeamColor expectedWinner) { + // Given + Board board = new Board(new HashMap<>(piecePositions)); + TestableChessGame chessGame = piecePositions.get(source).hasColor(TeamColor.WHITE) + ? TestableChessGame.whiteTurnOf(board) + : TestableChessGame.blackTurnOf(board); + + // When + chessGame.move(source, destination); + + // Then + assertAll( + () -> assertThat(chessGame.isGameEnd()).isTrue(), + () -> assertThat(chessGame.getWinner()).isEqualTo(expectedWinner) + ); + } + + static Stream kingCaughtCase() { + return Stream.of( + Arguments.of(Map.of(D4, BLACK_KING_PIECE, D3, WHITE_QUEEN_PIECE), D3, D4, TeamColor.WHITE), + Arguments.of(Map.of(D4, WHITE_KING_PIECE, D3, BLACK_QUEEN_PIECE), D3, D4, TeamColor.BLACK) + ); + } + + @ParameterizedTest + @MethodSource("specificScoreCase") + @DisplayName("각 팀별 현재 점수를 계산할 수 있다.") + void calculateScoreOfEachTeamTest(Map piecePositions, double expectedWhiteScore, double expectedBlackScore) { + // Given + Board board = new Board(piecePositions); + TestableChessGame chessGame = new TestableChessGame(null, board); + + // When + double whiteScore = chessGame.currentScoreOf(TeamColor.WHITE); + double blackScore = chessGame.currentScoreOf(TeamColor.BLACK); + + // Then + assertAll( + () -> assertThat(whiteScore).isEqualTo(expectedWhiteScore), + () -> assertThat(blackScore).isEqualTo(expectedBlackScore) + ); + } + + static Stream specificScoreCase() { + return Stream.of( + Arguments.of(BOARD_WHITE_19_5_BLACK_20, 19.5, 20), + Arguments.of(BOARD_WHITE_20_5_BLACK_20, 20.5, 20), + Arguments.of(BOARD_WHITE_2_BLACK_2_5, 2, 2.5), + Arguments.of(BOARD_WHITE_0_BLACK_3_5, 0, 3.5) + ); + } +} diff --git a/src/test/java/domain/game/GameRequestTest.java b/src/test/java/domain/game/GameRequestTest.java new file mode 100644 index 00000000000..d7a8c71e5d0 --- /dev/null +++ b/src/test/java/domain/game/GameRequestTest.java @@ -0,0 +1,87 @@ +package domain.game; + +import static domain.Fixture.Positions.B3; +import static domain.Fixture.Positions.E1; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import domain.GameCommand; +import domain.position.Position; +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 GameRequestTest { + @Test + @DisplayName("인자 없이 타입만 가지는 명령을 생성할 수 있다.") + void noArgumentTest() { + assertThatCode(() -> GameRequest.ofNoArgument(GameCommand.START)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("MOVE 타입의 명령인 경우 두 개의 Position 인자를 가질 수 있다.") + void moveRequestArgumentTest() { + List positions = List.of(E1, B3); + GameRequest gameRequest = new GameRequest(GameCommand.MOVE, positions); + + assertAll( + () -> assertThat(gameRequest.source()).isEqualTo(E1), + () -> assertThat(gameRequest.destination()).isEqualTo(B3) + ); + } + + @ParameterizedTest + @MethodSource("nonMoveCommandCase") + @DisplayName("MOVE 타입의 명령이 아닌 경우 인자를 가져오면 예외가 발생한다.") + void nonMoveRequestArgumentTest(GameCommand gameCommand) { + List positions = List.of(E1, B3); + GameRequest gameRequest = new GameRequest(gameCommand, positions); + + assertAll( + () -> assertThatThrownBy(gameRequest::source) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("유효하지 않은 커멘드 타입입니다."), + () -> assertThatThrownBy(gameRequest::destination) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("유효하지 않은 커멘드 타입입니다.") + ); + } + + static Stream nonMoveCommandCase() { + return Stream.of( + Arguments.of(GameCommand.START), + Arguments.of(GameCommand.STATUS), + Arguments.of(GameCommand.END) + ); + } + + @ParameterizedTest + @MethodSource("allCommandCase") + @DisplayName("GameCommand 의 각 속성 값을 포워딩한다.") + void methodForwardTest(GameCommand gameCommand) { + GameRequest gameRequest = GameRequest.ofNoArgument(gameCommand); + + assertAll( + () -> assertThat(gameRequest.isContinuable()).isEqualTo(gameCommand.isContinuable()), + () -> assertThat(gameRequest.isStart()).isEqualTo(gameCommand.isStart()), + () -> assertThat(gameRequest.isStatus()).isEqualTo(gameCommand.isStatus()), + () -> assertThat(gameRequest.isEnd()).isEqualTo(gameCommand.isEnd()) + ); + } + + static Stream allCommandCase() { + return Stream.of( + Arguments.of(GameCommand.START), + Arguments.of(GameCommand.MOVE), + Arguments.of(GameCommand.STATUS), + Arguments.of(GameCommand.END) + ); + } +} diff --git a/src/test/java/domain/game/TestableChessGame.java b/src/test/java/domain/game/TestableChessGame.java new file mode 100644 index 00000000000..b1f17fddc82 --- /dev/null +++ b/src/test/java/domain/game/TestableChessGame.java @@ -0,0 +1,19 @@ +package domain.game; + +import domain.game.state.BlackTurn; +import domain.game.state.GameState; +import domain.game.state.WhiteTurn; + +public class TestableChessGame extends ChessGame { + public TestableChessGame(GameState state, Board board) { + super(state, board); + } + + public static TestableChessGame whiteTurnOf(Board board) { + return new TestableChessGame(WhiteTurn.getInstance(), board); + } + + public static TestableChessGame blackTurnOf(Board board) { + return new TestableChessGame(BlackTurn.getInstance(), board); + } +} diff --git a/src/test/java/domain/game/TurnTest.java b/src/test/java/domain/game/TurnTest.java deleted file mode 100644 index 795f3dbc28d..00000000000 --- a/src/test/java/domain/game/TurnTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package domain.game; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class TurnTest { - @Test - @DisplayName("초기 턴은 흰색 팀부터 시작한다.") - void initialTurnTest() { - Turn turn = new Turn(); - assertThat(turn.current()).isEqualTo(TeamColor.WHITE); - } - - @Test - @DisplayName("턴을 토글할 수 있다.") - void nextTurnTest() { - // Given - Turn turn = new Turn(); - - // When - turn.next(); - - // Then - assertThat(turn.current()).isEqualTo(TeamColor.BLACK); - } -} diff --git a/src/test/java/service/DBServiceTest.java b/src/test/java/service/DBServiceTest.java new file mode 100644 index 00000000000..df5b161173c --- /dev/null +++ b/src/test/java/service/DBServiceTest.java @@ -0,0 +1,56 @@ +package service; + +import static domain.Fixture.Positions.F5; +import static domain.Fixture.PredefinedBoardsOfEachScore.BOARD_WHITE_19_5_BLACK_20; +import static domain.game.PieceType.WHITE_PAWN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import dao.FakeGameDao; +import dao.FakePieceDao; +import domain.game.ChessGame; +import domain.game.TeamColor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DBServiceTest { + private TestableDBService dbService; + + @BeforeEach + void setUp() { + dbService = new TestableDBService(new FakeGameDao(), new FakePieceDao()); + } + + @Test + @DisplayName("게임의 현재 상태를 불러올 수 있다.") + void loadGameTest() { + // Given + ChessGame chessGameId1 = dbService.loadGame(1); + ChessGame chessGameId2 = dbService.loadGame(2); + + // Then + assertAll( + () -> assertThat(chessGameId1.currentPlayingTeam()).isEqualTo(TeamColor.BLACK), + () -> assertThat(chessGameId2.currentPlayingTeam()).isEqualTo(TeamColor.WHITE), + () -> assertThat(chessGameId1.getPositionsOfPieces().get(F5).getPieceType()).isEqualTo(WHITE_PAWN) + ); + } + + @Test + @DisplayName("게임을 저장하고 불러올 수 있다.") + void saveGameTest() { + // Given + ChessGame chessGame = ChessGame.of(TeamColor.BLACK, BOARD_WHITE_19_5_BLACK_20); + + // When + int gameId = dbService.saveGame(chessGame); + + // Then + assertAll( + () -> assertThat(dbService.loadGame(gameId).currentPlayingTeam()).isEqualTo(TeamColor.BLACK), + () -> assertThat(dbService.loadGame(gameId).currentScoreOf(TeamColor.WHITE)).isEqualTo(19.5), + () -> assertThat(dbService.loadGame(gameId).currentScoreOf(TeamColor.BLACK)).isEqualTo(20) + ); + } +} diff --git a/src/test/java/service/TestableDBService.java b/src/test/java/service/TestableDBService.java new file mode 100644 index 00000000000..12105135280 --- /dev/null +++ b/src/test/java/service/TestableDBService.java @@ -0,0 +1,10 @@ +package service; + +import dao.GameDao; +import dao.PieceDao; + +public class TestableDBService extends DBService { + public TestableDBService(GameDao gameDao, PieceDao pieceDao) { + super(gameDao, pieceDao); + } +}