diff --git a/README.md b/README.md index aa00cd76fe3..cb4d07f43f8 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,100 @@ ## ✔ 기능 요구사항 +## ✨ Controller + +### Controller + +- [x] 사용자가 입력한 명령어에 따라 게임의 상태를 확인한다. +- [x] 현재 게임이 실행 중인지 판단한다. + +### EndController + +- [x] 사용자가 입력한 명령어가 End가 아니라면 예외를 발생시킨다. +- [x] 현재 게임이 실행 중인지 판단한다. + +### MoveController + +- [x] 사용자가 입력한 명령어에 따라서 상태를 변경한다. + - [x] 사용자가 입력한 명령어가 Start라면 예외를 발생시키고, End라면 종료한다. + - [x] 사용자가 입력한 status거나 모든 킹이 살아있는 게 아니라면 상태를 status로 변경한다. +- [x] 사용자가 입력한 체스말을 시작 지점에서 끝 지점으로 이동시키고, 차례를 변경한다. +- [x] 현재 게임이 실행 중인지 판단한다. + +### StartController + +- [x] 사용자가 입력한 명령어가 move/status라면 예외를 발생시키고, End라면 종료한다. +- [x] 현재 게임이 실행 중인지 판단한다. + +### StatusController + +- [x] 사용자가 입력한 명령어가 start라면 예외를 발생시키고, move면 상태를 move로 변경하고, end라면 종료한다. +- [x] status일 경우 각 진영의 체스판 점수를 계산한다. +- [x] 현재 게임이 실행 중인지 판단한다. +- [x] 현재 살아있는 킹이 없다면 종료 메시지를 출력하고, end로 변경한다. + +### ChessHandler + +- [x] 체스 게임에 대한 전반적인 실행을 담당한다. + +### Command + +- [x] 사용자가 입력한 명령어에 대해서 관리한다. +- [x] 사용자가 입력한 명령어가 start, move, end인지 검증한다. +- [x] 사용자가 입력한 명령어가 start인지 확인한다. +- [x] 사용자가 입력한 명령어가 end인지 확인한다. +- [x] 사용자가 입력한 명령어가 status인지 확인한다. +- [x] 사용자가 입력한 명령어가 move일 때 올바른 명령어 길이로 들어오는지 확인한다. + +### CommandType + +- [x] 명령어의 종류에 대해서 관리한다. (start, move, end, status) + +--- + +## ✨dao + +### RowMapper + +- [x] DB에서 조회한 결과에 대해 각 테이블에 맞는 엔티티로 매핑시킨다. + +### UserDaoImpl + +- [x] 사용자의 이름을 기준으로 사용자 엔티티를 조회한다. +- [x] 사용자의 정보를 삽입한다. + +### ChessGameDaoImpl + +- [x] 사용자의 아이디를 기준으로 체스 게임을 조회한다. +- [x] 사용자가 현재 진행 중인 체스 게임을 저장한다. +- [x] 체스 게임 아이디에 해당하는 게임의 현재 진영을 업데이트한다. +- [x] 사용자의 아이디에 해당하는 체스 게임을 제거한다. + +### PieceDaoImpl + +- [x] 체스 게임 아이디를 기준으로 체스말 리스트의 정보를 조회한다. +- [x] 입력으로 들어온 체스 게임에 대한 체스말의 정보를 저장한다. +- [x] 입력으로 들어온 체스 게임에 대해 특정 위치에 존재하는 체스말의 정보를 제거한다. +- [x] 체스 게임 아이디에 해당하는 체스말 정보를 제거한다 + +--- + +## ✨database + +### ChessProperties + +- [x] DB 연결에 필요한 프로퍼티 정보를 관리한다. + +### JdbcTemplate + +- [x] 쿼리에 따라 테이블에서 일치하는 결과를 1개만 조회하여 매핑된 결과를 반환한다. +- [x] 쿼리에 따라 테이블에서 일치하는 결과 전부를 조회하여 매핑된 결과를 반환한다. +- [x] 쿼리에 따라 테이블에 업데이트 연산 (삽입, 수정, 삭제)을 진행한다. + +--- + +## ✨domain - board + ### ChessBoard - [x] 체스판을 초기화한다. @@ -13,10 +107,27 @@ - [x] 특정 위치에 존재하는 체스말을 제거한다. - [x] 특정 위치에 체스말을 둔다. - [x] 특정 말의 종류에 따라서 시작 위치에서 종료 위치로 이동 가능한지 판단한다. +- [x] 현재 체스판에서 살아있는 킹들을 조회한다. +- [x] 입력받은 진영의 체스판을 반환한다. + +### ChessBoardFactory + +- [x] 체스판의 초기 상태에 대해서 초기화한다. + +--- + +## ✨domain - chess + +### ScoreVO + +- [x] 각 진영의 체스 결과에 대한 점수를 담는다. ### ChessGame - [x] 체스 게임을 진행한다. +- [x] 현재 체스 게임에서 킹이 살아있는지 판단한다. +- [x] WHITE 진영의 체스판을 반환한다. +- [x] BLACK 진영의 체스판을 반환한다. ### CampType @@ -24,18 +135,26 @@ - [x] 입력받은 위치의 열이 소문자면 WHITE, 대문자면 BLACK 진영으로 나눈다. - [x] 플레이할 진영을 번갈아가며 반환한다. -### Piece +### ChessScoreCalculator -- [x] 체스말이 어떤 진영에 속하는지 관리한다. -- [x] 두 개의 체스말이 동일한 진영에 속하는지 판단한다. -- [x] 체스말이 입력받은 진영에 속하는지 판단한다. -- [x] 체스말이 시작 위치에서 도착 위치로 이동 가능한지 판단한다. -- [x] 체스말이 공격 가능한지 판단한다. +- [x] 현재 체스판의 각 진영에 대한 점수를 계산한다. -### PieceType +--- -- [x] 체스말의 종류를 관리한다. -- [x] 특정 체스말이 시작 위치에서 종료 위치까지 이동 가능한지 판단한다. +## ✨domain - piece + +### Direction + +- [x] 체스말이 이동하는 방향으로 관리한다. +- [x] 모든 방향에 대해서 반환한다. +- [x] 상하좌우에 대해서 반환한다. +- [x] 상하좌우의 대각선에 대해서 반환한다. + +### Location + +- [x] 이동 가능한 모든 위치에 대해 관리한다. +- [x] 이동 가능한 위치에 새로운 정보를 추가한다. +- [x] 이동 가능한 위치에 입력받은 위치가 포함되어 있는지 확인한다. ### Position @@ -50,107 +169,139 @@ - [x] 현재 위치의 rank과 입력받은 위치의 rank가 동일한지 판단한다. - [x] 현재 위치와 입력받은 위치가 동일한지 판단한다. -### Direction +### PositionConverter -- [x] 체스말이 이동하는 방향으로 관리한다. -- [x] 모든 방향에 대해서 반환한다. -- [x] 상하좌우에 대해서 반환한다. -- [x] 상하좌우의 대각선에 대해서 반환한다. +- [x] 입력받은 source, target 위치를 가로, 세로 값으로 분리한다. + - [x] 가로값은 왼쪽부터 0~7 세로값은 아래부터 0~7으로 변환한다. +- [x] 사용자가 입력한 위치를 검증한다. + - [x] 입력받은 위치 명령어의 길이가 2인지 확인한다. + - [x] 첫 번째 글자가 a~h, 두 번째 글자가 1~8인지 확인한다. -### Movable +### Score -- [x] 체스말의 움직임을 관리한다. +- [x] 점수에 대해서 관리한다. +- [x] 인자로 들어온 점수를 합한 값을 반환한다. +- [x] 인자로 들어온 점수를 뺀 값을 반환한다. +- [x] 인자로 들어온 횟수만큼 곱한 값을 반환한다. + +### Piece + +- [x] 체스말이 어떤 진영에 속하는지 관리한다. +- [x] 두 개의 체스말이 동일한 진영에 속하는지 판단한다. +- [x] 체스말이 입력받은 진영에 속하는지 판단한다. +- [x] 체스말이 시작 위치에서 도착 위치로 이동 가능한지 판단한다. - [x] 체스말이 공격 가능한지 판단한다. +- [x] 체스말이 킹인지 판단한다. +- [x] 체스말이 폰인지 판단한다. -### Location +### PieceType -- [x] 이동 가능한 모든 위치에 대해 관리한다. -- [x] 이동 가능한 위치에 새로운 정보를 추가한다. -- [x] 이동 가능한 위치에 입력받은 위치가 포함되어 있는지 확인한다. +- [x] 체스말의 종류를 관리한다. +- [x] 특정 체스말이 시작 위치에서 종료 위치까지 이동 가능한지 판단한다. +- [x] 각 체스말의 점수에 대해 관리한다. + +### Movable + +- [x] 체스말의 움직임을 관리한다. +- [x] 체스말이 공격 가능한지 판단한다. ### Move - [x] 입력받은 위치로 이동시킨다. - [x] 체스말이 이동 가능한 위치를 전부 반환한다. -### QueenMove +### Queen - [x] 입력받은 위치에 대해서 퀸이 이동 가능한지 판단한다. -### RookMove +### Rook - [x] 입력받은 위치에 대해서 룩이 이동 가능한지 판단한다. -### BishopMove +### Bishop - [x] 입력받은 위치에 대해서 비숍이 이동 가능한지 판단한다. -### KingMove +### King - [x] 입력받은 위치에 대해서 킹이 이동 가능한지 판단한다. -### KnightMove +### Knight - [x] 입력받은 위치에 대해서 나이트가 이동 가능한지 판단한다. -### PawnMove +### Pawn - [x] 입력받은 출발 위치의 행보다 도착 위치의 행이 더 크면 UP 방향, 아니면 DOWN 방향으로 이동할 수 있는지 판단한다. -### PositionConverter +--- -- [x] 입력받은 source, target 위치를 가로, 세로 값으로 분리한다. - - [x] 가로값은 왼쪽부터 0~7 세로값은 아래부터 0~7으로 변환한다. -- [x] 사용자가 입력한 위치를 검증한다. - - [x] 입력받은 위치 명령어의 길이가 2인지 확인한다. - - [x] 첫 번째 글자가 a~h, 두 번째 글자가 1~8인지 확인한다. +## ✨domain - user -### InputView +### User -- [x] 명령어를 입력받는다. +- [x] 사용자의 이름이 20자 초과라면 예외를 발생시킨다. -### Command +--- -- [x] 사용자가 입력한 명령어에 대해서 관리한다. -- [x] 사용자가 입력한 명령어가 start, move, end인지 검증한다. -- [x] 사용자가 입력한 명령어가 start인지 확인한다. -- [x] 사용자가 입력한 명령어가 end인지 확인한다. -- [x] 사용자가 입력한 명령어가 move일 때 올바른 명령어 길이로 들어오는지 확인한다. +## ✨domain - entity -### CommandType +### UserEntity -- [x] 명령어의 종류에 대해서 관리한다. (start, move, end) +- [x] user (사용자) 테이블에서 조회한 결과에 대해 담는다. -### Status +### ChessGameEntity -- [x] 사용자가 입력한 명령어에 따라 게임의 상태를 확인한다. -- [x] 현재 게임이 실행 중인지 판단한다. +- [x] chess_game (체스 게임) 테이블에서 조회한 결과에 대해 담는다. -### EndController +--- -- [x] 사용자가 입력한 명령어가 End가 아니라면 예외를 발생시킨다. -- [x] 현재 게임이 실행 중인지 판단한다. +## ✨domain - service -### MoveController +### UserService -- [x] 사용자가 입력한 명령어가 Start라면 예외를 발생시키고, End라면 종료한다. -- [x] 사용자가 입력한 체스말을 시작 지점에서 끝 지점으로 이동시키고, 차례를 변경한다. -- [x] 현재 게임이 실행 중인지 판단한다. +- [x] 사용자가 입력한 이름이 이미 존재한다면 해당 사용자의 아이디를, 아니라면 새롭게 삽입 후 생성된 아이디를 반환한다. +- [x] 사용자의 이름을 바탕으로 사용자의 정보를 삽입한다. -### StartController +### ChessGameService -- [x] 사용자가 입력한 명령어가 Move라면 예외를 발생시키고, End라면 종료한다. -- [x] 현재 게임이 실행 중인지 판단한다. +- [x] 사용자의 아이디에 해당하는 체스 게임이 존재하면 해당 게임을 반환하고, 아니라면 새 게임을 반환한다. +- [x] 사용자의 아이디를 기준으로 체스 게임을 조회한다. +- [x] 체스말 정보를 바탕으로 체스말을 저장한다. +- [x] 체스 게임 아이디에 해당하는 진영 정보를 업데이트한다. +- [x] 입력받은 위치에 해당하는 체스말을 제거한다. +- [x] 사용자의 아이디에 해당하는 체스 기물과 체스판 정보를 제거한다. -### PieceName +### ChessBoardService -- [x] 체스말의 종류에 따라 체스말의 이름으로 변환한다. +- [x] 체스 게임 아이디를 기준으로 체스판 정보를 조회한다. +- [x] 체스말 정보를 바탕으로 체스말을 저장한다. +- [x] 입력받은 위치에 해당하는 체스말을 제거한다. +- [x] 체스판의 모든 기물을 저장한다. +- [x] 체스 게임 아이디에 해당하는 모든 기물을 제거한다. + +### ServiceManager + +- [x] 서비스의 생성을 총괄한다. + +--- + +## ✨domain - view + +### InputView + +- [x] 명령어를 입력받는다. ### OutputView +- [x] 사용자에게 이름을 입력받는 메시지를 출력한다. - [x] 게임 안내 메시지를 출력한다. - [x] 체스판에 있는 체스말을 출력하고, 비어있는 곳은 .으로 출력한다. -- source에 있는 말이 target으로 이동된 체스판을 출력한다. +- [x] 각 진영의 점수와 이긴 진영에 대해서 출력한다. + +### PieceName + +- [x] 체스말의 종류에 따라 체스말의 이름으로 변환한다. --- diff --git a/build.gradle b/build.gradle index a0a012f5798..6afbd4f57c5 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,8 @@ repositories { } dependencies { + runtimeOnly 'mysql:mysql-connector-java:8.0.28' + implementation 'org.yaml:snakeyaml:2.0' testImplementation 'org.assertj:assertj-core:3.22.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' } diff --git a/src/main/java/chess/ChessApplication.java b/src/main/java/chess/ChessApplication.java index b33c92c9651..67a98d40d7f 100644 --- a/src/main/java/chess/ChessApplication.java +++ b/src/main/java/chess/ChessApplication.java @@ -1,16 +1,15 @@ package chess; import chess.controller.ChessHandler; -import chess.service.ChessBoardService; -import chess.service.PieceService; -import chess.service.PositionService; +import chess.database.JdbcTemplate; +import chess.database.properties.ChessProperties; +import chess.service.ServiceManager; public final class ChessApplication { public static void main(String[] args) { - final PieceService pieceService = new PieceService(); - final PositionService positionService = new PositionService(); - final ChessBoardService chessBoardService = new ChessBoardService(pieceService, positionService); - final ChessHandler chessHandler = new ChessHandler(chessBoardService); + final ChessProperties chessProperties = new ChessProperties(); + final ServiceManager serviceManager = new ServiceManager(new JdbcTemplate(chessProperties)); + final ChessHandler chessHandler = new ChessHandler(serviceManager); chessHandler.run(); } } diff --git a/src/main/java/chess/controller/ChessHandler.java b/src/main/java/chess/controller/ChessHandler.java index 36fbfee4ecb..f1b4d1a2d52 100644 --- a/src/main/java/chess/controller/ChessHandler.java +++ b/src/main/java/chess/controller/ChessHandler.java @@ -1,47 +1,65 @@ package chess.controller; +import chess.controller.mapper.ChessBoardDtoMapper; import chess.controller.status.Controller; import chess.controller.status.StartController; import chess.domain.chess.ChessGame; -import chess.service.ChessBoardService; +import chess.service.ChessGameService; +import chess.service.ServiceManager; +import chess.service.UserService; import chess.view.InputView; import chess.view.OutputView; import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; public final class ChessHandler { - private final ChessBoardService chessBoardService; + private final UserService userService; + private final ChessGameService chessGameService; - public ChessHandler(final ChessBoardService chessBoardService) { - this.chessBoardService = chessBoardService; + public ChessHandler(final ServiceManager serviceManager) { + this.userService = serviceManager.userService(); + this.chessGameService = serviceManager.chessGameService(); } public void run() { + final long userId = getUserId(); OutputView.printStartMessage(); - final ChessGame chessGame = new ChessGame(); - start(chessGame); - } - - private void start(final ChessGame chessGame) { - Controller controller = new StartController(chessGame); + Controller controller = new StartController(userId, chessGameService); while (controller.isRun()) { - controller = play(chessGame, controller); + controller = play(controller); } } - private Controller play(final ChessGame chessGame, Controller controller) { + private Controller play(Controller controller) { try { - List commands = InputView.getCommand(); + final List commands = InputView.getCommands(); final Command command = Command.findCommand(commands); - controller = controller.checkCommand(command, - () -> OutputView.printBoard( - chessBoardService.createChessBoardDto(chessGame.getChessBoard()) - )); + controller = controller.checkCommand(command); + final Optional chessGame = controller.findGame(); + chessGame.ifPresent(game -> + OutputView.printBoard(ChessBoardDtoMapper.createChessBoardDto(game.getChessBoard()))); return controller; } catch (IllegalArgumentException e) { OutputView.print(e.getMessage()); - return play(chessGame, controller); + return play(controller); + } + } + + private long getUserId() { + OutputView.printUserNameInputMessage(); + return getUserIdWithRetry(InputView::getCommand); + } + + private long getUserIdWithRetry(final Supplier supplier) { + try { + final String name = supplier.get(); + return userService.getOrCreateUserId(name); + } catch (IllegalArgumentException e) { + OutputView.print(e.getMessage()); + return getUserIdWithRetry(supplier); } } } diff --git a/src/main/java/chess/controller/Command.java b/src/main/java/chess/controller/Command.java index c88af463d12..16e70c78159 100644 --- a/src/main/java/chess/controller/Command.java +++ b/src/main/java/chess/controller/Command.java @@ -2,6 +2,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Objects; public final class Command { private static final int COMMAND_INDEX = 0; @@ -36,8 +37,25 @@ public boolean isEnd() { return type == CommandType.END; } + public boolean isStatus() { + return type == CommandType.STATUS; + } + public boolean isCorrectWhenMove() { - return commands.size() == MOVE_COMMAND_SIZE; + return type == CommandType.MOVE && commands.size() == MOVE_COMMAND_SIZE; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Command command = (Command) o; + return type == command.type && Objects.equals(commands, command.commands); + } + + @Override + public int hashCode() { + return Objects.hash(type, commands); } public List getCommands() { diff --git a/src/main/java/chess/controller/CommandType.java b/src/main/java/chess/controller/CommandType.java index a5eb3e6d2d8..92cc334da17 100644 --- a/src/main/java/chess/controller/CommandType.java +++ b/src/main/java/chess/controller/CommandType.java @@ -1,5 +1,5 @@ package chess.controller; public enum CommandType { - START, MOVE, END + START, MOVE, END, STATUS } diff --git a/src/main/java/chess/controller/dto/ChessResultDto.java b/src/main/java/chess/controller/dto/ChessResultDto.java new file mode 100644 index 00000000000..c98e69f2973 --- /dev/null +++ b/src/main/java/chess/controller/dto/ChessResultDto.java @@ -0,0 +1,19 @@ +package chess.controller.dto; + +public class ChessResultDto { + private final double whiteScore; + private final double blackScore; + + public ChessResultDto(final double whiteScore, final double blackScore) { + this.whiteScore = whiteScore; + this.blackScore = blackScore; + } + + public double getWhiteScore() { + return whiteScore; + } + + public double getBlackScore() { + return blackScore; + } +} diff --git a/src/main/java/chess/controller/mapper/ChessBoardDtoMapper.java b/src/main/java/chess/controller/mapper/ChessBoardDtoMapper.java new file mode 100644 index 00000000000..adc7e57379d --- /dev/null +++ b/src/main/java/chess/controller/mapper/ChessBoardDtoMapper.java @@ -0,0 +1,23 @@ +package chess.controller.mapper; + +import chess.controller.dto.ChessBoardDto; +import chess.controller.dto.PieceDto; +import chess.controller.dto.PositionDto; +import chess.domain.piece.Piece; +import chess.domain.piece.move.Position; + +import java.util.HashMap; +import java.util.Map; + +public final class ChessBoardDtoMapper { + + public static ChessBoardDto createChessBoardDto(final Map chessBoard) { + final Map boardDto = new HashMap<>(); + for (Position position : chessBoard.keySet()) { + boardDto.put( + PositionDtoMapper.createPositionDto(position.getRank(), position.getFile()), + PieceDtoMapper.createPieceDto(chessBoard.get(position))); + } + return new ChessBoardDto(boardDto); + } +} diff --git a/src/main/java/chess/controller/mapper/ChessResultDtoMapper.java b/src/main/java/chess/controller/mapper/ChessResultDtoMapper.java new file mode 100644 index 00000000000..68fde7cf387 --- /dev/null +++ b/src/main/java/chess/controller/mapper/ChessResultDtoMapper.java @@ -0,0 +1,10 @@ +package chess.controller.mapper; + +import chess.controller.dto.ChessResultDto; +import chess.domain.chess.vo.ChessScore; + +public class ChessResultDtoMapper { + public static ChessResultDto from(final ChessScore chessScore) { + return new ChessResultDto(chessScore.getWhiteScore().getScore(), chessScore.getBlackScore().getScore()); + } +} diff --git a/src/main/java/chess/service/PieceService.java b/src/main/java/chess/controller/mapper/PieceDtoMapper.java similarity index 56% rename from src/main/java/chess/service/PieceService.java rename to src/main/java/chess/controller/mapper/PieceDtoMapper.java index c5e9340a4d9..0f0e3aeec1b 100644 --- a/src/main/java/chess/service/PieceService.java +++ b/src/main/java/chess/controller/mapper/PieceDtoMapper.java @@ -1,11 +1,11 @@ -package chess.service; +package chess.controller.mapper; import chess.controller.dto.PieceDto; import chess.domain.piece.Piece; -public final class PieceService { +public final class PieceDtoMapper { - public PieceDto createPieceDto(final Piece piece) { + public static PieceDto createPieceDto(final Piece piece) { return new PieceDto(piece.getPieceType().name(), piece.getCampType().name()); } } diff --git a/src/main/java/chess/controller/mapper/PositionDtoMapper.java b/src/main/java/chess/controller/mapper/PositionDtoMapper.java new file mode 100644 index 00000000000..ea5aae6e26d --- /dev/null +++ b/src/main/java/chess/controller/mapper/PositionDtoMapper.java @@ -0,0 +1,10 @@ +package chess.controller.mapper; + +import chess.controller.dto.PositionDto; + +public final class PositionDtoMapper { + + public static PositionDto createPositionDto(final int rank, final int file) { + return new PositionDto(rank, file); + } +} diff --git a/src/main/java/chess/controller/status/Controller.java b/src/main/java/chess/controller/status/Controller.java index accf8df8cd9..8e65c0385eb 100644 --- a/src/main/java/chess/controller/status/Controller.java +++ b/src/main/java/chess/controller/status/Controller.java @@ -1,9 +1,14 @@ package chess.controller.status; import chess.controller.Command; +import chess.domain.chess.ChessGame; + +import java.util.Optional; public interface Controller { - Controller checkCommand(final Command command, final Runnable runnable); + Controller checkCommand(final Command command); boolean isRun(); + + Optional findGame(); } diff --git a/src/main/java/chess/controller/status/EndController.java b/src/main/java/chess/controller/status/EndController.java index 20c52655d8a..38cd6d5fad1 100644 --- a/src/main/java/chess/controller/status/EndController.java +++ b/src/main/java/chess/controller/status/EndController.java @@ -1,10 +1,16 @@ package chess.controller.status; import chess.controller.Command; +import chess.domain.chess.ChessGame; + +import java.util.Optional; public final class EndController implements Controller { + EndController() { + } + @Override - public Controller checkCommand(final Command command, final Runnable runnable) { + public Controller checkCommand(final Command command) { throw new IllegalArgumentException("게임이 끝났습니다."); } @@ -12,4 +18,9 @@ public Controller checkCommand(final Command command, final Runnable runnable) { public boolean isRun() { return false; } + + @Override + public Optional findGame() { + return Optional.empty(); + } } diff --git a/src/main/java/chess/controller/status/MoveController.java b/src/main/java/chess/controller/status/MoveController.java index d17dc744ada..5871a3d6244 100644 --- a/src/main/java/chess/controller/status/MoveController.java +++ b/src/main/java/chess/controller/status/MoveController.java @@ -1,51 +1,70 @@ package chess.controller.status; import chess.controller.Command; -import chess.domain.chess.CampType; import chess.domain.chess.ChessGame; import chess.domain.piece.move.Position; import chess.domain.piece.move.PositionConverter; +import chess.service.ChessGameService; import java.util.List; +import java.util.Optional; public final class MoveController implements Controller { - private final ChessGame chessGame; - private final CampType campType; - public MoveController(final ChessGame chessGame, final CampType campType) { - this.chessGame = chessGame; - this.campType = campType; - } - - private Controller move(final Command command, final Runnable runnable) { - validateCommand(command); - final List commands = command.getCommands(); - final Position source = PositionConverter.convert(commands.get(1)); - final Position target = PositionConverter.convert(commands.get(2)); - chessGame.setUp(source, target, campType); - runnable.run(); - return new MoveController(chessGame, campType.changeTurn()); - } + private final long userId; + private final ChessGameService chessGameService; - private void validateCommand(final Command command) { - if (!command.isCorrectWhenMove()) { - throw new IllegalArgumentException("'move source위치 target위치 - 예. move b2 b3'와 같은 형태로 입력해 주세요."); - } + MoveController(final long userId, final ChessGameService chessGameService) { + this.userId = userId; + this.chessGameService = chessGameService; } @Override - public Controller checkCommand(final Command command, final Runnable runnable) { + public Controller checkCommand(final Command command) { if (command.isStart()) { throw new IllegalArgumentException("이미 시작이 완료되었습니다."); } if (command.isEnd()) { return new EndController(); } - return move(command, runnable); + if (command.isStatus()) { + return new StatusController(userId, chessGameService).getStatus(true); + } + return move(command); } @Override public boolean isRun() { return true; } + + @Override + public Optional findGame() { + final ChessGame chessGame = chessGameService.getOrCreateChessGame(userId); + return Optional.of(chessGame); + } + + Controller move(final Command command) { + validateCommand(command); + playChessGame(command); + final ChessGame chessGame = chessGameService.getOrCreateChessGame(userId); + if (!chessGame.isKingAlive()) { + return new StatusController(userId, chessGameService).getStatus(false); + } + return new MoveController(userId, chessGameService); + } + + private void validateCommand(final Command command) { + if (!command.isCorrectWhenMove()) { + throw new IllegalArgumentException("'move source위치 target위치 - 예. move b2 b3'와 같은 형태로 입력해 주세요."); + } + } + + private void playChessGame(final Command command) { + final List commands = command.getCommands(); + final Position source = PositionConverter.convert(commands.get(1)); + final Position target = PositionConverter.convert(commands.get(2)); + chessGameService.play(userId, source, target); + } + } diff --git a/src/main/java/chess/controller/status/StartController.java b/src/main/java/chess/controller/status/StartController.java index de300e5432b..924aea513d5 100644 --- a/src/main/java/chess/controller/status/StartController.java +++ b/src/main/java/chess/controller/status/StartController.java @@ -1,31 +1,39 @@ package chess.controller.status; import chess.controller.Command; -import chess.domain.chess.CampType; import chess.domain.chess.ChessGame; +import chess.service.ChessGameService; -public final class StartController implements Controller { +import java.util.Optional; - private final ChessGame chessGame; +public final class StartController implements Controller { + private final long userId; + private final ChessGameService chessGameService; - public StartController(final ChessGame chessGame) { - this.chessGame = chessGame; + public StartController(final long userId, final ChessGameService chessGameService) { + this.userId = userId; + this.chessGameService = chessGameService; } @Override - public Controller checkCommand(final Command command, final Runnable runnable) { + public Controller checkCommand(final Command command) { if (command.isEnd()) { return new EndController(); } - if (command.isMove()) { + if (!command.isStart()) { throw new IllegalArgumentException("게임이 시작되지 않았습니다."); } - runnable.run(); - return new MoveController(chessGame, CampType.WHITE); + return new MoveController(userId, chessGameService); } @Override public boolean isRun() { return true; } + + @Override + public Optional findGame() { + final ChessGame chessGame = chessGameService.getOrCreateChessGame(userId); + return Optional.of(chessGame); + } } diff --git a/src/main/java/chess/controller/status/StatusController.java b/src/main/java/chess/controller/status/StatusController.java new file mode 100644 index 00000000000..1a5b98b3f3a --- /dev/null +++ b/src/main/java/chess/controller/status/StatusController.java @@ -0,0 +1,67 @@ +package chess.controller.status; + +import chess.controller.Command; +import chess.controller.dto.ChessResultDto; +import chess.controller.mapper.ChessResultDtoMapper; +import chess.domain.chess.ChessGame; +import chess.domain.chess.ChessGameCalculator; +import chess.domain.chess.vo.ChessScore; +import chess.service.ChessGameService; +import chess.view.OutputView; + +import java.util.Optional; + +public final class StatusController implements Controller { + private final long userId; + private final ChessGameService chessGameService; + + StatusController(final long userId, final ChessGameService chessGameService) { + this.userId = userId; + this.chessGameService = chessGameService; + } + + @Override + public Controller checkCommand(final Command command) { + if (command.isStart()) { + throw new IllegalArgumentException("이미 시작이 완료되었습니다."); + } + if (command.isEnd()) { + return new EndController(); + } + if (command.isMove()) { + return new MoveController(userId, chessGameService).move(command); + } + return getStatus(true); + } + + @Override + public boolean isRun() { + return true; + } + + @Override + public Optional findGame() { + final ChessGame chessGame = chessGameService.getOrCreateChessGame(userId); + return Optional.of(chessGame); + } + + Controller getStatus(final boolean isGameRun) { + if (!isGameRun) { + OutputView.print("킹이 사망하여 게임이 종료되었습니다!"); + runCalculator().run(); + chessGameService.clear(userId); + return new EndController(); + } + runCalculator().run(); + return this; + } + + private Runnable runCalculator() { + return () -> { + final ChessGame chessGame = chessGameService.getOrCreateChessGame(userId); + final ChessScore chessResult = ChessGameCalculator.calculate(chessGame); + final ChessResultDto chessResultDto = ChessResultDtoMapper.from(chessResult); + OutputView.printChessResult(chessResultDto); + }; + } +} diff --git a/src/main/java/chess/dao/chess/ChessGameDao.java b/src/main/java/chess/dao/chess/ChessGameDao.java new file mode 100644 index 00000000000..f979a505721 --- /dev/null +++ b/src/main/java/chess/dao/chess/ChessGameDao.java @@ -0,0 +1,16 @@ +package chess.dao.chess; + +import chess.domain.chess.CampType; +import chess.entity.ChessGameEntity; + +import java.util.Optional; + +public interface ChessGameDao { + Optional findByUserId(final long userId); + + Long insert(final ChessGameEntity chessGameEntity); + + void updateCurrentCampById(long id, CampType currentCamp); + + void deleteByUserId(long userId); +} diff --git a/src/main/java/chess/dao/chess/ChessGameDaoImpl.java b/src/main/java/chess/dao/chess/ChessGameDaoImpl.java new file mode 100644 index 00000000000..686883228e4 --- /dev/null +++ b/src/main/java/chess/dao/chess/ChessGameDaoImpl.java @@ -0,0 +1,52 @@ +package chess.dao.chess; + +import chess.database.JdbcTemplate; +import chess.domain.chess.CampType; +import chess.entity.ChessGameEntity; + +import java.util.Optional; + +public final class ChessGameDaoImpl implements ChessGameDao { + private final JdbcTemplate jdbcTemplate; + + public ChessGameDaoImpl(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public Optional findByUserId(final long userId) { + final String query = "SELECT * FROM chess_game WHERE user_id = ?"; + + return jdbcTemplate.findOne(query, (resultSet -> new ChessGameEntity( + resultSet.getLong("chess_game_id"), + resultSet.getString("current_camp"), + resultSet.getLong("user_id")) + ), String.valueOf(userId)); + } + + @Override + public Long insert(final ChessGameEntity chessGameEntity) { + final String query = "INSERT INTO chess_game(current_camp, user_id) VALUES (?, ?)"; + + return jdbcTemplate.executeUpdate(query, + chessGameEntity.getCurrentCamp(), + String.valueOf(chessGameEntity.getUserId())); + } + + @Override + public void updateCurrentCampById(final long id, final CampType currentCamp) { + final String query = "UPDATE chess_game SET current_camp = ? WHERE chess_game_id = ?"; + + jdbcTemplate.executeUpdate(query, + currentCamp.name(), + String.valueOf(id)); + } + + @Override + public void deleteByUserId(final long userId) { + final String query = "DELETE FROM chess_game where user_id = ?"; + + jdbcTemplate.executeUpdate(query, + String.valueOf(userId)); + } +} diff --git a/src/main/java/chess/dao/chess/PieceDao.java b/src/main/java/chess/dao/chess/PieceDao.java new file mode 100644 index 00000000000..eb1702d57a8 --- /dev/null +++ b/src/main/java/chess/dao/chess/PieceDao.java @@ -0,0 +1,15 @@ +package chess.dao.chess; + +import chess.entity.PieceEntity; + +import java.util.List; + +public interface PieceDao { + List findByChessGameId(final long chessGameId); + + Long insert(final PieceEntity pieceEntity); + + void deleteByPositions(final long chessGameId, final PieceEntity... pieceEntity); + + void deleteByChessGameId(long chessGameId); +} diff --git a/src/main/java/chess/dao/chess/PieceDaoImpl.java b/src/main/java/chess/dao/chess/PieceDaoImpl.java new file mode 100644 index 00000000000..627e887a29a --- /dev/null +++ b/src/main/java/chess/dao/chess/PieceDaoImpl.java @@ -0,0 +1,84 @@ +package chess.dao.chess; + +import chess.database.JdbcTemplate; +import chess.entity.PieceEntity; + +import java.util.List; + +public class PieceDaoImpl implements PieceDao { + + private final JdbcTemplate jdbcTemplate; + + public PieceDaoImpl(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public List findByChessGameId(final long chessGameId) { + final String query = "SELECT * FROM piece WHERE chess_game_id = ?"; + return jdbcTemplate.findAll(query, (resultSet -> PieceEntity.create( + resultSet.getLong("piece_id"), + resultSet.getLong("chess_game_id"), + resultSet.getInt("piece_rank"), + resultSet.getInt("piece_file"), + resultSet.getString("piece_type"), + resultSet.getString("camp"))), String.valueOf(chessGameId)); + } + + @Override + public Long insert(final PieceEntity pieceEntity) { + final String query = "INSERT INTO piece(piece_rank, piece_file, piece_type, camp, chess_game_id) " + + "VALUES (?, ?, ?, ?, ?)"; + return jdbcTemplate.executeUpdate(query, + String.valueOf(pieceEntity.getRank()), + String.valueOf(pieceEntity.getFile()), + pieceEntity.getPieceType(), pieceEntity.getCampType(), + String.valueOf(pieceEntity.getChessGameId())); + } + + @Override + public void deleteByPositions(final long chessGameId, final PieceEntity... pieceEntity) { + if (pieceEntity == null) { + return; + } + if (pieceEntity.length == 1) { + deleteOneByPosition(chessGameId, pieceEntity[0]); + return; + } + final PieceEntity source = pieceEntity[0]; + final PieceEntity destination = pieceEntity[1]; + deleteTwoByPosition(chessGameId, source, destination); + } + + @Override + public void deleteByChessGameId(final long chessGameId) { + final String query = "DELETE FROM piece where chess_game_id = ?"; + + jdbcTemplate.executeUpdate(query, + String.valueOf(chessGameId)); + } + + private void deleteOneByPosition(final long chessGameId, final PieceEntity pieceEntity) { + final String query = "DELETE FROM piece WHERE chess_game_id = ? " + + "and piece_rank = ? and piece_file = ?"; + + jdbcTemplate.executeUpdate(query, + String.valueOf(chessGameId), + String.valueOf(pieceEntity.getRank()), + String.valueOf(pieceEntity.getFile())); + } + + private void deleteTwoByPosition(final long chessGameId, final PieceEntity source, final PieceEntity destination) { + final String query = "DELETE FROM piece WHERE chess_game_id = ? " + + "and (" + + " (piece_rank = ? and piece_file = ?) or (piece_rank = ? and piece_file = ?)" + + ")"; + + jdbcTemplate.executeUpdate(query, + String.valueOf(chessGameId), + String.valueOf(source.getRank()), + String.valueOf(source.getFile()), + String.valueOf(destination.getRank()), + String.valueOf(destination.getFile())); + } +} diff --git a/src/main/java/chess/dao/mapper/RowMapper.java b/src/main/java/chess/dao/mapper/RowMapper.java new file mode 100644 index 00000000000..d0b0e0fc325 --- /dev/null +++ b/src/main/java/chess/dao/mapper/RowMapper.java @@ -0,0 +1,9 @@ +package chess.dao.mapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@FunctionalInterface +public interface RowMapper { + T mapRow(final ResultSet resultSet) throws SQLException; +} diff --git a/src/main/java/chess/dao/user/UserDao.java b/src/main/java/chess/dao/user/UserDao.java new file mode 100644 index 00000000000..baf77003884 --- /dev/null +++ b/src/main/java/chess/dao/user/UserDao.java @@ -0,0 +1,11 @@ +package chess.dao.user; + +import chess.entity.UserEntity; + +import java.util.Optional; + +public interface UserDao { + Optional findByName(final String name); + + Long insert(final String name); +} diff --git a/src/main/java/chess/dao/user/UserDaoImpl.java b/src/main/java/chess/dao/user/UserDaoImpl.java new file mode 100644 index 00000000000..89a5bb57fb0 --- /dev/null +++ b/src/main/java/chess/dao/user/UserDaoImpl.java @@ -0,0 +1,30 @@ +package chess.dao.user; + +import chess.database.JdbcTemplate; +import chess.entity.UserEntity; + +import java.util.Optional; + +public final class UserDaoImpl implements UserDao { + + private final JdbcTemplate jdbcTemplate; + + public UserDaoImpl(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public Optional findByName(final String name) { + final String query = "SELECT * FROM user WHERE user_name = ?"; + return jdbcTemplate.findOne(query, (resultSet -> new UserEntity( + resultSet.getLong("user_id"), + resultSet.getString("user_name")) + ), name); + } + + @Override + public Long insert(final String name) { + final String query = "INSERT INTO user(user_name) VALUES(?)"; + return jdbcTemplate.executeUpdate(query, name); + } +} diff --git a/src/main/java/chess/database/JdbcTemplate.java b/src/main/java/chess/database/JdbcTemplate.java new file mode 100644 index 00000000000..3bdf2373881 --- /dev/null +++ b/src/main/java/chess/database/JdbcTemplate.java @@ -0,0 +1,95 @@ +package chess.database; + +import chess.dao.mapper.RowMapper; +import chess.database.properties.ChessProperties; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public final class JdbcTemplate { + private final ChessProperties chessProperties; + + public JdbcTemplate(final ChessProperties chessProperties) { + this.chessProperties = chessProperties; + } + + public Optional findOne(final String query, + final RowMapper rowMapper, + final String... params) { + try (final Connection connection = getConnection(); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + for (int i = 0; i < params.length; i++) { + preparedStatement.setString(i + 1, params[i]); + } + + final ResultSet resultSet = preparedStatement.executeQuery(); + if (!resultSet.next()) { + return Optional.empty(); + } + return Optional.ofNullable(rowMapper.mapRow(resultSet)); + } catch (SQLException e) { + System.out.println("SQL Exception! = " + e.getMessage()); + throw new RuntimeException(e); + } + } + + public List findAll(final String query, + final RowMapper rowMapper, + final String... params) { + try (final Connection connection = getConnection(); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + for (int i = 0; i < params.length; i++) { + preparedStatement.setString(i + 1, params[i]); + } + + final List results = new ArrayList<>(); + final ResultSet resultSet = preparedStatement.executeQuery(); + while (resultSet.next()) { + results.add(rowMapper.mapRow(resultSet)); + } + return results; + } catch (SQLException e) { + System.out.println("SQL Exception! = " + e.getMessage()); + throw new RuntimeException(e); + } + } + + public Long executeUpdate(final String query, + final String... params) { + try (final Connection connection = getConnection(); + final PreparedStatement preparedStatement = connection.prepareStatement( + query, Statement.RETURN_GENERATED_KEYS)) { + + for (int i = 0; i < params.length; i++) { + preparedStatement.setString(i + 1, params[i]); + } + preparedStatement.executeUpdate(); + + final ResultSet generatedKeys = preparedStatement.getGeneratedKeys(); + if (!generatedKeys.next()) { + return null; + } + return generatedKeys.getLong(1); + } catch (SQLException e) { + System.out.println("SQL Exception! = " + e.getMessage()); + throw new RuntimeException(e); + } + } + + private Connection getConnection() { + try { + return DriverManager.getConnection(chessProperties.getUrl(), + chessProperties.getUserName(), chessProperties.getPassword()); + } catch (final SQLException e) { + System.out.println("DB Connection Exception! = " + e.getMessage()); + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/chess/database/properties/ChessProperties.java b/src/main/java/chess/database/properties/ChessProperties.java new file mode 100644 index 00000000000..ba8e974527f --- /dev/null +++ b/src/main/java/chess/database/properties/ChessProperties.java @@ -0,0 +1,45 @@ +package chess.database.properties; + +import org.yaml.snakeyaml.Yaml; + +import java.io.FileReader; +import java.io.IOException; +import java.util.Map; + +public final class ChessProperties { + private static final String URL = "url"; + private static final String USERNAME = "username"; + private static final String PASSWORD = "password"; + + private final String url; + private final String userName; + private final String password; + + public ChessProperties() { + final Map properties = getProperties(); + this.url = properties.get(URL); + this.userName = properties.get(USERNAME); + this.password = properties.get(PASSWORD); + } + + private Map getProperties() { + try (final FileReader fileReader = new FileReader("src/main/resources/application.yml")) { + return new Yaml().load(fileReader); + } catch (IOException e) { + System.out.println("properties read Exception! = " + e.getMessage()); + throw new RuntimeException(e); + } + } + + public String getUrl() { + return url; + } + + public String getUserName() { + return userName; + } + + public String getPassword() { + return password; + } +} diff --git a/src/main/java/chess/domain/board/ChessBoard.java b/src/main/java/chess/domain/board/ChessBoard.java index a9b6ccaade9..d9f4076d94a 100644 --- a/src/main/java/chess/domain/board/ChessBoard.java +++ b/src/main/java/chess/domain/board/ChessBoard.java @@ -1,10 +1,14 @@ package chess.domain.board; +import chess.domain.chess.CampType; import chess.domain.chess.ChessGame; import chess.domain.piece.Piece; import chess.domain.piece.move.Position; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; public final class ChessBoard { private final Map board; @@ -18,6 +22,10 @@ public static ChessBoard getInstance(final ChessGame chessGame) { return new ChessBoard(board); } + public static ChessBoard create(final Map board) { + return new ChessBoard(board); + } + public boolean contains(final Position position) { return board.containsKey(position); } @@ -45,6 +53,19 @@ public boolean isPossibleRoute(final Position source, final Position target) { return targetPiece == null || !targetPiece.isSameCamp(board.get(source)); } + public List getAliveKings() { + return board.keySet().stream() + .map(board::get) + .filter(Piece::isKing) + .collect(Collectors.toUnmodifiableList()); + } + + public Map getBoardByCamp(final CampType campType) { + return board.entrySet().stream() + .filter(entry -> entry.getValue().isSameCamp(campType)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + private boolean isObstructed(final Position target, final Position unitPosition, final Position currentPosition) { if (currentPosition.isSame(target)) { return false; @@ -56,6 +77,19 @@ private boolean isObstructed(final Position target, final Position unitPosition, return isObstructed(target, unitPosition, nextPosition); } + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final ChessBoard that = (ChessBoard) o; + return Objects.equals(board, that.board); + } + + @Override + public int hashCode() { + return Objects.hash(board); + } + public Map getBoard() { return Map.copyOf(board); } diff --git a/src/main/java/chess/domain/board/ChessBoardFactory.java b/src/main/java/chess/domain/board/ChessBoardFactory.java index b006ec533dc..a9dc40b073b 100644 --- a/src/main/java/chess/domain/board/ChessBoardFactory.java +++ b/src/main/java/chess/domain/board/ChessBoardFactory.java @@ -38,7 +38,7 @@ static ChessBoardFactory getInstance(final ChessGame chessGame) { return chessBoardFactory; } - public Map createBoard() { + Map createBoard() { final Map board = new HashMap<>(); createWhiteArea(board); createBlackArea(board); diff --git a/src/main/java/chess/domain/chess/CampType.java b/src/main/java/chess/domain/chess/CampType.java index b355421287a..3917462e828 100644 --- a/src/main/java/chess/domain/chess/CampType.java +++ b/src/main/java/chess/domain/chess/CampType.java @@ -1,9 +1,13 @@ package chess.domain.chess; +import java.util.Arrays; + public enum CampType { BLACK, WHITE; + private static final String ERROR_MESSAGE = "일치하는 진영이 존재하지 않습니다."; + public static CampType divide(final char columnPosition) { if (Character.isLowerCase(columnPosition)) { return WHITE; @@ -11,6 +15,15 @@ public static CampType divide(final char columnPosition) { return BLACK; } + public static CampType from(final String name) { + return Arrays.stream(CampType.values()) + .filter(campType -> campType.name().equals(name)) + .findFirst() + .orElseThrow(() -> { + throw new IllegalArgumentException(ERROR_MESSAGE); + }); + } + public CampType changeTurn() { if (this == BLACK) { return WHITE; diff --git a/src/main/java/chess/domain/chess/ChessGame.java b/src/main/java/chess/domain/chess/ChessGame.java index d469240e566..62a46e73e8f 100644 --- a/src/main/java/chess/domain/chess/ChessGame.java +++ b/src/main/java/chess/domain/chess/ChessGame.java @@ -4,22 +4,34 @@ import chess.domain.piece.Piece; import chess.domain.piece.move.Position; +import java.util.List; import java.util.Map; public final class ChessGame { + private static final int ALL_KING_ALIVE_COUNT = 2; + private final ChessBoard chessBoard; private CampType currentCamp; - public ChessGame() { + public ChessGame(final CampType currentCamp) { + this.currentCamp = currentCamp; this.chessBoard = ChessBoard.getInstance(this); } - public void setUp(final Position source, final Position target, final CampType currentCamp) { + public ChessGame(final ChessBoard chessBoard, final CampType currentCamp) { + this.chessBoard = chessBoard; this.currentCamp = currentCamp; - play(source, target); } - private void play(final Position source, final Position target) { + public Map getWhiteBoard() { + return chessBoard.getBoardByCamp(CampType.WHITE); + } + + public Map getBlackBoard() { + return chessBoard.getBoardByCamp(CampType.BLACK); + } + + public void play(final Position source, final Position target) { validateCamp(source); validateTargetSameCamp(target); validateObstacle(source, target); @@ -27,6 +39,12 @@ private void play(final Position source, final Position target) { throw new IllegalArgumentException("이동할 수 없는 위치입니다."); } movePiece(source, target); + currentCamp = currentCamp.changeTurn(); + } + + public boolean isKingAlive() { + List aliveKings = chessBoard.getAliveKings(); + return aliveKings.size() == ALL_KING_ALIVE_COUNT; } private void validateObstacle(final Position source, final Position target) { @@ -48,7 +66,6 @@ private void validateTargetSameCamp(final Position target) { if (!chessBoard.contains(target)) { return; } - validatePieceExistence(target); final Piece piece = chessBoard.getPiece(target); if (piece.isSameCamp(currentCamp)) { throw new IllegalArgumentException("아군 기물이 있는 곳으로 이동할 수 없습니다."); @@ -78,8 +95,12 @@ private void validatePieceExistence(final Position position) { throw new IllegalArgumentException("체스말이 존재하는 위치를 입력해 주세요."); } } - + public Map getChessBoard() { return chessBoard.getBoard(); } + + public CampType getCurrentCamp() { + return currentCamp; + } } diff --git a/src/main/java/chess/domain/chess/ChessGameCalculator.java b/src/main/java/chess/domain/chess/ChessGameCalculator.java new file mode 100644 index 00000000000..10f19116206 --- /dev/null +++ b/src/main/java/chess/domain/chess/ChessGameCalculator.java @@ -0,0 +1,62 @@ +package chess.domain.chess; + +import chess.domain.chess.vo.ChessScore; +import chess.domain.piece.Piece; +import chess.domain.piece.Score; +import chess.domain.piece.move.Position; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public final class ChessGameCalculator { + + private static final Score DEFAULT_SCORE = Score.create(0); + private static final int PAWN_SUBTRACT_COUNT_THRESHOLD = 1; + private static final Score PAWN_SPECIAL_SCORE = Score.create(0.5); + + public static ChessScore calculate(final ChessGame chessGame) { + final Map whiteBoard = chessGame.getWhiteBoard(); + Score whiteScore = calculateScore(whiteBoard); + + final Map blackBoard = chessGame.getBlackBoard(); + Score blackScore = calculateScore(blackBoard); + + return new ChessScore(whiteScore, blackScore); + } + + private static Score calculateScore(Map board) { + final Map> piecesByFile = groupBoardByFile(board); + Score score = calculateTotalScore(board); + for (List pieces : piecesByFile.values()) { + final Score subtractScore = getSubtractScoreByPawn(pieces); + score = score.subtract(subtractScore); + } + return score; + } + + private static Score calculateTotalScore(Map board) { + return board.values().stream() + .map(Piece::getScore) + .reduce(DEFAULT_SCORE, Score::add); + } + + private static Map> groupBoardByFile(final Map board) { + return board.entrySet().stream() + .collect(Collectors.groupingBy(entry -> entry.getKey().getFile(), + Collectors.mapping(Map.Entry::getValue, Collectors.toList()) + )); + } + + private static Score getSubtractScoreByPawn(final List pieces) { + final long pawnCount = getPawnCount(pieces); + if (pawnCount > PAWN_SUBTRACT_COUNT_THRESHOLD) { + return PAWN_SPECIAL_SCORE.multiply(pawnCount); + } + return DEFAULT_SCORE; + } + + private static long getPawnCount(final List pieces) { + return pieces.stream().filter(Piece::isPawn).count(); + } +} diff --git a/src/main/java/chess/domain/chess/vo/ChessScore.java b/src/main/java/chess/domain/chess/vo/ChessScore.java new file mode 100644 index 00000000000..f0ccd5100f2 --- /dev/null +++ b/src/main/java/chess/domain/chess/vo/ChessScore.java @@ -0,0 +1,36 @@ +package chess.domain.chess.vo; + +import chess.domain.piece.Score; + +import java.util.Objects; + +public class ChessScore { + private final Score whiteScore; + private final Score blackScore; + + public ChessScore(final Score whiteScore, final Score blackScore) { + this.whiteScore = whiteScore; + this.blackScore = blackScore; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final ChessScore chessScore = (ChessScore) o; + return Objects.equals(whiteScore, chessScore.whiteScore) && Objects.equals(blackScore, chessScore.blackScore); + } + + @Override + public int hashCode() { + return Objects.hash(whiteScore, blackScore); + } + + public Score getWhiteScore() { + return whiteScore; + } + + public Score getBlackScore() { + return blackScore; + } +} diff --git a/src/main/java/chess/domain/piece/Piece.java b/src/main/java/chess/domain/piece/Piece.java index db011c51b7d..15755d6e3f5 100644 --- a/src/main/java/chess/domain/piece/Piece.java +++ b/src/main/java/chess/domain/piece/Piece.java @@ -30,18 +30,32 @@ public boolean isSameCamp(final CampType diffType) { public boolean canMove(final Position source, final Position target, final boolean isTargetExist) { if (pieceType == PieceType.PAWN && movable.canMove(source, target)) { + validatePawnDirection(source, target); return validatePawnMove(source, target, isTargetExist); } return movable.canMove(source, target); } public boolean canAttack(final Position source, final Position target, final boolean isTargetExist) { - if (pieceType == PieceType.PAWN && movable.canAttack(source, target) && !isTargetExist) { - throw new IllegalArgumentException("폰이 공격할 수 있는 위치가 아닙니다."); + if (pieceType == PieceType.PAWN) { + validatePawnDirection(source, target); + validatePawnCanAttack(source, target, isTargetExist); } return movable.canAttack(source, target); } + public boolean isKing() { + return pieceType == PieceType.KING; + } + + public boolean isPawn() { + return pieceType == PieceType.PAWN; + } + + public Score getScore() { + return pieceType.getScore(); + } + private boolean validatePawnMove(final Position source, final Position target, final boolean isTargetExist) { if (isPawnFirstMove(source)) { return !isTargetExist; @@ -61,6 +75,19 @@ private boolean validatePawnOneMove(final Position source, final Position target return !isTargetExist; } + private void validatePawnDirection(final Position source, final Position target) { + if ((campType == CampType.WHITE && source.isRankGreaterThan(target)) || + (campType == CampType.BLACK && target.isRankGreaterThan(source))) { + throw new IllegalArgumentException("폰은 앞으로만 이동할 수 있습니다."); + } + } + + private void validatePawnCanAttack(final Position source, final Position target, final boolean isTargetExist) { + if (movable.canAttack(source, target) && !isTargetExist) { + throw new IllegalArgumentException("폰이 공격할 수 있는 위치가 아닙니다."); + } + } + @Override public boolean equals(final Object o) { if (this == o) return true; diff --git a/src/main/java/chess/domain/piece/PieceType.java b/src/main/java/chess/domain/piece/PieceType.java index e61a1ce622f..2bc1b8aef3b 100644 --- a/src/main/java/chess/domain/piece/PieceType.java +++ b/src/main/java/chess/domain/piece/PieceType.java @@ -1,10 +1,33 @@ package chess.domain.piece; +import java.util.Arrays; + public enum PieceType { - QUEEN, - ROOK, - KNIGHT, - PAWN, - BISHOP, - KING + QUEEN(Score.create(9)), + ROOK(Score.create(5)), + KNIGHT(Score.create(2.5)), + PAWN(Score.create(1)), + BISHOP(Score.create(3)), + KING(Score.create(0)); + + private static final String ERROR_MESSAGE = "일치하는 체스 말의 타입이 존재하지 않습니다."; + + private final Score score; + + PieceType(final Score score) { + this.score = score; + } + + public static PieceType from(final String name) { + return Arrays.stream(PieceType.values()) + .filter(pieceType -> pieceType.name().equals(name)) + .findFirst() + .orElseThrow(() -> { + throw new IllegalArgumentException(ERROR_MESSAGE); + }); + } + + public Score getScore() { + return score; + } } diff --git a/src/main/java/chess/domain/piece/Score.java b/src/main/java/chess/domain/piece/Score.java new file mode 100644 index 00000000000..6a324711bb7 --- /dev/null +++ b/src/main/java/chess/domain/piece/Score.java @@ -0,0 +1,51 @@ +package chess.domain.piece; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class Score { + private static final Map CACHE = new HashMap<>(); + + private final double score; + + private Score(final double score) { + this.score = score; + } + + public static Score create(final double score) { + if (!CACHE.containsKey(score)) { + CACHE.put(score, new Score(score)); + } + return CACHE.get(score); + } + + public Score add(final Score other) { + return new Score(score + other.score); + } + + public Score subtract(final Score other) { + return new Score(score - other.score); + } + + public Score multiply(final long count) { + return new Score(score * count); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Score score1 = (Score) o; + return Double.compare(score1.score, score) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(score); + } + + public double getScore() { + return score; + } +} diff --git a/src/main/java/chess/domain/user/User.java b/src/main/java/chess/domain/user/User.java new file mode 100644 index 00000000000..77f0e9b4e32 --- /dev/null +++ b/src/main/java/chess/domain/user/User.java @@ -0,0 +1,27 @@ +package chess.domain.user; + +public class User { + private static final int MIN_LENGTH = 1; + private static final int MAX_LENGTH = 20; + + private final String name; + + private User(final String name) { + this.name = name; + } + + public static User create(final String name) { + validateLength(name); + return new User(name); + } + + private static void validateLength(final String name) { + if (name.length() < MIN_LENGTH || name.length() > MAX_LENGTH) { + throw new IllegalArgumentException(String.format("이름은 %d~%d글자 사이여야 합니다.", MIN_LENGTH, MAX_LENGTH)); + } + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/chess/entity/ChessGameEntity.java b/src/main/java/chess/entity/ChessGameEntity.java new file mode 100644 index 00000000000..4b4390bb7ff --- /dev/null +++ b/src/main/java/chess/entity/ChessGameEntity.java @@ -0,0 +1,30 @@ +package chess.entity; + +public final class ChessGameEntity { + private Long id; + private final String currentCamp; + private final Long userId; + + public ChessGameEntity(final Long id, final String currentCamp, final Long userId) { + this.id = id; + this.currentCamp = currentCamp; + this.userId = userId; + } + + public ChessGameEntity(final String currentCamp, final Long userId) { + this.currentCamp = currentCamp; + this.userId = userId; + } + + public Long getId() { + return id; + } + + public String getCurrentCamp() { + return currentCamp; + } + + public Long getUserId() { + return userId; + } +} diff --git a/src/main/java/chess/entity/PieceEntity.java b/src/main/java/chess/entity/PieceEntity.java new file mode 100644 index 00000000000..fae4a9e0bcc --- /dev/null +++ b/src/main/java/chess/entity/PieceEntity.java @@ -0,0 +1,76 @@ +package chess.entity; + +public final class PieceEntity { + private Long id; + private final Long chessGameId; + private final Integer rank; + private final Integer file; + private String pieceType; + private String campType; + + private PieceEntity(final Long id, final Long chessGameId, final Integer rank, + final Integer file, final String pieceType, final String campType) { + this.id = id; + this.chessGameId = chessGameId; + this.rank = rank; + this.file = file; + this.pieceType = pieceType; + this.campType = campType; + } + + public PieceEntity(final Long chessGameId, final Integer rank, final Integer file) { + this.chessGameId = chessGameId; + this.rank = rank; + this.file = file; + } + + public static PieceEntity create(final Long id, final Long chessGameId, final Integer rank, + final Integer file, final String pieceType, final String campType) { + return new PieceEntity(id, chessGameId, rank, file, pieceType, campType); + } + + public static PieceEntity createWithChessGameId(final Long chessGameId, final Integer rank, final Integer file, + final String pieceType, final String campType) { + return new PieceEntity(null, chessGameId, rank, file, pieceType, campType); + } + + public static PieceEntity createWithLocation(final Long chessGameId, final Integer rank, final Integer file) { + return new PieceEntity(chessGameId, rank, file); + } + + @Override + public String toString() { + return "PieceEntity{" + + "id=" + id + + ", chessGameId=" + chessGameId + + ", rank=" + rank + + ", file=" + file + + ", pieceType='" + pieceType + '\'' + + ", campType='" + campType + '\'' + + '}'; + } + + public Long getId() { + return id; + } + + public Integer getRank() { + return rank; + } + + public Integer getFile() { + return file; + } + + public String getPieceType() { + return pieceType; + } + + public String getCampType() { + return campType; + } + + public Long getChessGameId() { + return chessGameId; + } +} diff --git a/src/main/java/chess/entity/UserEntity.java b/src/main/java/chess/entity/UserEntity.java new file mode 100644 index 00000000000..4a90d440ac2 --- /dev/null +++ b/src/main/java/chess/entity/UserEntity.java @@ -0,0 +1,23 @@ +package chess.entity; + +public final class UserEntity { + private Long id; + private final String name; + + public UserEntity(final Long id, final String name) { + this.id = id; + this.name = name; + } + + public UserEntity(final String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/chess/script/docker-compose.yml b/src/main/java/chess/script/docker-compose.yml new file mode 100644 index 00000000000..558a1d5a53f --- /dev/null +++ b/src/main/java/chess/script/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/script/table.sql b/src/main/java/chess/script/table.sql new file mode 100644 index 00000000000..af97e83dd2f --- /dev/null +++ b/src/main/java/chess/script/table.sql @@ -0,0 +1,31 @@ +-- user +CREATE TABLE user +( + user_id BIGINT(20) NOT NULL AUTO_INCREMENT, + user_name varchar(255) UNIQUE NOT NULL, + PRIMARY KEY (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- chess_game +CREATE TABLE chess_game +( + chess_game_id BIGINT(20) NOT NULL AUTO_INCREMENT, + current_camp VARCHAR(5) NOT NULL, + user_id BIGINT(20) NOT NULL, + PRIMARY KEY (chess_game_id), + FOREIGN KEY (user_id) REFERENCES user (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- piece +CREATE TABLE piece +( + piece_id BIGINT(20) NOT NULL AUTO_INCREMENT, + piece_rank INT NOT NULL, + piece_file INT NOT NULL, + piece_type VARCHAR(10) NOT NULL, + camp VARCHAR(5) NOT NULL, + chess_game_id BIGINT(20) NOT NULL, + PRIMARY KEY (piece_id), + FOREIGN KEY (chess_game_id) REFERENCES chess_game (chess_game_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + diff --git a/src/main/java/chess/service/ChessBoardService.java b/src/main/java/chess/service/ChessBoardService.java index 28ed1b740eb..bb30ddbbc31 100644 --- a/src/main/java/chess/service/ChessBoardService.java +++ b/src/main/java/chess/service/ChessBoardService.java @@ -1,31 +1,46 @@ package chess.service; -import chess.controller.dto.ChessBoardDto; -import chess.controller.dto.PieceDto; -import chess.controller.dto.PositionDto; +import chess.dao.chess.PieceDao; +import chess.domain.board.ChessBoard; import chess.domain.piece.Piece; import chess.domain.piece.move.Position; +import chess.entity.PieceEntity; +import chess.service.mapper.ChessBoardMapper; -import java.util.HashMap; +import java.util.List; import java.util.Map; -public final class ChessBoardService { +public class ChessBoardService { + private final PieceDao pieceDao; - private final PieceService pieceService; - private final PositionService positionService; + ChessBoardService(final PieceDao pieceDao) { + this.pieceDao = pieceDao; + } + + ChessBoard getByChessGameId(final long chessGameId) { + final List pieceEntities = pieceDao.findByChessGameId(chessGameId); + return ChessBoardMapper.from(pieceEntities); + } - public ChessBoardService(final PieceService pieceService, final PositionService positionService) { - this.pieceService = pieceService; - this.positionService = positionService; + void savePiece(final PieceEntity pieceEntity) { + pieceDao.insert(pieceEntity); } - public ChessBoardDto createChessBoardDto(final Map chessBoard) { - final Map boardDto = new HashMap<>(); + void deletePieces(final PieceEntity sourcePiece, final PieceEntity targetPiece) { + final Long chessGameId = sourcePiece.getChessGameId(); + pieceDao.deleteByPositions(chessGameId, sourcePiece, targetPiece); + } + + void saveAll(final Long chessGameId, final Map chessBoard) { for (Position position : chessBoard.keySet()) { - boardDto.put( - positionService.createPositionDto(position.getRank(), position.getFile()), - pieceService.createPieceDto(chessBoard.get(position))); + final Piece piece = chessBoard.get(position); + PieceEntity pieceEntity = PieceEntity.createWithChessGameId(chessGameId, position.getRank(), + position.getFile(), piece.getPieceType().name(), piece.getCampType().name()); + pieceDao.insert(pieceEntity); } - return new ChessBoardDto(boardDto); + } + + void deleteByChessGameId(final Long chessGameId) { + pieceDao.deleteByChessGameId(chessGameId); } } diff --git a/src/main/java/chess/service/ChessGameService.java b/src/main/java/chess/service/ChessGameService.java new file mode 100644 index 00000000000..de05f8decd4 --- /dev/null +++ b/src/main/java/chess/service/ChessGameService.java @@ -0,0 +1,88 @@ +package chess.service; + +import chess.dao.chess.ChessGameDao; +import chess.domain.board.ChessBoard; +import chess.domain.chess.CampType; +import chess.domain.chess.ChessGame; +import chess.domain.piece.Piece; +import chess.domain.piece.move.Position; +import chess.entity.ChessGameEntity; +import chess.entity.PieceEntity; + +import java.util.Map; +import java.util.Optional; + +public final class ChessGameService { + private final ChessGameDao chessGameDao; + private final ChessBoardService chessBoardService; + + public ChessGameService(final ChessGameDao chessGameDao, final ChessBoardService chessBoardService) { + this.chessGameDao = chessGameDao; + this.chessBoardService = chessBoardService; + } + + public ChessGame getOrCreateChessGame(final long userId) { + final Optional findChessGameEntity = chessGameDao.findByUserId(userId); + if (findChessGameEntity.isEmpty()) { + return getNewChessGame(userId); + } + final ChessGameEntity chessGameEntity = findChessGameEntity.get(); + return getExistChessGame(chessGameEntity); + } + + public long getChessGameId(final long userId) { + final Optional findChessGameEntity = chessGameDao.findByUserId(userId); + if (findChessGameEntity.isEmpty()) { + throw new IllegalArgumentException("저장된 데이터가 존재하지 않습니다."); + } + return findChessGameEntity.get().getId(); + } + + public void play(final long userId, final Position source, final Position target) { + final ChessGame chessGame = getOrCreateChessGame(userId); + chessGame.play(source, target); + final long chessGameId = getChessGameId(userId); + deletePieces(source, target, chessGameId); + savePieces(target, chessGameId, chessGame); + chessGameDao.updateCurrentCampById(chessGameId, chessGame.getCurrentCamp()); + } + + private void deletePieces(final Position source, final Position target, final long chessGameId) { + final PieceEntity sourcePiece = PieceEntity.createWithLocation(chessGameId, source.getRank(), source.getFile()); + final PieceEntity targetPiece = PieceEntity.createWithLocation(chessGameId, target.getRank(), target.getFile()); + chessBoardService.deletePieces(sourcePiece, targetPiece); + } + + private void savePieces(final Position target, final long chessGameId, final ChessGame chessGame) { + final Map chessBoard = chessGame.getChessBoard(); + final Piece piece = chessBoard.get(target); + final PieceEntity savedPiece = PieceEntity.createWithChessGameId(chessGameId, target.getRank(), + target.getFile(), piece.getPieceType().name(), piece.getCampType().name()); + chessBoardService.savePiece(savedPiece); + } + + public void clear(final long userId) { + final long chessGameId = getChessGameId(userId); + chessBoardService.deleteByChessGameId(chessGameId); + chessGameDao.deleteByUserId(userId); + } + + private ChessGame getNewChessGame(final long userId) { + final CampType currentCamp = CampType.WHITE; + final long chessGameId = chessGameDao.insert(new ChessGameEntity(currentCamp.name(), userId)); + final ChessGame chessGame = new ChessGame(currentCamp); + chessBoardService.saveAll(chessGameId, chessGame.getChessBoard()); + return chessGame; + } + + private ChessGame getExistChessGame(final ChessGameEntity chessGameEntity) { + final ChessBoard chessBoard = getChessBoard(chessGameEntity); + final String currentCamp = chessGameEntity.getCurrentCamp(); + return new ChessGame(chessBoard, CampType.from(currentCamp)); + } + + private ChessBoard getChessBoard(final ChessGameEntity chessGameEntity) { + final long chessGameId = chessGameEntity.getId(); + return chessBoardService.getByChessGameId(chessGameId); + } +} diff --git a/src/main/java/chess/service/PositionService.java b/src/main/java/chess/service/PositionService.java deleted file mode 100644 index 652e97340a4..00000000000 --- a/src/main/java/chess/service/PositionService.java +++ /dev/null @@ -1,10 +0,0 @@ -package chess.service; - -import chess.controller.dto.PositionDto; - -public final class PositionService { - - public PositionDto createPositionDto(final int rank, final int file) { - return new PositionDto(rank, file); - } -} diff --git a/src/main/java/chess/service/ServiceHandler.java b/src/main/java/chess/service/ServiceHandler.java new file mode 100644 index 00000000000..875120b24c6 --- /dev/null +++ b/src/main/java/chess/service/ServiceHandler.java @@ -0,0 +1,7 @@ +package chess.service; + +public interface ServiceHandler { + UserService userService(); + + ChessGameService chessGameService(); +} diff --git a/src/main/java/chess/service/ServiceManager.java b/src/main/java/chess/service/ServiceManager.java new file mode 100644 index 00000000000..ac831df19ba --- /dev/null +++ b/src/main/java/chess/service/ServiceManager.java @@ -0,0 +1,29 @@ +package chess.service; + +import chess.dao.chess.ChessGameDao; +import chess.dao.chess.ChessGameDaoImpl; +import chess.dao.chess.PieceDao; +import chess.dao.chess.PieceDaoImpl; +import chess.dao.user.UserDaoImpl; +import chess.database.JdbcTemplate; + +public final class ServiceManager implements ServiceHandler { + private final JdbcTemplate jdbcTemplate; + + public ServiceManager(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public UserService userService() { + return new UserService(new UserDaoImpl(jdbcTemplate)); + } + + @Override + public ChessGameService chessGameService() { + final ChessGameDao chessGameDao = new ChessGameDaoImpl(jdbcTemplate); + final PieceDao pieceDao = new PieceDaoImpl(jdbcTemplate); + final ChessBoardService chessBoardService = new ChessBoardService(pieceDao); + return new ChessGameService(chessGameDao, chessBoardService); + } +} diff --git a/src/main/java/chess/service/UserService.java b/src/main/java/chess/service/UserService.java new file mode 100644 index 00000000000..6ca64f9253f --- /dev/null +++ b/src/main/java/chess/service/UserService.java @@ -0,0 +1,29 @@ +package chess.service; + +import chess.dao.user.UserDao; +import chess.domain.user.User; +import chess.entity.UserEntity; + +import java.util.Optional; + +public final class UserService { + private final UserDao userDao; + + public UserService(final UserDao userDao) { + this.userDao = userDao; + } + + public Long getOrCreateUserId(final String name) { + final User user = User.create(name); + final String userName = user.getName(); + final Optional userEntity = userDao.findByName(userName); + if (userEntity.isPresent()) { + return userEntity.get().getId(); + } + return save(name); + } + + private Long save(final String name) { + return userDao.insert(name); + } +} diff --git a/src/main/java/chess/service/mapper/ChessBoardMapper.java b/src/main/java/chess/service/mapper/ChessBoardMapper.java new file mode 100644 index 00000000000..f0a20319ed2 --- /dev/null +++ b/src/main/java/chess/service/mapper/ChessBoardMapper.java @@ -0,0 +1,28 @@ +package chess.service.mapper; + +import chess.domain.board.ChessBoard; +import chess.domain.chess.CampType; +import chess.domain.piece.Movable; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceType; +import chess.domain.piece.move.Position; +import chess.entity.PieceEntity; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class ChessBoardMapper { + + public static ChessBoard from(final List pieceEntities) { + final Map chessBoard = new HashMap<>(); + for (PieceEntity pieceEntity : pieceEntities) { + final Position position = new Position(pieceEntity.getRank(), pieceEntity.getFile()); + final PieceType pieceType = PieceType.from(pieceEntity.getPieceType()); + final CampType campType = CampType.from(pieceEntity.getCampType()); + final Movable movable = MovableMapper.from(pieceType); + chessBoard.put(position, new Piece(pieceType, campType, movable)); + } + return ChessBoard.create(chessBoard); + } +} diff --git a/src/main/java/chess/service/mapper/MovableMapper.java b/src/main/java/chess/service/mapper/MovableMapper.java new file mode 100644 index 00000000000..b25b22fece1 --- /dev/null +++ b/src/main/java/chess/service/mapper/MovableMapper.java @@ -0,0 +1,31 @@ +package chess.service.mapper; + +import chess.domain.piece.Bishop; +import chess.domain.piece.King; +import chess.domain.piece.Knight; +import chess.domain.piece.Movable; +import chess.domain.piece.Pawn; +import chess.domain.piece.PieceType; +import chess.domain.piece.Queen; +import chess.domain.piece.Rook; + +public final class MovableMapper { + public static Movable from(final PieceType pieceType) { + if (pieceType == PieceType.QUEEN) { + return new Queen(); + } + if (pieceType == PieceType.ROOK) { + return new Rook(); + } + if (pieceType == PieceType.KNIGHT) { + return new Knight(); + } + if (pieceType == PieceType.PAWN) { + return new Pawn(); + } + if (pieceType == PieceType.BISHOP) { + return new Bishop(); + } + return new King(); + } +} diff --git a/src/main/java/chess/view/InputView.java b/src/main/java/chess/view/InputView.java index 5dbcd5bd9b6..579815b7138 100644 --- a/src/main/java/chess/view/InputView.java +++ b/src/main/java/chess/view/InputView.java @@ -9,11 +9,15 @@ public final class InputView { private static final String INPUT_ERROR_MESSAGE = "빈 값을 입력할 수 없습니다."; private static final Scanner scanner = new Scanner(System.in); - public static List getCommand() { + public static List getCommands() { final String command = readCommand(); return Arrays.asList(command.split(DELIMITER)); } + public static String getCommand() { + return readCommand(); + } + private static String readCommand() { final String command = scanner.nextLine(); if (command == null || command.isBlank()) { diff --git a/src/main/java/chess/view/OutputView.java b/src/main/java/chess/view/OutputView.java index dd7ca613ac3..3292893230b 100644 --- a/src/main/java/chess/view/OutputView.java +++ b/src/main/java/chess/view/OutputView.java @@ -1,6 +1,7 @@ package chess.view; import chess.controller.dto.ChessBoardDto; +import chess.controller.dto.ChessResultDto; import chess.controller.dto.PieceDto; import chess.controller.dto.PositionDto; @@ -17,11 +18,26 @@ public final class OutputView { "> 게임 시작 : %s\n" + "> 게임 종료 : %s\n" + "> 게임 이동 : %s source위치 target위치 - 예. move b2 b3"; + private static final String RESULT_MESSAGE = "> 체스 게임 결과입니다.\n" + + "> 백진영 (WHITE) : %.1f\n" + + "> 흑진영 (BLACK) : %.1f\n" + + "> 승리한 팀 : %s\n"; + + private static final String USER_NAME_INPUT_MESSAGE = "플레이어님의 이름을 입력해 주세요. (1~20자)"; + + private static final String DRAW = "DRAW!"; + private static final String WHITE = "WHITE"; + private static final String BLACK = "BLACK"; + public static void print(final String message) { System.out.println(message); } + public static void printUserNameInputMessage() { + print(USER_NAME_INPUT_MESSAGE); + } + public static void printStartMessage() { print(String.format(START_MESSAGE, START.name().toLowerCase(), END.name().toLowerCase(), MOVE.name().toLowerCase())); @@ -35,6 +51,13 @@ public static void printBoard(final ChessBoardDto chessBoardDto) { print(boardMessage.toString()); } + public static void printChessResult(final ChessResultDto chessResultDto) { + final double whiteScore = chessResultDto.getWhiteScore(); + final double blackScore = chessResultDto.getBlackScore(); + final String winnerCamp = getWinnerCamp(whiteScore, blackScore); + print(String.format(RESULT_MESSAGE, whiteScore, blackScore, winnerCamp)); + } + private static String makeFileMessage(final ChessBoardDto chessBoardDto, final int rank) { final StringBuilder fileMessage = new StringBuilder(); for (int file = 0; file < BOARD_SIZE; file++) { @@ -51,4 +74,14 @@ private static char getPieceName(final ChessBoardDto chessBoardDto, final int ra } return BLANK; } + + private static String getWinnerCamp(final double whiteScore, final double blackScore) { + if (whiteScore == blackScore) { + return DRAW; + } + if (whiteScore < blackScore) { + return BLACK; + } + return WHITE; + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 00000000000..2fba60dfe58 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,3 @@ +url: jdbc:mysql://localhost:13306/chess?useSSL=false&serverTimezone=UTC +username: root +password: root diff --git a/src/test/java/chess/controller/CommandTest.java b/src/test/java/chess/controller/CommandTest.java new file mode 100644 index 00000000000..82148ba3e32 --- /dev/null +++ b/src/test/java/chess/controller/CommandTest.java @@ -0,0 +1,102 @@ +package chess.controller; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +class CommandTest { + + @ParameterizedTest(name = "입력한 명령어에 따라서 Command를 조회해온다.") + @CsvSource(value = {"start:START", "move a2 a4:MOVE", "end:END", "status:STATUS"}, delimiter = ':') + void findCommand_success(final String command, final CommandType commandType) { + // given + final List commands = Arrays.asList(command.split(" ")); + final Command expected = new Command(commandType, commands); + + // when + final Command actual = Command.findCommand(commands); + + // then + assertThat(actual) + .isEqualTo(expected); + } + + @ParameterizedTest(name = "start, move, end, status 외의 명령어를 입력하면 예외가 발생한다.") + @ValueSource(strings = {"star", "@#$F@#", "203948029f"}) + void findCommand_fail(final String command) { + // given + final List commands = Arrays.asList(command.split(" ")); + + // when, then + assertThatThrownBy(() -> Command.findCommand(commands)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("잘못된 명령어 입력입니다."); + } + + @Test + @DisplayName("현재 명령어가 start인지 판단한다.") + void isStart() { + // given + final Command command = new Command(CommandType.START, List.of("start")); + + // when, then + assertThat(command.isStart()) + .isTrue(); + } + + @Test + @DisplayName("현재 명령어가 move인지 판단한다.") + void isMove() { + // given + final Command command = new Command(CommandType.MOVE, List.of("move", "a2", "a4")); + + // when, then + assertThat(command.isMove()) + .isTrue(); + } + + @Test + @DisplayName("현재 명령어가 end인지 판단한다.") + void isEnd() { + // given + final Command command = new Command(CommandType.END, List.of("end")); + + // when, then + assertThat(command.isEnd()) + .isTrue(); + } + + @Test + @DisplayName("현재 명령어가 status인지 판단한다.") + void isStatus() { + // given + final Command command = new Command(CommandType.STATUS, List.of("status")); + + // when, then + assertThat(command.isStatus()) + .isTrue(); + } + + @ParameterizedTest(name = "사용자가 입력한 명령어가 move일 때 명령어의 길이가 3인지 판단한다.") + @CsvSource(value = {"move:false", "move : false", "move a3:false", "move a2 a4:true", "move a3 a4 a5:false"}, delimiter = ':') + void isCorrectWhenMove(final String userCommand, final boolean expected) { + // given + List commands = Arrays.asList(userCommand.split(" ")); + final Command command = new Command(CommandType.MOVE, commands); + + // when + boolean actual = command.isCorrectWhenMove(); + + // then + assertThat(actual) + .isSameAs(expected); + } +} diff --git a/src/test/java/chess/service/ChessBoardServiceTest.java b/src/test/java/chess/controller/mapper/ChessBoardDtoMapperTest.java similarity index 65% rename from src/test/java/chess/service/ChessBoardServiceTest.java rename to src/test/java/chess/controller/mapper/ChessBoardDtoMapperTest.java index 2074b5be104..18bb8d63666 100644 --- a/src/test/java/chess/service/ChessBoardServiceTest.java +++ b/src/test/java/chess/controller/mapper/ChessBoardDtoMapperTest.java @@ -1,6 +1,7 @@ -package chess.service; +package chess.controller.mapper; import chess.controller.dto.ChessBoardDto; +import chess.domain.chess.CampType; import chess.domain.chess.ChessGame; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -8,19 +9,17 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -class ChessBoardServiceTest { +class ChessBoardDtoMapperTest { @Test @DisplayName("체스판 정보가 주어지면, 체스판 dto를 생성한다.") void createChessBoardDto() { // given - final ChessBoardService chessBoardService = new ChessBoardService( - new PieceService(), new PositionService()); - final ChessGame chessGame = new ChessGame(); + final ChessGame chessGame = new ChessGame(CampType.WHITE); // when, then final ChessBoardDto chessBoardDto = assertDoesNotThrow(() -> - chessBoardService.createChessBoardDto(chessGame.getChessBoard())); + ChessBoardDtoMapper.createChessBoardDto(chessGame.getChessBoard())); assertThat(chessBoardDto) .isInstanceOf(ChessBoardDto.class); } diff --git a/src/test/java/chess/service/PieceServiceTest.java b/src/test/java/chess/controller/mapper/PieceDtoMapperTest.java similarity index 77% rename from src/test/java/chess/service/PieceServiceTest.java rename to src/test/java/chess/controller/mapper/PieceDtoMapperTest.java index d4cd6f34ce4..73f7746b9e8 100644 --- a/src/test/java/chess/service/PieceServiceTest.java +++ b/src/test/java/chess/controller/mapper/PieceDtoMapperTest.java @@ -1,4 +1,4 @@ -package chess.service; +package chess.controller.mapper; import chess.controller.dto.PieceDto; import chess.domain.chess.CampType; @@ -11,17 +11,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -class PieceServiceTest { +class PieceDtoMapperTest { @Test @DisplayName("체스말 정보가 주어지면, 체스말에 대한 dto를 생성한다.") void createPieceDto() { // given - final PieceService pieceService = new PieceService(); final Piece piece = new Piece(PieceType.ROOK, CampType.WHITE, new Rook()); // when, then - final PieceDto pieceDto = assertDoesNotThrow(() -> pieceService.createPieceDto(piece)); + final PieceDto pieceDto = assertDoesNotThrow(() -> PieceDtoMapper.createPieceDto(piece)); assertThat(pieceDto) .isInstanceOf(PieceDto.class); } diff --git a/src/test/java/chess/service/PositionServiceTest.java b/src/test/java/chess/controller/mapper/PositionDtoMapperTest.java similarity index 78% rename from src/test/java/chess/service/PositionServiceTest.java rename to src/test/java/chess/controller/mapper/PositionDtoMapperTest.java index 20d0405416d..b56e8be0e4d 100644 --- a/src/test/java/chess/service/PositionServiceTest.java +++ b/src/test/java/chess/controller/mapper/PositionDtoMapperTest.java @@ -1,4 +1,4 @@ -package chess.service; +package chess.controller.mapper; import chess.controller.dto.PositionDto; import org.junit.jupiter.api.DisplayName; @@ -7,18 +7,17 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -class PositionServiceTest { +class PositionDtoMapperTest { @Test @DisplayName("rank, file이 주어지면 위치에 대한 dto를 생성한다.") void createPositionDto() { // given - final PositionService positionService = new PositionService(); final int rank = 3; final int file = 3; // when, then - final PositionDto positionDto = assertDoesNotThrow(() -> positionService.createPositionDto(rank, file)); + final PositionDto positionDto = assertDoesNotThrow(() -> PositionDtoMapper.createPositionDto(rank, file)); assertThat(positionDto) .isInstanceOf(PositionDto.class); } diff --git a/src/test/java/chess/controller/status/EndControllerTest.java b/src/test/java/chess/controller/status/EndControllerTest.java index bf1d76fe4bb..c486fd52cb8 100644 --- a/src/test/java/chess/controller/status/EndControllerTest.java +++ b/src/test/java/chess/controller/status/EndControllerTest.java @@ -20,8 +20,7 @@ void checkCommand() { final Command command = new Command(CommandType.END, List.of("end")); // when, then - assertThatThrownBy(() -> endController.checkCommand(command, () -> { - })) + assertThatThrownBy(() -> endController.checkCommand(command)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("게임이 끝났습니다."); } diff --git a/src/test/java/chess/controller/status/MoveControllerTest.java b/src/test/java/chess/controller/status/MoveControllerTest.java index 7ae8eebe044..15b3fdc06b6 100644 --- a/src/test/java/chess/controller/status/MoveControllerTest.java +++ b/src/test/java/chess/controller/status/MoveControllerTest.java @@ -2,8 +2,10 @@ import chess.controller.Command; import chess.controller.CommandType; -import chess.domain.chess.CampType; -import chess.domain.chess.ChessGame; +import chess.service.ChessGameService; +import chess.service.MockServiceManagerHandler; +import chess.service.ServiceHandler; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -17,16 +19,23 @@ class MoveControllerTest { + private ChessGameService chessGameService; + + @BeforeEach + void init() { + final ServiceHandler mockServiceManagerHandler = new MockServiceManagerHandler(); + chessGameService = mockServiceManagerHandler.chessGameService(); + } + @Test @DisplayName(value = "게임이 움직임 상태일 때 start를 입력하면 예외가 발생한다.") void checkCommandStart() { // given - final MoveController moveController = new MoveController(new ChessGame(), CampType.WHITE); + final MoveController moveController = new MoveController(1L, chessGameService); final Command command = new Command(CommandType.START, List.of("start")); // when, then - assertThatThrownBy(() -> moveController.checkCommand(command, () -> { - })) + assertThatThrownBy(() -> moveController.checkCommand(command)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("이미 시작이 완료되었습니다."); } @@ -35,28 +44,42 @@ void checkCommandStart() { @DisplayName(value = "게임이 움직임 상태일 때 end를 입력하면 게임이 종료된다.") void checkCommandEnd() { // given - final MoveController moveController = new MoveController(new ChessGame(), CampType.WHITE); + final MoveController moveController = new MoveController(1L, chessGameService); final Command command = new Command(CommandType.END, List.of("end")); // when - Controller controller = moveController.checkCommand(command, () -> { - }); + Controller controller = moveController.checkCommand(command); // then assertThat(controller) .isInstanceOf(EndController.class); } + @Test + @DisplayName(value = "게임이 움직임 상태일 때 status를 입력하면 게임 결과를 출력하도록 제어한다.") + void checkCommandStatus() { + // given + final MoveController moveController = new MoveController(1L, chessGameService); + final Command command = new Command(CommandType.STATUS, List.of("status")); + + // when + Controller controller = moveController.checkCommand(command); + + // then + assertThat(controller) + .isInstanceOf(StatusController.class); + } + + @ParameterizedTest(name = "게임이 움직임 상태일 때 올바르지 않은 명령어 형식을 입력하면 예외가 발생한다.") @ValueSource(strings = {"move", "move a2", "", " move ", "move a2 a3 a5", "move a2a3"}) void checkCommandValidate(final String commands) { // given - final MoveController moveController = new MoveController(new ChessGame(), CampType.WHITE); + final MoveController moveController = new MoveController(1L, chessGameService); final Command command = new Command(CommandType.MOVE, Arrays.asList(commands.split(" "))); // when, then - assertThatThrownBy(() -> moveController.checkCommand(command, () -> { - })) + assertThatThrownBy(() -> moveController.checkCommand(command)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("'move source위치 target위치 - 예. move b2 b3'와 같은 형태로 입력해 주세요."); } @@ -65,7 +88,7 @@ void checkCommandValidate(final String commands) { @DisplayName(value = "게임이 움직임 상태일 때 실행 중인지 체크하면 true 반환한다") void isRun() { // given - final MoveController moveController = new MoveController(new ChessGame(), CampType.WHITE); + final MoveController moveController = new MoveController(1L, chessGameService); // when boolean isRun = moveController.isRun(); diff --git a/src/test/java/chess/controller/status/StartControllerTest.java b/src/test/java/chess/controller/status/StartControllerTest.java index 0391e1b1f6c..d2600bee3f1 100644 --- a/src/test/java/chess/controller/status/StartControllerTest.java +++ b/src/test/java/chess/controller/status/StartControllerTest.java @@ -2,7 +2,10 @@ import chess.controller.Command; import chess.controller.CommandType; -import chess.domain.chess.ChessGame; +import chess.service.ChessGameService; +import chess.service.MockServiceManagerHandler; +import chess.service.ServiceHandler; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -13,30 +16,47 @@ class StartControllerTest { - @Test + private ChessGameService chessGameService; + + @BeforeEach + void init() { + final ServiceHandler mockServiceManagerHandler = new MockServiceManagerHandler(); + chessGameService = mockServiceManagerHandler.chessGameService(); + } + @DisplayName(value = "게임이 시작 상태일 때 사용자가 입력한 명령어가 move면 예외가 발생한다.") - void checkCommandFailWhenMove() { + void checkCommandMove() { // given - final StartController startController = new StartController(new ChessGame()); + final StartController startController = new StartController(1L, chessGameService); final Command command = new Command(CommandType.MOVE, List.of("move a2")); // when, then - assertThatThrownBy(() -> startController.checkCommand(command, () -> { - })) + assertThatThrownBy(() -> startController.checkCommand(command)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("게임이 시작되지 않았습니다."); + } + + @DisplayName(value = "게임이 시작 상태일 때 사용자가 입력한 명령어가 status면 예외가 발생한다.") + void checkCommandStatus() { + // given + final StartController startController = new StartController(1L, chessGameService); + final Command command = new Command(CommandType.STATUS, List.of("status")); + + // when, then + assertThatThrownBy(() -> startController.checkCommand(command)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("게임이 시작되지 않았습니다."); } @Test @DisplayName(value = "게임이 시작 상태일 때 사용자가 입력한 명령어가 end면 게임이 종료된다.") - void checkCommand() { + void checkCommandEnd() { // given - final StartController startController = new StartController(new ChessGame()); + final StartController startController = new StartController(1L, chessGameService); final Command command = new Command(CommandType.END, List.of("end")); // when - Controller controller = startController.checkCommand(command, () -> { - }); + Controller controller = startController.checkCommand(command); // then assertThat(controller) @@ -47,7 +67,7 @@ void checkCommand() { @DisplayName(value = "게임이 시작 상태일 때 실행 중인지 체크하면 true를 반환한다") void isRun() { // given - final StartController startController = new StartController(new ChessGame()); + final StartController startController = new StartController(1L, chessGameService); // when boolean isRun = startController.isRun(); diff --git a/src/test/java/chess/controller/status/StatusControllerTest.java b/src/test/java/chess/controller/status/StatusControllerTest.java new file mode 100644 index 00000000000..b4359ca4440 --- /dev/null +++ b/src/test/java/chess/controller/status/StatusControllerTest.java @@ -0,0 +1,96 @@ +package chess.controller.status; + +import chess.controller.Command; +import chess.controller.CommandType; +import chess.service.ChessGameService; +import chess.service.MockServiceManagerHandler; +import chess.service.ServiceHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class StatusControllerTest { + private ChessGameService chessGameService; + + @BeforeEach + void init() { + final ServiceHandler mockServiceManagerHandler = new MockServiceManagerHandler(); + chessGameService = mockServiceManagerHandler.chessGameService(); + } + + @Test + @DisplayName(value = "게임이 status 상태일 때 start를 입력하면 예외가 발생한다.") + void checkCommandStart() { + // given + final StatusController statusController = new StatusController(1L, chessGameService); + final Command command = new Command(CommandType.START, List.of("start")); + + // when, then + assertThatThrownBy(() -> statusController.checkCommand(command)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미 시작이 완료되었습니다."); + } + + @Test + @DisplayName(value = "게임이 status 상태일 때 end를 입력하면 게임이 종료된다.") + void checkCommandEnd() { + // given + final StatusController statusController = new StatusController(1L, chessGameService); + final Command command = new Command(CommandType.END, List.of("end")); + + // when + Controller controller = statusController.checkCommand(command); + + // then + assertThat(controller) + .isInstanceOf(EndController.class); + } + + @Test + @DisplayName(value = "게임이 status 상태일 때 move를 입력하면 이동하도록 제어한다.") + void checkCommandMove() { + // given + final StatusController statusController = new StatusController(1L, chessGameService); + final Command command = new Command(CommandType.MOVE, List.of("move", "a2", "a4")); + + // when + Controller controller = statusController.checkCommand(command); + + // then + assertThat(controller) + .isInstanceOf(MoveController.class); + } + + @Test + @DisplayName(value = "게임이 status 상태일 때 실행 중인지 체크하면 true를 반환한다") + void isRun() { + // given + final StatusController statusController = new StatusController(1L, chessGameService); + + // when + boolean isRun = statusController.isRun(); + + // then + assertThat(isRun) + .isTrue(); + } + + @Test + @DisplayName("게임이 진행 중인 상태가 아니라면 end 상태로 변경한다.") + void getStatus() { + // given + final StatusController statusController = new StatusController(1L, chessGameService); + + // when + Controller controller = statusController.getStatus(false); + + // then + assertThat(controller) + .isInstanceOf(EndController.class); + } +} diff --git a/src/test/java/chess/dao/chess/ChessGameDaoImplTest.java b/src/test/java/chess/dao/chess/ChessGameDaoImplTest.java new file mode 100644 index 00000000000..69bee45cc82 --- /dev/null +++ b/src/test/java/chess/dao/chess/ChessGameDaoImplTest.java @@ -0,0 +1,94 @@ +package chess.dao.chess; + +import chess.domain.chess.CampType; +import chess.entity.ChessGameEntity; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class ChessGameDaoImplTest { + + @Test + @DisplayName("사용자의 아이디에 존재하는 체스 게임이 존재하면, 해당 체스 게임을 반환한다.") + void findByUserId() { + // given + final ChessGameDao chessGameDao = new MockChessGameDao(); + final ChessGameEntity createdChessGameEntity = new ChessGameEntity(1L, "WHITE", 1L); + chessGameDao.insert(createdChessGameEntity); + final long expected = createdChessGameEntity.getId(); + final long userId = 1L; + + // when + final Optional chessGameEntity = chessGameDao.findByUserId(userId); + final long actual = chessGameEntity.orElseThrow().getId(); + + // then + assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("사용자가 아이디에 해당하는 체스 게임이 존재하지 않으면, 빈 객체를 반환한다.") + void findByUserId_empty() { + // given + final ChessGameDao chessGameDao = new MockChessGameDao(); + final long userId = 2L; + + // when + final Optional chessGameEntity = chessGameDao.findByUserId(userId); + + // then + assertThat(chessGameEntity) + .isEqualTo(Optional.empty()); + } + + @Test + @DisplayName("사용자가 현재 진행 중인 체스 게임을 저장한다") + void insert() { + // given + final ChessGameDao chessGameDao = new MockChessGameDao(); + + // when + final long savedChessGameId = chessGameDao.insert(new ChessGameEntity("WHITE", 1L)); + + // then + assertThat(savedChessGameId) + .isEqualTo(1L); + } + + @Test + @DisplayName("체스 게임 아이디에 해당하는 게임의 현재 진영을 업데이트한다") + void updateCurrentCampById() { + // given + final ChessGameDao chessGameDao = new MockChessGameDao(); + final long savedChessGameId = chessGameDao.insert(new ChessGameEntity("WHITE", 1L)); + final long expected = new ChessGameEntity(1L, "BLACK", 1L).getId(); + + // when + chessGameDao.updateCurrentCampById(savedChessGameId, CampType.BLACK); + + // then + final long actual = chessGameDao.findByUserId(1L).orElseThrow().getId(); + assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("사용자의 아이디에 해당하는 체스 게임을 제거한다") + void deleteByUserId() { + // given + final ChessGameDao chessGameDao = new MockChessGameDao(); + chessGameDao.insert(new ChessGameEntity("WHITE", 1L)); + + // when + chessGameDao.deleteByUserId(1L); + + // then + final Optional actual = chessGameDao.findByUserId(1L); + assertThat(actual) + .isEqualTo(Optional.empty()); + } +} diff --git a/src/test/java/chess/dao/chess/MockChessGameDao.java b/src/test/java/chess/dao/chess/MockChessGameDao.java new file mode 100644 index 00000000000..505e53e683e --- /dev/null +++ b/src/test/java/chess/dao/chess/MockChessGameDao.java @@ -0,0 +1,46 @@ +package chess.dao.chess; + +import chess.domain.chess.CampType; +import chess.entity.ChessGameEntity; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public class MockChessGameDao implements ChessGameDao { + private final Map STORAGE = new ConcurrentHashMap<>(); + private final AtomicLong pk = new AtomicLong(0L); + + @Override + public Optional findByUserId(final long userId) { + return STORAGE.values().stream() + .filter(chessGameEntity -> chessGameEntity.getUserId().equals(userId)) + .map(chessGameEntity -> new ChessGameEntity(chessGameEntity.getId(), chessGameEntity.getCurrentCamp(), + chessGameEntity.getUserId())) + .findFirst(); + } + + @Override + public Long insert(final ChessGameEntity chessGameEntity) { + final long key = pk.addAndGet(1L); + final ChessGameEntity savedChessGameEntity = new ChessGameEntity(key, + chessGameEntity.getCurrentCamp(), chessGameEntity.getUserId()); + STORAGE.put(key, savedChessGameEntity); + return pk.longValue(); + } + + @Override + public void updateCurrentCampById(final long id, final CampType currentCamp) { + final ChessGameEntity chessGameEntity = STORAGE.get(id); + STORAGE.put(id, new ChessGameEntity(chessGameEntity.getId(), currentCamp.name(), chessGameEntity.getUserId())); + } + + @Override + public void deleteByUserId(final long userId) { + STORAGE.keySet().stream() + .filter(key -> Objects.equals(STORAGE.get(key).getUserId(), userId)) + .forEach(STORAGE::remove); + } +} diff --git a/src/test/java/chess/dao/chess/MockPieceDao.java b/src/test/java/chess/dao/chess/MockPieceDao.java new file mode 100644 index 00000000000..71294097052 --- /dev/null +++ b/src/test/java/chess/dao/chess/MockPieceDao.java @@ -0,0 +1,58 @@ +package chess.dao.chess; + +import chess.entity.PieceEntity; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +public class MockPieceDao implements PieceDao { + private final Map STORAGE = new ConcurrentHashMap<>(); + private final AtomicLong pk = new AtomicLong(0L); + + @Override + public List findByChessGameId(final long chessGameId) { + return STORAGE.values().stream() + .filter(pieceEntity -> pieceEntity.getChessGameId().equals(chessGameId)) + .map(pieceEntity -> PieceEntity.create( + pieceEntity.getId(), + pieceEntity.getChessGameId(), pieceEntity.getRank(), + pieceEntity.getFile(), pieceEntity.getPieceType(), pieceEntity.getCampType())) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public Long insert(final PieceEntity pieceEntity) { + STORAGE.put(pk.addAndGet(1L), pieceEntity); + return pk.longValue(); + } + + @Override + public void deleteByPositions(final long chessGameId, final PieceEntity... pieceEntity) { + delete(pieceEntity[0]); + if (pieceEntity.length == 2) { + delete(pieceEntity[1]); + } + } + + @Override + public void deleteByChessGameId(final long chessGameId) { + STORAGE.keySet().stream() + .filter(key -> Objects.equals(STORAGE.get(key).getChessGameId(), chessGameId)) + .forEach(STORAGE::remove); + } + + private void delete(final PieceEntity pieceEntity) { + STORAGE.entrySet().stream() + .filter(entry -> isSamePosition(entry.getValue(), pieceEntity)) + .forEach(entry -> STORAGE.remove(entry.getKey())); + } + + private boolean isSamePosition(final PieceEntity value, final PieceEntity other) { + return Objects.equals(value.getRank(), other.getRank()) + && Objects.equals(value.getFile(), other.getFile()); + } +} diff --git a/src/test/java/chess/dao/chess/PieceDaoImplTest.java b/src/test/java/chess/dao/chess/PieceDaoImplTest.java new file mode 100644 index 00000000000..b20b26db849 --- /dev/null +++ b/src/test/java/chess/dao/chess/PieceDaoImplTest.java @@ -0,0 +1,95 @@ +package chess.dao.chess; + +import chess.entity.PieceEntity; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class PieceDaoImplTest { + @Test + @DisplayName("체스 게임 아이디를 기준으로 체스말 리스트의 정보를 조회한다.") + void findByChessGameId() { + // given + final PieceDao pieceDao = new MockPieceDao(); + final long chessGameId = 1L; + final List pieceEntities = PieceEntityHelper.createPieceEntities(chessGameId); + pieceEntities.forEach(pieceDao::insert); + + // when + final List findChessEntities = pieceDao.findByChessGameId(chessGameId); + + // then + final List actual = getPieceEntityIds(findChessEntities); + final List expected = getPieceEntityIds(pieceEntities); + + assertThat(actual) + .isEqualTo(expected); + + assertThat(actual.size()) + .isSameAs(18); + } + + @Test + @DisplayName("체스 게임에 해당하는 체스말의 정보를 저장한다") + void insert() { + // given + final PieceDao pieceDao = new MockPieceDao(); + + // when + final Long savedPieceId = pieceDao.insert(PieceEntity.createWithChessGameId(1L, 1, 2, + "PAWN", "WHITE")); + + // then + assertThat(savedPieceId) + .isEqualTo(1L); + } + + @Test + @DisplayName("입력으로 들어온 체스 게임에 대해 특정 위치에 존재하는 체스말의 정보를 제거한다") + void deleteByPositions() { + // given + final PieceDao pieceDao = new MockPieceDao(); + final long chessGameId = 1L; + final PieceEntity entity1 = PieceEntity.createWithChessGameId(chessGameId, 1, 1, "PAWN", "WHITE"); + final PieceEntity entity2 = PieceEntity.createWithChessGameId(chessGameId, 1, 1, "PAWN", "WHITE"); + pieceDao.insert(entity1); + pieceDao.insert(entity2); + + // when + pieceDao.deleteByPositions(chessGameId, entity1, entity2); + + // then + assertThat(pieceDao.findByChessGameId(1L)) + .isEqualTo(Collections.emptyList()); + } + + @Test + @DisplayName("체스 게임 아이디에 해당하는 체스말 정보를 제거한다") + void deleteByChessGameId() { + // given + final PieceDao pieceDao = new MockPieceDao(); + final long chessGameId = 1L; + final PieceEntity entity1 = PieceEntity.createWithChessGameId(chessGameId, 1, 1, "PAWN", "WHITE"); + final PieceEntity entity2 = PieceEntity.createWithChessGameId(chessGameId, 1, 1, "PAWN", "WHITE"); + pieceDao.insert(entity1); + pieceDao.insert(entity2); + + // when + pieceDao.deleteByChessGameId(chessGameId); + + // then + assertThat(pieceDao.findByChessGameId(1L)) + .isEqualTo(Collections.emptyList()); + } + + private List getPieceEntityIds(final List pieceEntities) { + return pieceEntities.stream() + .map(PieceEntity::getId) + .collect(Collectors.toUnmodifiableList()); + } +} diff --git a/src/test/java/chess/dao/chess/PieceEntityHelper.java b/src/test/java/chess/dao/chess/PieceEntityHelper.java new file mode 100644 index 00000000000..b4a4033433d --- /dev/null +++ b/src/test/java/chess/dao/chess/PieceEntityHelper.java @@ -0,0 +1,38 @@ +package chess.dao.chess; + +import chess.entity.PieceEntity; + +import java.util.ArrayList; +import java.util.List; + +public class PieceEntityHelper { + + public static List createPieceEntities(final Long chessGameId) { + final List results = new ArrayList<>(); + results.addAll(createMockPieces(1L, chessGameId, 0, "WHITE")); + results.addAll(createMockPawnPieces(6L, chessGameId, 1, "WHITE")); + results.addAll(createMockPawnPieces(10L, chessGameId, 6, "BLACK")); + results.addAll(createMockPieces(14L, chessGameId, 7, "BLACK")); + return results; + } + + private static List createMockPieces(final long id, final long chessGameId, + final int rank, final String campType) { + final List results = new ArrayList<>(); + results.add(PieceEntity.create(id, chessGameId, rank, 0, "ROOK", campType)); + results.add(PieceEntity.create(id + 1, chessGameId, rank, 1, "KNIGHT", campType)); + results.add(PieceEntity.create(id + 2, chessGameId, rank, 2, "BISHOP", campType)); + results.add(PieceEntity.create(id + 3, chessGameId, rank, 3, "QUEEN", campType)); + results.add(PieceEntity.create(id + 4, chessGameId, rank, 4, "KING", campType)); + return results; + } + + private static List createMockPawnPieces(final long id, final long chessGameId, + final int rank, final String campType) { + final List results = new ArrayList<>(); + for (int index = 0; index < 4; index++) { + results.add(PieceEntity.create(id + index, chessGameId, rank, index, "PAWN", campType)); + } + return results; + } +} diff --git a/src/test/java/chess/dao/user/MockUserDao.java b/src/test/java/chess/dao/user/MockUserDao.java new file mode 100644 index 00000000000..de37f2e7f76 --- /dev/null +++ b/src/test/java/chess/dao/user/MockUserDao.java @@ -0,0 +1,27 @@ +package chess.dao.user; + +import chess.entity.UserEntity; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public class MockUserDao implements UserDao { + private final Map STORAGE = new ConcurrentHashMap<>(); + private final AtomicLong pk = new AtomicLong(0L); + + @Override + public Optional findByName(final String name) { + return STORAGE.entrySet().stream() + .filter(entry -> entry.getValue().getName().equals(name)) + .map(entry -> new UserEntity(entry.getKey(), entry.getValue().getName())) + .findFirst(); + } + + @Override + public Long insert(final String name) { + STORAGE.put(pk.addAndGet(1L), new UserEntity(name)); + return pk.longValue(); + } +} diff --git a/src/test/java/chess/dao/user/UserDaoImplTest.java b/src/test/java/chess/dao/user/UserDaoImplTest.java new file mode 100644 index 00000000000..0e46e2b59c5 --- /dev/null +++ b/src/test/java/chess/dao/user/UserDaoImplTest.java @@ -0,0 +1,57 @@ +package chess.dao.user; + +import chess.entity.UserEntity; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class UserDaoImplTest { + + @Test + @DisplayName("사용자가 입력한 이름이 존재하면 해당 이름을 가진 User 엔티티를 반환한다.") + void findByName() { + // given + final UserDao userDao = new MockUserDao(); + final String name = "journey"; + userDao.insert(name); + final long expected = new UserEntity(1L, name).getId(); + + // when + final Optional userEntity = userDao.findByName(name); + final long actual = userEntity.orElseThrow().getId(); + + // then + assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("사용자가 입력한 이름이 존재하지 않으면 빈 객체를 반환한다.") + void findByNameEmpty() { + // given + final UserDao userDao = new MockUserDao(); + + // when + final Optional userEntity = userDao.findByName("pobi"); + + assertThat(userEntity) + .isEqualTo(Optional.empty()); + } + + @Test + @DisplayName("사용자 정보를 삽입하면, 삽입된 사용자의 아이디를 반환한다.") + void insert() { + // given + final UserDao userDao = new MockUserDao(); + + // when + Long userId = userDao.insert("journey"); + + // then + assertThat(userId) + .isEqualTo(1L); + } +} diff --git a/src/test/java/chess/domain/board/ChessBoardTest.java b/src/test/java/chess/domain/board/ChessBoardTest.java index e2fbfa23363..39978450e40 100644 --- a/src/test/java/chess/domain/board/ChessBoardTest.java +++ b/src/test/java/chess/domain/board/ChessBoardTest.java @@ -2,6 +2,7 @@ import chess.domain.chess.CampType; import chess.domain.chess.ChessGame; +import chess.domain.piece.King; import chess.domain.piece.Pawn; import chess.domain.piece.Piece; import chess.domain.piece.PieceType; @@ -12,9 +13,11 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import java.util.List; import java.util.Map; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; class ChessBoardTest { @@ -22,7 +25,7 @@ class ChessBoardTest { @DisplayName("체스판을 생성하고, 체스판 말의 수가 32개인지 확인한다.") void create() { // given - final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame()); + final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame(CampType.WHITE)); // when final Map board = chessBoard.getBoard(); @@ -36,7 +39,7 @@ void create() { @CsvSource(value = {"0:0:true", "3:5:false"}, delimiter = ':') void contains(final int rank, final int file, final boolean expected) { // given - final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame()); + final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame(CampType.WHITE)); // when, then assertThat(chessBoard.contains(new Position(rank, file))) @@ -47,7 +50,7 @@ void contains(final int rank, final int file, final boolean expected) { @DisplayName("특정 위치에 존재하는 체스말을 반환한다.") void getPiece() { // given - final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame()); + final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame(CampType.WHITE)); // when, then assertThat(chessBoard.getPiece(new Position(0, 0))) @@ -58,7 +61,7 @@ void getPiece() { @DisplayName("입력받은 위치에 존재하는 체스말을 제거한다.") void removePiece() { // given - final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame()); + final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame(CampType.WHITE)); final Position removePosition = new Position(1, 0); // when @@ -73,7 +76,7 @@ void removePiece() { @DisplayName("입력받은 위치에 체스말을 둔다.") void putPiece() { // given - final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame()); + final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame(CampType.WHITE)); final Position putPosition = new Position(2, 0); final Piece piece = new Piece(PieceType.PAWN, CampType.WHITE, new Pawn()); @@ -89,7 +92,7 @@ void putPiece() { @CsvSource(value = {"1:3:false", "3:6:true"}, delimiter = ':') void isPossibleRoute(final int rank, final int file, final boolean expected) { // given - final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame()); + final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame(CampType.WHITE)); final Position source = new Position(0, 3); final Position target = new Position(rank, file); @@ -103,4 +106,35 @@ void isPossibleRoute(final int rank, final int file, final boolean expected) { assertThat(actual) .isSameAs(expected); } + + @Test + @DisplayName("현재 체스판에서 살아있는 킹들을 조회한다.") + void getAliveKings() { + // given + final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame(CampType.WHITE)); + + // when + List aliveKings = chessBoard.getAliveKings(); + List expected = List.of(new Piece(PieceType.KING, CampType.WHITE, new King()), + new Piece(PieceType.KING, CampType.BLACK, new King())); + + // then + assertAll(() -> assertThat(aliveKings).isEqualTo(expected), + () -> assertThat(aliveKings.size()).isSameAs(2)); + } + + @Test + @DisplayName("입력받은 진영의 체스판을 반환한다.") + void getBoardByCamp() { + // given + final ChessBoard chessBoard = ChessBoard.getInstance(new ChessGame(CampType.WHITE)); + + // when + final Map whiteBoard = chessBoard.getBoardByCamp(CampType.WHITE); + final Map blackBoard = chessBoard.getBoardByCamp(CampType.BLACK); + + // then + assertAll(() -> assertThat(whiteBoard.size()).isEqualTo(16), + () -> assertThat(blackBoard.size()).isEqualTo(16)); + } } diff --git a/src/test/java/chess/domain/chess/CampTypeTest.java b/src/test/java/chess/domain/chess/CampTypeTest.java index 1290f49c7d5..90b9d26483d 100644 --- a/src/test/java/chess/domain/chess/CampTypeTest.java +++ b/src/test/java/chess/domain/chess/CampTypeTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.params.provider.CsvSource; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; class CampTypeTest { @@ -32,10 +33,7 @@ void changeTurn() { CampType blackActual = black.changeTurn(); // then - assertThat(whiteActual) - .isEqualTo(black); - - assertThat(blackActual) - .isEqualTo(white); + assertAll(() -> assertThat(whiteActual).isEqualTo(black), + () -> assertThat(blackActual).isEqualTo(white)); } } diff --git a/src/test/java/chess/domain/chess/ChessGameCalculatorTest.java b/src/test/java/chess/domain/chess/ChessGameCalculatorTest.java new file mode 100644 index 00000000000..d3bac7e0fcd --- /dev/null +++ b/src/test/java/chess/domain/chess/ChessGameCalculatorTest.java @@ -0,0 +1,26 @@ +package chess.domain.chess; + +import chess.domain.chess.vo.ChessScore; +import chess.domain.piece.Score; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class ChessGameCalculatorTest { + + @Test + @DisplayName("현재 체스판의 각 진영에 대한 점수를 계산한다.") + void calculate() { + // given + final ChessGame chessGame = new ChessGame(CampType.WHITE); + final ChessScore expected = new ChessScore(Score.create(38.0), Score.create(38.0)); + + // when + ChessScore actual = ChessGameCalculator.calculate(chessGame); + + // then + assertThat(actual) + .isEqualTo(expected); + } +} diff --git a/src/test/java/chess/domain/chess/ChessGameHelper.java b/src/test/java/chess/domain/chess/ChessGameHelper.java new file mode 100644 index 00000000000..e36ad34a3ed --- /dev/null +++ b/src/test/java/chess/domain/chess/ChessGameHelper.java @@ -0,0 +1,24 @@ +package chess.domain.chess; + +import chess.domain.piece.move.Position; + +public class ChessGameHelper { + public static void playKingDie(final ChessGame chessGame) { + chessGame.play(new Position(7, 1), new Position(5, 0)); + chessGame.play(new Position(3, 0), new Position(4, 0)); + chessGame.play(new Position(6, 1), new Position(5, 1)); + chessGame.play(new Position(4, 0), new Position(5, 1)); + chessGame.play(new Position(6, 2), new Position(5, 2)); + chessGame.play(new Position(0, 0), new Position(5, 0)); + chessGame.play(new Position(5, 2), new Position(4, 2)); + chessGame.play(new Position(1, 3), new Position(3, 3)); + chessGame.play(new Position(6, 4), new Position(4, 4)); + chessGame.play(new Position(0, 2), new Position(5, 7)); + chessGame.play(new Position(7, 3), new Position(3, 7)); + chessGame.play(new Position(0, 6), new Position(2, 5)); + chessGame.play(new Position(7, 4), new Position(6, 4)); + chessGame.play(new Position(3, 3), new Position(4, 3)); + chessGame.play(new Position(6, 4), new Position(5, 4)); + chessGame.play(new Position(4, 3), new Position(5, 4)); + } +} diff --git a/src/test/java/chess/domain/chess/ChessGameTest.java b/src/test/java/chess/domain/chess/ChessGameTest.java index 3e82b9f2968..16283632c77 100644 --- a/src/test/java/chess/domain/chess/ChessGameTest.java +++ b/src/test/java/chess/domain/chess/ChessGameTest.java @@ -1,9 +1,15 @@ package chess.domain.chess; +import chess.domain.piece.Piece; import chess.domain.piece.move.Position; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import java.util.Map; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; class ChessGameTest { @@ -12,13 +18,59 @@ class ChessGameTest { @CsvSource(value = {"6:6"}, delimiter = ':') void playMovableFail(final int rank, final int file) { // given - final ChessGame chessGame = new ChessGame(); + final ChessGame chessGame = new ChessGame(CampType.WHITE); final Position source = new Position(rank, file); final Position target = new Position(6, 7); // when, then - assertThatThrownBy(() -> chessGame.setUp(source, target, CampType.WHITE)) + assertThatThrownBy(() -> chessGame.play(source, target)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("현재 차례가 아닙니다. 현재 차례 = WHITE"); } + + @Test + @DisplayName("모든 진영의 왕이 살아있는 것이 아니라면 false를 반환한다.") + void run() { + // given + final ChessGame chessGame = new ChessGame(CampType.WHITE); + final Position source = new Position(1, 0); + final Position target = new Position(3, 0); + + // when + chessGame.play(source, target); + ChessGameHelper.playKingDie(chessGame); + final boolean isGameRun = chessGame.isKingAlive(); + + // then + assertThat(isGameRun) + .isFalse(); + } + + @Test + @DisplayName("WHITE 진영의 체스판을 반환한다.") + void getWhiteBoard() { + // given + final ChessGame chessGame = new ChessGame(CampType.WHITE); + + // when + final Map whiteBoard = chessGame.getWhiteBoard(); + + // then + assertThat(whiteBoard.size()) + .isEqualTo(16); + } + + @Test + @DisplayName("BLACK 진영의 체스판을 반환한다.") + void getBlackBoard() { + // given + final ChessGame chessGame = new ChessGame(CampType.WHITE); + + // when + final Map blackBoard = chessGame.getBlackBoard(); + + // then + assertThat(blackBoard.size()) + .isEqualTo(16); + } } diff --git a/src/test/java/chess/domain/piece/MoveTest.java b/src/test/java/chess/domain/piece/MoveTest.java index 5481effe551..d2b30c07dc7 100644 --- a/src/test/java/chess/domain/piece/MoveTest.java +++ b/src/test/java/chess/domain/piece/MoveTest.java @@ -18,6 +18,7 @@ import static chess.domain.piece.move.Direction.UP_LEFT; import static chess.domain.piece.move.Direction.UP_RIGHT; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; public class MoveTest { @@ -105,10 +106,7 @@ void pawn_getAllPositions() { final Location blackActual = Move.getLocation(source, blackDirections, moveCount); // then - assertThat(whiteExpected) - .isEqualTo(whiteActual); - - assertThat(blackExpected) - .isEqualTo(blackActual); + assertAll(() -> assertThat(whiteExpected).isEqualTo(whiteActual), + () -> assertThat(blackExpected).isEqualTo(blackActual)); } } diff --git a/src/test/java/chess/domain/piece/PieceTest.java b/src/test/java/chess/domain/piece/PieceTest.java index d2e86ee0097..c77a26c8409 100644 --- a/src/test/java/chess/domain/piece/PieceTest.java +++ b/src/test/java/chess/domain/piece/PieceTest.java @@ -9,6 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; class PieceTest { @@ -25,11 +26,8 @@ void isSameCamp_withPiece() { final boolean actualTrue = pawn.isSameCamp(queen); // then - assertThat(actualFalse) - .isFalse(); - - assertThat(actualTrue) - .isTrue(); + assertAll(() -> assertThat(actualFalse).isFalse(), + () -> assertThat(actualTrue).isTrue()); } @Test @@ -43,11 +41,8 @@ void isSameCamp_withCampType() { boolean actualFalse = pawn.isSameCamp(CampType.BLACK); // then - assertThat(actualTrue) - .isTrue(); - - assertThat(actualFalse) - .isFalse(); + assertAll(() -> assertThat(actualTrue).isTrue(), + () -> assertThat(actualFalse).isFalse()); } @ParameterizedTest(name = "폰은 처음에 기물이 없는 곳으로 최대 2칸 이동할 수 있다.") @@ -81,6 +76,20 @@ void pawn_canMoveFail() { .hasMessage("폰은 처음 이후 1칸만 전진할 수 있습니다."); } + @ParameterizedTest(name = "폰은 앞으로만 이동할 수 있다.") + @CsvSource(value = {"1:WHITE", "3:BLACK"}, delimiter = ':') + void pawn_canMoveFail(final int rank, final CampType currentCamp) { + // given + final Piece pawn = new Piece(PieceType.PAWN, currentCamp, new Pawn()); + final Position source = new Position(2, 1); + final Position target = new Position(rank, 1); + + // when, then + assertThatThrownBy(() -> pawn.canMove(source, target, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("폰은 앞으로만 이동할 수 있습니다."); + } + @Test @DisplayName("폰은 공격 가능한 위치에 상대 기물이 존재하지 않는 경우 예외가 발생한다.") void pawn_canAttackFail() { @@ -95,6 +104,7 @@ void pawn_canAttackFail() { .hasMessage("폰이 공격할 수 있는 위치가 아닙니다."); } + @Test @DisplayName("폰은 공격 가능한 위치에 상대 기물이 존재하는 경우 공격할 수 있다.") void pawn_canAttack() { @@ -110,4 +120,42 @@ void pawn_canAttack() { assertThat(actual) .isTrue(); } + + @ParameterizedTest(name = "폰은 앞에 있는 기물만 공격할 수 있다.") + @CsvSource(value = {"1:0:WHITE", "3:2:BLACK"}, delimiter = ':') + void pawn_canAttackFail(final int rank, final int file, final CampType currentCamp) { + // given + final Piece pawn = new Piece(PieceType.PAWN, currentCamp, new Pawn()); + final Position source = new Position(2, 1); + final Position target = new Position(rank, file); + + // when, then + assertThatThrownBy(() -> pawn.canMove(source, target, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("폰은 앞으로만 이동할 수 있습니다."); + } + + @Test + @DisplayName("체스말이 킹인지 판단한다.") + void isKing() { + // given + final Piece king = new Piece(PieceType.KING, CampType.WHITE, new King()); + final Piece pawn = new Piece(PieceType.PAWN, CampType.WHITE, new Pawn()); + + // when, then + assertAll(() -> assertThat(king.isKing()).isTrue(), + () -> assertThat(pawn.isKing()).isFalse()); + } + + @Test + @DisplayName("체스말이 폰인지 판단한다.") + void isPawn() { + // given + final Piece king = new Piece(PieceType.KING, CampType.WHITE, new King()); + final Piece pawn = new Piece(PieceType.PAWN, CampType.WHITE, new Pawn()); + + // when, then + assertAll(() -> assertThat(pawn.isPawn()).isTrue(), + () -> assertThat(king.isPawn()).isFalse()); + } } diff --git a/src/test/java/chess/domain/piece/ScoreTest.java b/src/test/java/chess/domain/piece/ScoreTest.java new file mode 100644 index 00000000000..18781b4fef8 --- /dev/null +++ b/src/test/java/chess/domain/piece/ScoreTest.java @@ -0,0 +1,54 @@ +package chess.domain.piece; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class ScoreTest { + + @Test + @DisplayName("인자로 들어온 점수를 합한 값을 반환한다.") + void add() { + // given + final Score score = Score.create(1); + final Score expected = Score.create(3.5); + + // when + final Score actual = score.add(Score.create(2.5)); + + // then + assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("인자로 들어온 점수를 뺀 값을 반환한다.") + void subtract() { + // given + final Score score = Score.create(3.5); + final Score expected = Score.create(1); + + // when + final Score actual = score.subtract(Score.create(2.5)); + + // then + assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("인자로 들어온 횟수만큼 곱한 값을 반환한다.") + void multiply() { + // given + final Score score = Score.create(0.5); + final Score expected = Score.create(1); + + // when + final Score actual = score.multiply(2); + + // then + assertThat(actual) + .isEqualTo(expected); + } +} diff --git a/src/test/java/chess/domain/user/UserTest.java b/src/test/java/chess/domain/user/UserTest.java new file mode 100644 index 00000000000..2016070168e --- /dev/null +++ b/src/test/java/chess/domain/user/UserTest.java @@ -0,0 +1,17 @@ +package chess.domain.user; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +class UserTest { + + @ParameterizedTest(name = "사용자의 이름이 1~20자 이내가 아니라면 예외가 발생한다.") + @ValueSource(strings = {"abcdeabcdeabcdeabcdea", ""}) + void create(final String name) { + assertThatThrownBy(() -> User.create(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 1~20글자 사이여야 합니다."); + } +} diff --git a/src/test/java/chess/service/ChessBoardHelper.java b/src/test/java/chess/service/ChessBoardHelper.java new file mode 100644 index 00000000000..04748c70fbf --- /dev/null +++ b/src/test/java/chess/service/ChessBoardHelper.java @@ -0,0 +1,56 @@ +package chess.service; + +import chess.domain.board.ChessBoard; +import chess.domain.chess.CampType; +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.move.Position; + +import java.util.HashMap; +import java.util.Map; + +import static chess.domain.piece.PieceType.BISHOP; +import static chess.domain.piece.PieceType.KING; +import static chess.domain.piece.PieceType.KNIGHT; +import static chess.domain.piece.PieceType.PAWN; +import static chess.domain.piece.PieceType.QUEEN; +import static chess.domain.piece.PieceType.ROOK; + +public final class ChessBoardHelper { + + public static ChessBoard createMockProgressBoard() { + final Map board = new HashMap<>(); + createWhiteProgressArea(board); + createBlackProgressArea(board); + return ChessBoard.create(board); + } + + private static void createWhiteProgressArea(final Map board) { + createProgressPieces(board, 0, CampType.WHITE); + createProgressPawnPieces(board, 1, CampType.WHITE); + } + + private static void createBlackProgressArea(final Map board) { + createProgressPawnPieces(board, 6, CampType.BLACK); + createProgressPieces(board, 7, CampType.BLACK); + } + + private static void createProgressPieces(final Map board, final int rank, final CampType campType) { + board.put(new Position(rank, 0), new Piece(ROOK, campType, new Rook())); + board.put(new Position(rank, 1), new Piece(KNIGHT, campType, new Knight())); + board.put(new Position(rank, 2), new Piece(BISHOP, campType, new Bishop())); + board.put(new Position(rank, 3), new Piece(QUEEN, campType, new Queen())); + board.put(new Position(rank, 4), new Piece(KING, campType, new King())); + } + + private static void createProgressPawnPieces(final Map board, final int rank, final CampType campType) { + for (int file = 0; file < 4; file++) { + board.put(new Position(rank, file), new Piece(PAWN, campType, new Pawn())); + } + } +} diff --git a/src/test/java/chess/service/ChessBoardServiceTestHandler.java b/src/test/java/chess/service/ChessBoardServiceTestHandler.java new file mode 100644 index 00000000000..ef634327980 --- /dev/null +++ b/src/test/java/chess/service/ChessBoardServiceTestHandler.java @@ -0,0 +1,125 @@ +package chess.service; + +import chess.dao.chess.MockPieceDao; +import chess.dao.chess.PieceDao; +import chess.dao.chess.PieceEntityHelper; +import chess.domain.board.ChessBoard; +import chess.domain.chess.CampType; +import chess.domain.piece.Pawn; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceType; +import chess.domain.piece.move.Position; +import chess.entity.PieceEntity; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class ChessBoardServiceTestHandler { + + @Test + @DisplayName("체스 게임 아이디로 체스판 정보를 조회한다.") + void getByChessGameId() { + // given + final Long chessGameId = 1L; + final PieceDao pieceDao = createPieceDao(chessGameId); + final ChessBoardService chessBoardService = new ChessBoardService(pieceDao); + final ChessBoard expected = ChessBoardHelper.createMockProgressBoard(); + + // when + final ChessBoard actual = chessBoardService.getByChessGameId(1L); + + // then + assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("체스말 정보를 바탕으로 체스말을 저장한다.") + void save() { + // given + final PieceDao pieceDao = new MockPieceDao(); + final ChessBoardService chessBoardService = new ChessBoardService(pieceDao); + final ChessBoard expected = createMockChessBoard(); + + // when + chessBoardService.savePiece(PieceEntity.createWithChessGameId(1L, 1, 1, + "PAWN", "WHITE")); + + // then + final ChessBoard actual = chessBoardService.getByChessGameId(1L); + assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("입력받은 위치에 해당하는 체스말을 제거한다") + void deletePieces() { + // given + final PieceDao pieceDao = new MockPieceDao(); + final ChessBoardService chessBoardService = new ChessBoardService(pieceDao); + final PieceEntity source = PieceEntity.createWithLocation(1L, 0, 0); + final PieceEntity target = PieceEntity.createWithLocation(1L, 0, 1); + chessBoardService.savePiece(source); + chessBoardService.savePiece(target); + + // when + chessBoardService.deletePieces(source, target); + + // then + final ChessBoard actual = chessBoardService.getByChessGameId(1L); + assertThat(actual) + .isEqualTo(ChessBoard.create(Collections.emptyMap())); + } + + @Test + @DisplayName("체스판의 모든 기물을 저장한다") + void saveAll() { + // given + final PieceDao pieceDao = new MockPieceDao(); + final ChessBoardService chessBoardService = new ChessBoardService(pieceDao); + + // when + chessBoardService.saveAll(1L, createMockChessBoard().getBoard()); + + // then + final ChessBoard actual = chessBoardService.getByChessGameId(1L); + assertThat(actual) + .isEqualTo(createMockChessBoard()); + } + + @Test + @DisplayName("체스 게임 아이디에 해당하는 모든 기물을 제거한다") + void deleteByChessGameId() { + // given + final Long chessGameId = 1L; + final PieceDao pieceDao = createPieceDao(chessGameId); + final ChessBoardService chessBoardService = new ChessBoardService(pieceDao); + + // when + chessBoardService.deleteByChessGameId(chessGameId); + + // then + final ChessBoard expected = chessBoardService.getByChessGameId(1L); + assertThat(expected) + .isEqualTo(ChessBoard.create(Collections.emptyMap())); + } + + private PieceDao createPieceDao(final Long chessGameId) { + final PieceDao pieceDao = new MockPieceDao(); + final List pieceEntities = PieceEntityHelper.createPieceEntities(chessGameId); + pieceEntities.forEach(pieceDao::insert); + return pieceDao; + } + + private ChessBoard createMockChessBoard() { + final Map mockBoard = new HashMap<>(); + mockBoard.put(new Position(1, 1), new Piece(PieceType.PAWN, CampType.WHITE, new Pawn())); + return ChessBoard.create(mockBoard); + } +} diff --git a/src/test/java/chess/service/ChessGameServiceTestHandler.java b/src/test/java/chess/service/ChessGameServiceTestHandler.java new file mode 100644 index 00000000000..893a660f19a --- /dev/null +++ b/src/test/java/chess/service/ChessGameServiceTestHandler.java @@ -0,0 +1,159 @@ +package chess.service; + +import chess.dao.chess.MockChessGameDao; +import chess.dao.chess.MockPieceDao; +import chess.dao.chess.PieceEntityHelper; +import chess.domain.board.ChessBoard; +import chess.domain.chess.CampType; +import chess.domain.chess.ChessGame; +import chess.domain.piece.Piece; +import chess.domain.piece.move.Position; +import chess.entity.ChessGameEntity; +import chess.entity.PieceEntity; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class ChessGameServiceTestHandler { + + @Test + @DisplayName("사용자의 아이디에 해당하는 체스 게임이 존재하지 않으면 새 게임을 반환한다.") + void getOrCreateChessGame_empty() { + // given + final MockChessGameDao chessGameDao = new MockChessGameDao(); + final ChessGameService chessGameService = new ChessGameService( + chessGameDao, new ChessBoardService(new MockPieceDao())); + final Map expected = new ChessGame(CampType.WHITE).getChessBoard(); + + // when + final Map actual = chessGameService.getOrCreateChessGame(2L) + .getChessBoard(); + + // then + assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("사용자의 아이디에 해당하는 체스 게임이 존재하면 해당 게임을 반환한다.") + void getOrCreateChessGame() { + // given + final long userId = 1L; + final ChessGameService chessGameService = getChessGameService(userId); + final ChessBoard mockProgressBoard = ChessBoardHelper.createMockProgressBoard(); + final Map expected = new ChessGame(mockProgressBoard, CampType.WHITE) + .getChessBoard(); + + // when + final Map actual = chessGameService.getOrCreateChessGame(userId) + .getChessBoard(); + + // then + assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("사용자의 아이디를 기준으로 체스 게임을 조회한다") + void getChessGameId() { + // given + final long userId = 1L; + final ChessGameService chessGameService = getChessGameService(userId); + + // when + long chessGameId = chessGameService.getChessGameId(userId); + + // then + assertThat(chessGameId) + .isEqualTo(1L); + } + + @Test + @DisplayName("게임을 플레이하면, 시작 위치에 존재하는 체스말을 제거한다") + void play_deletePieces() { + // given + final long userId = 1L; + final ChessGameService chessGameService = getChessGameService(userId); + + // when + chessGameService.play(1L, new Position(1, 0), new Position(2, 0)); + + // then + final Map expected = new HashMap<>(chessGameService.getOrCreateChessGame(userId).getChessBoard()); + expected.remove(new Position(1, 0)); + final Map actual = chessGameService.getOrCreateChessGame(1L).getChessBoard(); + + assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("게임을 플레이하면, 체스말 정보를 바탕으로 체스말을 저장한다") + void play_savePieces() { + // given + final long userId = 1L; + final ChessGameService chessGameService = getChessGameService(userId); + + // when + chessGameService.play(1L, new Position(1, 1), new Position(2, 1)); + + // then + final long chessGameId = chessGameService.getChessGameId(1L); + assertThat(chessGameId) + .isEqualTo(1L); + } + + @Test + @DisplayName("게임을 플레이하면, 체스 게임 아이디에 해당하는 진영 정보를 업데이트한다.") + void play_updateCurrentCamp() { + // given + final long userId = 1L; + final ChessGameService chessGameService = getChessGameService(userId); + final CampType changedCamp = CampType.BLACK; + + // when + chessGameService.play(1L, new Position(1, 1), new Position(2, 1)); + + // then + final ChessGame chessGame = chessGameService.getOrCreateChessGame(1L); + final CampType campType = chessGame.getCurrentCamp(); + assertThat(campType) + .isEqualTo(changedCamp); + } + + @Test + @DisplayName("사용자의 아이디에 해당하는 체스 기물과 체스판 정보를 제거한다") + void clear() { + // given + final long userId = 1L; + final ChessGameService chessGameService = getChessGameService(userId); + + // when + chessGameService.clear(userId); + + // then + final Map actual = chessGameService.getOrCreateChessGame(userId).getChessBoard(); + final Map expected = new ChessGame(CampType.WHITE).getChessBoard(); + assertThat(actual) + .isEqualTo(expected); + } + + private ChessGameService getChessGameService(final long userId) { + final MockChessGameDao chessGameDao = new MockChessGameDao(); + final long chessGameId = chessGameDao.insert(new ChessGameEntity(1L, "WHITE", userId)); + final MockPieceDao pieceDao = getMockPieceDao(chessGameId); + return new ChessGameService(chessGameDao, new ChessBoardService(pieceDao)); + } + + private MockPieceDao getMockPieceDao(final long chessGameId) { + final List pieceEntities = PieceEntityHelper.createPieceEntities(chessGameId); + final MockPieceDao pieceDao = new MockPieceDao(); + pieceEntities.forEach(pieceDao::insert); + return pieceDao; + } +} diff --git a/src/test/java/chess/service/MockServiceManagerHandler.java b/src/test/java/chess/service/MockServiceManagerHandler.java new file mode 100644 index 00000000000..e802997e878 --- /dev/null +++ b/src/test/java/chess/service/MockServiceManagerHandler.java @@ -0,0 +1,18 @@ +package chess.service; + +import chess.dao.chess.MockChessGameDao; +import chess.dao.chess.MockPieceDao; +import chess.dao.user.MockUserDao; + +public class MockServiceManagerHandler implements ServiceHandler { + @Override + public UserService userService() { + return new UserService(new MockUserDao()); + } + + @Override + public ChessGameService chessGameService() { + final ChessBoardService chessBoardService = new ChessBoardService(new MockPieceDao()); + return new ChessGameService(new MockChessGameDao(), chessBoardService); + } +} diff --git a/src/test/java/chess/service/UserServiceTestHandler.java b/src/test/java/chess/service/UserServiceTestHandler.java new file mode 100644 index 00000000000..e7bac537823 --- /dev/null +++ b/src/test/java/chess/service/UserServiceTestHandler.java @@ -0,0 +1,38 @@ +package chess.service; + +import chess.dao.user.MockUserDao; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class UserServiceTestHandler { + + @Test + @DisplayName("사용자의 이름이 이미 저장되어 있다면, 존재하는 사용자의 아이디를 반환한다.") + void getOrCreateUserId_exist() { + // given + final UserService userService = new UserService(new MockUserDao()); + + // when + Long userId = userService.getOrCreateUserId("journey"); + + // then + assertThat(userId) + .isEqualTo(1L); + } + + @Test + @DisplayName("사용자의 이름이 저장되지 않았다면, 새롭게 저장 후 저장된 아이디를 반환한다.") + void getOrCreateUserId_new() { + // given + final UserService userService = new UserService(new MockUserDao()); + + // when + Long userId = userService.getOrCreateUserId("hello"); + + // then + assertThat(userId) + .isEqualTo(1L); + } +}