diff --git a/.gitignore b/.gitignore index 6c018781387..39d89f81263 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ out/ ### VS Code ### .vscode/ + +docker/db/mysql/data +docker/db/config diff --git a/README.md b/README.md index 8102f91c870..c28e3ee6b68 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,121 @@ -# java-chess +# 기능목록 -체스 미션 저장소 +## DB 정보 -## 우아한테크코스 코드리뷰 +chess 라는 schema 를 통해서 게임을 관리합니다 -- [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) +## 2단계 + +- view + - [x] 사용자의 id 를 받는다 + - [x] 사용자의 게임을 입력받는다 + +- controller + - [x] status를 입력할 경우 현재 게임의 상태를 출력한다 + - [x] 사용자의 id를 입력받아 해당 id 에 해당하는 thread 를 통해서 게임을 진행한다 + +### 도메인 + +- dao + - 게임을 불러올 수 있다 + - move 를 저장할 수 있다 + +- command + - [x] move 명령어를 입력받으면, 시작 위치와 끝 위치를 바탕으로 움직이게 한다 + - [x] start 명령어를 입력받으면, 게임을 시작한다 + - [x] end 명령어를 입력받으면, 게임을 종료한다 +- query + - [x] 현재 게임 기물 위치를 출력한다 + - [x] 현재 게임의 점수를 출력한다 +- ScoreCalculator + - [x] 각 팀의 점수를 계산한다 + - [x] 같은 file 에 있는 pawn 의 점수를 0.5점으로 계산한다 +- turn + - [x] 턴을 관리한다 +- piece + - [x] piece 에서 king 인지 여부를 확인한다 +- board + - [x] board 에서 king 이 죽었는지 여부를 확인한다 +- chessGame + - [x] 게임이 king 이 죽었을 때, 종료된다 + - [x] dao 로부터 move 로부터 받은 정보를 불러올 수 있어야 한다 + +#### 인프라 + +- Connection 을 관리한다 + - [x] 기본적으로 5개의 connection 을 가지고, 매 요청마다 다른 connection을 반환한다 +- Thread 를 관리한다 + - 기본적으로 5개의 thread 를 가지고, id 마다 하나의 쓰레드를 반환한다 + - 쓰레드를 반환하면, 해당 쓰레드는 다시 재사용될 수 있다 + - 5개가 넘는 thread 가 사용될 경우, 새로운 쓰레드를 생성한다 + - 메인 쓰레드에서 동시에 thread get 요청을 할 수 있으니, lock 을 걸어준다 + +## 1단계 + +### 컨트롤러 + +- GameCommand + - [x] 게임 시작 커맨드는 "start"다. + - [x] 게임 종료 커맨드는 "end"다. + - [x] 체스 기물 이동 요청 커맨드는 "move 시작위치 이동위치" 이다. + - [x] 위치의 첫 번째 인자는 a~h까지 가능하다. + - [x] 위치의 두 번째 인자는 1~8까지 가능하다. + +### 도메인 + +#### state + +- InitialPawnState + - [x] 해당 위치에 아무 말이 없다면 앞으로 한 칸 혹은 두 칸을 갈 수 있다. + - [x] 해당 위치에 상대편 말이 있다면 대각 방향으로 한 칸 이동할 수 있다. + +- MovedPawnState + - [x] 해당 위치에 아무 말이 없다면 앞으로 한 칸 갈 수 있다. + - [x] 해당 위치에 상대편 말이 있다면 대각 방향으로 한 칸 이동할 수 있다. + +- KnightState + - [x] 해당 위치에 같은 색의 기물이 없으면 앞으로 한 칸 이동한 후 같은 방향 대각으로 한 칸 이동할 수 있다. + - [x] 다른 piece 가 중간에 있더라도 뛰어넘을 수 있다 + +- BishopState + - [x] 해당 위치에 같은 색의 기물이 없으면 대각 방향으로 원하는 만큼 이동할 수 있다. + +- RookState + - [x] 해당 위치에 같은 색의 기물이 없으면 직선 방향으로 원하는 만큼 이동할 수 있다. + +- QueenState + - [x] 해당 위치에 같은 색의 기물이 없으면 직선 방향으로 원하는 만큼 이동할 수 있다. + - [x] 해당 위치에 같은 색의 기물이 없으면 대각 방향으로 원하는 만큼 이동할 수 있다. + +- KingState + - [x] 직선 혹은 대각 방향으로 한 칸 이동할 수 있다. + +#### piece + +- [x] 이름을 가진다. +- [x] 피스 타입을 가진다. +- [x] 움직임 전략을 가진다. + +#### Position + +- [x] File 과 Rank 를 갖는다. +- [x] 다른 포지션과의 Rank 순서 차이를 구할 수 있다. +- [x] 다른 포지션과의 File 순서 차이를 구할 수 있다. +- [x] 다른 포지션과의 직선 경로 Position 을 얻을 수 있다. + - [예외] 직선이 아니면 예외가 발생한다. + +#### Board + +- [x] Piece 들의 위치를 초기화 한다. +- [x] 보드는 자신의 판 정보를 반환할 수 있다. +- [x] 시작 Position 과 목표 Position 를 입력하면 Piece를 움직일 수 있다. + +### 뷰 + +#### InputView + +- [x] start 와 end 를 입력 받는다. + +#### OutputView + +- [x] 체스판을 출력한다. diff --git a/build.gradle b/build.gradle index 62e0781fadb..20a32fbc6b2 100644 --- a/build.gradle +++ b/build.gradle @@ -9,11 +9,10 @@ repositories { } dependencies { - implementation 'com.sparkjava:spark-core:2.9.3' - implementation 'com.sparkjava:spark-template-handlebars:2.7.1' - implementation 'ch.qos.logback:logback-classic:1.2.10' testImplementation 'org.assertj:assertj-core:3.22.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' + + runtimeOnly 'mysql:mysql-connector-java:8.0.28' } java { diff --git a/docker/db/mysql/init/init.sql b/docker/db/mysql/init/init.sql new file mode 100644 index 00000000000..d4efc1a72af --- /dev/null +++ b/docker/db/mysql/init/init.sql @@ -0,0 +1,21 @@ +CREATE TABLE user +( + id INT AUTO_INCREMENT PRIMARY KEY, + user_name VARCHAR(100) NOT NULL +); + +CREATE TABLE board +( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id VARCHAR(100), + status VARCHAR(10) NOT NULL +); + +CREATE TABLE move +( + id INT AUTO_INCREMENT PRIMARY KEY, + board_id INT, + origin VARCHAR(10) NOT NULL, + destination VARCHAR(10) NOT NULL, + turn INT NOT NULL +); diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000000..558a1d5a53f --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.9" +services: + db: + image: mysql:8.0.28 + platform: linux/x86_64 + restart: always + ports: + - "13306:3306" + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: chess + MYSQL_USER: user + MYSQL_PASSWORD: password + TZ: Asia/Seoul + volumes: + - ./db/mysql/data:/var/lib/mysql + - ./db/mysql/config:/etc/mysql/conf.d + - ./db/mysql/init:/docker-entrypoint-initdb.d diff --git a/src/main/java/chess/Application.java b/src/main/java/chess/Application.java new file mode 100644 index 00000000000..ab377cd44e3 --- /dev/null +++ b/src/main/java/chess/Application.java @@ -0,0 +1,13 @@ +package chess; + +import chess.controller.ControllerFactory; +import chess.controller.main.MainController; + +public class Application { + + public static void main(String[] args) { + ControllerFactory controllerFactory = ControllerFactory.getInstance(); + MainController mainController = controllerFactory.getMainController(); + mainController.run(); + } +} diff --git a/src/main/java/chess/WebApplication.java b/src/main/java/chess/WebApplication.java deleted file mode 100644 index d98b37b1f32..00000000000 --- a/src/main/java/chess/WebApplication.java +++ /dev/null @@ -1,22 +0,0 @@ -package chess; - -import spark.ModelAndView; -import spark.template.handlebars.HandlebarsTemplateEngine; - -import java.util.HashMap; -import java.util.Map; - -import static spark.Spark.get; - -public class WebApplication { - public static void main(String[] args) { - get("/", (req, res) -> { - Map model = new HashMap<>(); - return render(model, "index.html"); - }); - } - - private static String render(Map model, String templatePath) { - return new HandlebarsTemplateEngine().render(new ModelAndView(model, templatePath)); - } -} diff --git a/src/main/java/chess/controller/Controller.java b/src/main/java/chess/controller/Controller.java new file mode 100644 index 00000000000..de5d1f2c233 --- /dev/null +++ b/src/main/java/chess/controller/Controller.java @@ -0,0 +1,8 @@ +package chess.controller; + +import chess.controller.main.Request; + +public interface Controller { + + void run(Request request); +} diff --git a/src/main/java/chess/controller/ControllerFactory.java b/src/main/java/chess/controller/ControllerFactory.java new file mode 100644 index 00000000000..e2a0674d3da --- /dev/null +++ b/src/main/java/chess/controller/ControllerFactory.java @@ -0,0 +1,72 @@ +package chess.controller; + +import chess.controller.game.end.EndController; +import chess.controller.game.games.GamesController; +import chess.controller.game.move.MoveController; +import chess.controller.game.start.StartController; +import chess.controller.game.status.StatusController; +import chess.controller.main.ActionType; +import chess.controller.main.CommandMapper; +import chess.controller.main.MainController; +import chess.controller.room.create.CreateRoomController; +import chess.controller.room.join.JoinBoardController; +import chess.controller.user.LoginController; +import chess.service.ServiceFactory; +import chess.view.ViewFactory; +import java.util.Map; + +public class ControllerFactory { + + private static final ControllerFactory instance = new ControllerFactory(); + + private final StartController startController; + private final EndController endController; + private final MainController mainController; + private final MoveController moveController; + private final StatusController statusController; + private final GamesController gamesController; + private final CreateRoomController createRoomController; + private final JoinBoardController joinBoardController; + private final LoginController loginController; + + private ControllerFactory() { + ServiceFactory serviceFactory = ServiceFactory.getInstance(); + startController = new StartController(serviceFactory.getStartChessGameService()); + endController = new EndController(serviceFactory.getEndChessGameService()); + ViewFactory viewFactory = ViewFactory.getInstance(); + moveController = new MoveController(serviceFactory.getMoveChessGameService(), + serviceFactory.getLoadChessGameService(), viewFactory.getBoardOutput()); + statusController = new StatusController(viewFactory.getStatusOutput(), + serviceFactory.getStatusChessGameService()); + gamesController = new GamesController(serviceFactory.getGamesService(), viewFactory.getGamesOutput()); + createRoomController = new CreateRoomController(serviceFactory.getCreateRoomService(), + viewFactory.getCreateRoomOutput()); + joinBoardController = new JoinBoardController(viewFactory.getJoinBoard(), viewFactory.getJoinBoardOutput(), + serviceFactory.getLoadChessGameService(), viewFactory.getBoardOutput()); + loginController = new LoginController(viewFactory.getLogin(), viewFactory.getLoginOutput(), + serviceFactory.getLoginService()); + mainController = new MainController(createCommandMapper(), viewFactory.getErrorOutput(), + viewFactory.getInputView(), viewFactory.getInitialOutput()); + } + + public static ControllerFactory getInstance() { + return instance; + } + + private CommandMapper createCommandMapper() { + return new CommandMapper(Map.of( + ActionType.START, startController, + ActionType.END, endController, + ActionType.MOVE, moveController, + ActionType.STATUS, statusController, + ActionType.GAMES, gamesController, + ActionType.CREATE, createRoomController, + ActionType.JOIN, joinBoardController, + ActionType.LOGIN, loginController + )); + } + + public MainController getMainController() { + return mainController; + } +} diff --git a/src/main/java/chess/controller/ErrorOutput.java b/src/main/java/chess/controller/ErrorOutput.java new file mode 100644 index 00000000000..8892dda8ae2 --- /dev/null +++ b/src/main/java/chess/controller/ErrorOutput.java @@ -0,0 +1,6 @@ +package chess.controller; + +public interface ErrorOutput { + + void printError(String message); +} diff --git a/src/main/java/chess/controller/exception/BoardNotFoundException.java b/src/main/java/chess/controller/exception/BoardNotFoundException.java new file mode 100644 index 00000000000..6af69521536 --- /dev/null +++ b/src/main/java/chess/controller/exception/BoardNotFoundException.java @@ -0,0 +1,8 @@ +package chess.controller.exception; + +public class BoardNotFoundException extends RuntimeException { + + public BoardNotFoundException() { + super("방 잘못된 방번호를 입력하셨습니다 -join [방 번호] 형식으로 입력해주세요. -games 로 방 목록을 확인할 수 있습니다."); + } +} diff --git a/src/main/java/chess/controller/exception/LoginException.java b/src/main/java/chess/controller/exception/LoginException.java new file mode 100644 index 00000000000..52beb51eb87 --- /dev/null +++ b/src/main/java/chess/controller/exception/LoginException.java @@ -0,0 +1,8 @@ +package chess.controller.exception; + +public class LoginException extends RuntimeException { + + public LoginException() { + super("로그인이 필요합니다. login [id] 형식으로 입력해주세요."); + } +} diff --git a/src/main/java/chess/controller/game/BoardOutput.java b/src/main/java/chess/controller/game/BoardOutput.java new file mode 100644 index 00000000000..d1a11337e38 --- /dev/null +++ b/src/main/java/chess/controller/game/BoardOutput.java @@ -0,0 +1,9 @@ +package chess.controller.game; + +import chess.view.resposne.PieceResponse; +import java.util.List; + +public interface BoardOutput { + + void printBoard(List> boardResponse); +} diff --git a/src/main/java/chess/controller/game/end/EndController.java b/src/main/java/chess/controller/game/end/EndController.java new file mode 100644 index 00000000000..1a067317be1 --- /dev/null +++ b/src/main/java/chess/controller/game/end/EndController.java @@ -0,0 +1,20 @@ +package chess.controller.game.end; + +import chess.controller.Controller; +import chess.controller.main.Request; +import chess.service.game.EndChessGameService; + +public class EndController implements Controller { + + private final EndChessGameService endChessGameService; + + public EndController(EndChessGameService endChessGameService) { + this.endChessGameService = endChessGameService; + } + + @Override + public void run(Request request) { + EndRequest endRequest = EndRequest.from(request); + endChessGameService.end(endRequest.getBoardId()); + } +} diff --git a/src/main/java/chess/controller/game/end/EndRequest.java b/src/main/java/chess/controller/game/end/EndRequest.java new file mode 100644 index 00000000000..2e9d340dca0 --- /dev/null +++ b/src/main/java/chess/controller/game/end/EndRequest.java @@ -0,0 +1,28 @@ +package chess.controller.game.end; + +import chess.controller.exception.BoardNotFoundException; +import chess.controller.exception.LoginException; +import chess.controller.main.Request; + +public class EndRequest { + + private final int boardId; + + private EndRequest(int boardId) { + this.boardId = boardId; + } + + public static EndRequest from(Request request) { + if (request.getUserId().isEmpty()) { + throw new LoginException(); + } + if (request.getBoardId().isEmpty()) { + throw new BoardNotFoundException(); + } + return new EndRequest(request.getBoardId().get()); + } + + int getBoardId() { + return boardId; + } +} diff --git a/src/main/java/chess/controller/game/games/GamesController.java b/src/main/java/chess/controller/game/games/GamesController.java new file mode 100644 index 00000000000..709243fcce5 --- /dev/null +++ b/src/main/java/chess/controller/game/games/GamesController.java @@ -0,0 +1,22 @@ +package chess.controller.game.games; + +import chess.controller.Controller; +import chess.controller.main.Request; +import chess.service.game.GamesService; + +public class GamesController implements Controller { + + private final GamesService gamesService; + private final GamesOutput gamesOutput; + + public GamesController(GamesService gamesService, GamesOutput gamesOutput) { + this.gamesService = gamesService; + this.gamesOutput = gamesOutput; + } + + @Override + public void run(Request request) { + GamesRequest gamesRequest = GamesRequest.from(request); + gamesOutput.printGames(gamesService.findAllGamesByUserId(gamesRequest.getUserId())); + } +} diff --git a/src/main/java/chess/controller/game/games/GamesOutput.java b/src/main/java/chess/controller/game/games/GamesOutput.java new file mode 100644 index 00000000000..490fe89150f --- /dev/null +++ b/src/main/java/chess/controller/game/games/GamesOutput.java @@ -0,0 +1,8 @@ +package chess.controller.game.games; + +import java.util.List; + +public interface GamesOutput { + + void printGames(List games); +} diff --git a/src/main/java/chess/controller/game/games/GamesRequest.java b/src/main/java/chess/controller/game/games/GamesRequest.java new file mode 100644 index 00000000000..5b9aeda6874 --- /dev/null +++ b/src/main/java/chess/controller/game/games/GamesRequest.java @@ -0,0 +1,26 @@ +package chess.controller.game.games; + +import chess.controller.exception.LoginException; +import chess.controller.main.Request; +import java.util.Optional; + +public class GamesRequest { + + private final int userId; + + private GamesRequest(int userId) { + this.userId = userId; + } + + public static GamesRequest from(Request request) { + Optional userId = request.getUserId(); + if (userId.isEmpty()) { + throw new LoginException(); + } + return new GamesRequest(userId.get()); + } + + int getUserId() { + return userId; + } +} diff --git a/src/main/java/chess/controller/game/move/MoveController.java b/src/main/java/chess/controller/game/move/MoveController.java new file mode 100644 index 00000000000..a6799ecb1ae --- /dev/null +++ b/src/main/java/chess/controller/game/move/MoveController.java @@ -0,0 +1,56 @@ +package chess.controller.game.move; + +import chess.controller.Controller; +import chess.controller.exception.BoardNotFoundException; +import chess.controller.game.BoardOutput; +import chess.controller.main.Request; +import chess.domain.game.ChessGame; +import chess.domain.piece.Piece; +import chess.service.game.LoadChessGameService; +import chess.service.game.MoveChessGameService; +import chess.view.resposne.PieceResponse; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class MoveController implements Controller { + + private final MoveChessGameService moveChessGameService; + private final LoadChessGameService loadChessGameService; + private final BoardOutput boardOutput; + + public MoveController(MoveChessGameService moveChessGameService, LoadChessGameService loadChessGameService, + BoardOutput boardOutput) { + this.moveChessGameService = moveChessGameService; + this.loadChessGameService = loadChessGameService; + this.boardOutput = boardOutput; + } + + @Override + public void run(Request request) { + MoveRequest moveRequest = MoveRequest.from(request); + moveChessGameService.move(moveRequest.getBoardId(), moveRequest.getOrigin(), moveRequest.getDestination()); + Optional chessGame = loadChessGameService.load(moveRequest.getBoardId()); + if (chessGame.isEmpty()) { + throw new BoardNotFoundException(); + } + printBoard(chessGame.get()); + } + + private void printBoard(ChessGame chessGame) { + boardOutput.printBoard(makeBoardResponse(chessGame.getPieces())); + } + + + private List> makeBoardResponse(List> boardResult) { + return boardResult.stream() + .map(this::makeRankResponse) + .collect(Collectors.toList()); + } + + private List makeRankResponse(List row) { + return row.stream() + .map(PieceResponse::from) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/chess/controller/game/move/MoveRequest.java b/src/main/java/chess/controller/game/move/MoveRequest.java new file mode 100644 index 00000000000..4b11f1941dc --- /dev/null +++ b/src/main/java/chess/controller/game/move/MoveRequest.java @@ -0,0 +1,54 @@ +package chess.controller.game.move; + +import chess.controller.exception.BoardNotFoundException; +import chess.controller.exception.LoginException; +import chess.controller.main.Request; +import java.util.List; + +public class MoveRequest { + + private final int boardId; + private final String origin; + private final String destination; + + private MoveRequest(int boardId, String origin, String destination) { + this.boardId = boardId; + this.origin = origin; + this.destination = destination; + } + + public static MoveRequest from(Request request) { + validateCommands(request.commands()); + validateLogin(request); + if (request.getBoardId().isEmpty()) { + throw new BoardNotFoundException(); + } + String origin = request.commands().get(1); + String destination = request.commands().get(2); + return new MoveRequest(request.getBoardId().get(), origin, destination); + } + + private static void validateLogin(Request request) { + if (request.getUserId().isEmpty()) { + throw new LoginException(); + } + } + + private static void validateCommands(List commands) { + if (commands.size() != 3) { + throw new IllegalArgumentException("잘못된 명령어입니다.[출발지] [도착지] 형식으로 입력해주세요."); + } + } + + int getBoardId() { + return boardId; + } + + String getOrigin() { + return origin; + } + + String getDestination() { + return destination; + } +} diff --git a/src/main/java/chess/controller/game/start/StartController.java b/src/main/java/chess/controller/game/start/StartController.java new file mode 100644 index 00000000000..6fde1fa6b9c --- /dev/null +++ b/src/main/java/chess/controller/game/start/StartController.java @@ -0,0 +1,20 @@ +package chess.controller.game.start; + +import chess.controller.Controller; +import chess.controller.main.Request; +import chess.service.game.StartChessGameService; + +public class StartController implements Controller { + + private final StartChessGameService startChessGameService; + + public StartController(StartChessGameService startChessGameService) { + this.startChessGameService = startChessGameService; + } + + @Override + public void run(Request request) { + StartRequest startRequest = StartRequest.from(request); + startChessGameService.start(startRequest.getBoardId()); + } +} diff --git a/src/main/java/chess/controller/game/start/StartRequest.java b/src/main/java/chess/controller/game/start/StartRequest.java new file mode 100644 index 00000000000..5769e565c89 --- /dev/null +++ b/src/main/java/chess/controller/game/start/StartRequest.java @@ -0,0 +1,38 @@ +package chess.controller.game.start; + +import chess.controller.exception.BoardNotFoundException; +import chess.controller.exception.LoginException; +import chess.controller.main.Request; +import java.util.List; +import java.util.Optional; + +public class StartRequest { + + private final int boardId; + + private StartRequest(int boardId) { + this.boardId = boardId; + } + + public static StartRequest from(Request request) { + validateCommands(request.commands()); + if (request.getUserId().isEmpty()) { + throw new LoginException(); + } + Optional boardId = request.getBoardId(); + if (boardId.isEmpty()) { + throw new BoardNotFoundException(); + } + return new StartRequest(boardId.get()); + } + + private static void validateCommands(List commands) { + if (commands.size() != 1) { + throw new IllegalArgumentException("잘못된 명령어입니다."); + } + } + + public int getBoardId() { + return boardId; + } +} diff --git a/src/main/java/chess/controller/game/status/StatusController.java b/src/main/java/chess/controller/game/status/StatusController.java new file mode 100644 index 00000000000..75871cbcaad --- /dev/null +++ b/src/main/java/chess/controller/game/status/StatusController.java @@ -0,0 +1,23 @@ +package chess.controller.game.status; + +import chess.controller.Controller; +import chess.controller.main.Request; +import chess.service.game.StatusChessGameService; + +public class StatusController implements Controller { + + private final StatusOutput statusOutput; + private final StatusChessGameService statusChessGameService; + + public StatusController(StatusOutput statusOutput, StatusChessGameService statusChessGameService) { + this.statusOutput = statusOutput; + this.statusChessGameService = statusChessGameService; + } + + @Override + public void run(Request request) { + StatusRequest statusRequest = StatusRequest.from(request); + StatusResponse statusResponse = statusChessGameService.status(statusRequest.getBoardId()); + statusOutput.printStatus(statusResponse); + } +} diff --git a/src/main/java/chess/controller/game/status/StatusOutput.java b/src/main/java/chess/controller/game/status/StatusOutput.java new file mode 100644 index 00000000000..2af1fba3381 --- /dev/null +++ b/src/main/java/chess/controller/game/status/StatusOutput.java @@ -0,0 +1,6 @@ +package chess.controller.game.status; + +public interface StatusOutput { + + void printStatus(StatusResponse statusResponse); +} diff --git a/src/main/java/chess/controller/game/status/StatusRequest.java b/src/main/java/chess/controller/game/status/StatusRequest.java new file mode 100644 index 00000000000..c42b6dda440 --- /dev/null +++ b/src/main/java/chess/controller/game/status/StatusRequest.java @@ -0,0 +1,24 @@ +package chess.controller.game.status; + +import chess.controller.main.Request; + +public class StatusRequest { + + private final int boardId; + + private StatusRequest(int boardId) { + this.boardId = boardId; + } + + public static StatusRequest from(Request request) { + if (request.getUserId().isEmpty()) { + throw new IllegalArgumentException("로그인이 필요합니다."); + } + int roomId = Integer.parseInt(request.commands().get(1)); + return new StatusRequest(roomId); + } + + public int getBoardId() { + return boardId; + } +} diff --git a/src/main/java/chess/controller/game/status/StatusResponse.java b/src/main/java/chess/controller/game/status/StatusResponse.java new file mode 100644 index 00000000000..83002d8cac1 --- /dev/null +++ b/src/main/java/chess/controller/game/status/StatusResponse.java @@ -0,0 +1,20 @@ +package chess.controller.game.status; + +public class StatusResponse { + + private final double whiteScore; + private final double blackScore; + + public StatusResponse(double whiteScore, 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/main/ActionType.java b/src/main/java/chess/controller/main/ActionType.java new file mode 100644 index 00000000000..29ad64334cf --- /dev/null +++ b/src/main/java/chess/controller/main/ActionType.java @@ -0,0 +1,12 @@ +package chess.controller.main; + +public enum ActionType { + START, + END, + MOVE, + STATUS, + GAMES, + CREATE, + JOIN, + LOGIN +} diff --git a/src/main/java/chess/controller/main/CommandMapper.java b/src/main/java/chess/controller/main/CommandMapper.java new file mode 100644 index 00000000000..ee8ab8d3fb1 --- /dev/null +++ b/src/main/java/chess/controller/main/CommandMapper.java @@ -0,0 +1,17 @@ +package chess.controller.main; + +import chess.controller.Controller; +import java.util.Map; + +public class CommandMapper { + + private final Map actions; + + public CommandMapper(Map actions) { + this.actions = actions; + } + + public Controller getController(ActionType type) { + return actions.get(type); + } +} diff --git a/src/main/java/chess/controller/main/InitialOutput.java b/src/main/java/chess/controller/main/InitialOutput.java new file mode 100644 index 00000000000..4a4cda6ef8d --- /dev/null +++ b/src/main/java/chess/controller/main/InitialOutput.java @@ -0,0 +1,6 @@ +package chess.controller.main; + +public interface InitialOutput { + + void printInitialMessage(); +} diff --git a/src/main/java/chess/controller/main/Input.java b/src/main/java/chess/controller/main/Input.java new file mode 100644 index 00000000000..c0a3fd666f0 --- /dev/null +++ b/src/main/java/chess/controller/main/Input.java @@ -0,0 +1,6 @@ +package chess.controller.main; + +public interface Input { + + Request inputGameCommand(); +} diff --git a/src/main/java/chess/controller/main/MainController.java b/src/main/java/chess/controller/main/MainController.java new file mode 100644 index 00000000000..a56128678ab --- /dev/null +++ b/src/main/java/chess/controller/main/MainController.java @@ -0,0 +1,34 @@ +package chess.controller.main; + +import chess.controller.Controller; +import chess.controller.ErrorOutput; +import chess.view.InputView; + +public class MainController { + + private final CommandMapper commandMapper; + private final InputView inputView; + private final ErrorOutput errorOutput; + private final InitialOutput initialOutput; + + public MainController(CommandMapper commandMapper, ErrorOutput errorOutput, InputView inputView, + InitialOutput initialOutput) { + this.commandMapper = commandMapper; + this.inputView = inputView; + this.errorOutput = errorOutput; + this.initialOutput = initialOutput; + } + + public void run() { + initialOutput.printInitialMessage(); + while (true) { + try { + Request request = inputView.inputGameCommand(); + Controller controller = commandMapper.getController(request.getActionType()); + controller.run(request); + } catch (Exception e) { + errorOutput.printError(e.getMessage()); + } + } + } +} diff --git a/src/main/java/chess/controller/main/Request.java b/src/main/java/chess/controller/main/Request.java new file mode 100644 index 00000000000..a064b23e10f --- /dev/null +++ b/src/main/java/chess/controller/main/Request.java @@ -0,0 +1,15 @@ +package chess.controller.main; + +import java.util.List; +import java.util.Optional; + +public interface Request { + + ActionType getActionType(); + + List commands(); + + Optional getBoardId(); + + Optional getUserId(); +} diff --git a/src/main/java/chess/controller/room/create/CreateRoomController.java b/src/main/java/chess/controller/room/create/CreateRoomController.java new file mode 100644 index 00000000000..b6cb8bd587e --- /dev/null +++ b/src/main/java/chess/controller/room/create/CreateRoomController.java @@ -0,0 +1,23 @@ +package chess.controller.room.create; + +import chess.controller.Controller; +import chess.controller.main.Request; +import chess.service.room.CreateRoomService; + +public class CreateRoomController implements Controller { + + private final CreateRoomService createRoomService; + private final CreateRoomOutput createRoomOutput; + + public CreateRoomController(CreateRoomService createRoomService, CreateRoomOutput createRoomOutput) { + this.createRoomService = createRoomService; + this.createRoomOutput = createRoomOutput; + } + + @Override + public void run(Request request) { + CreateRoomRequest createRoomRequest = CreateRoomRequest.from(request); + int roomId = createRoomService.createRoom(createRoomRequest.getUserId()); + createRoomOutput.printCreateRoomSuccess(roomId); + } +} diff --git a/src/main/java/chess/controller/room/create/CreateRoomOutput.java b/src/main/java/chess/controller/room/create/CreateRoomOutput.java new file mode 100644 index 00000000000..38b9ade8ad4 --- /dev/null +++ b/src/main/java/chess/controller/room/create/CreateRoomOutput.java @@ -0,0 +1,6 @@ +package chess.controller.room.create; + +public interface CreateRoomOutput { + + void printCreateRoomSuccess(int roomId); +} diff --git a/src/main/java/chess/controller/room/create/CreateRoomRequest.java b/src/main/java/chess/controller/room/create/CreateRoomRequest.java new file mode 100644 index 00000000000..a66db06caa4 --- /dev/null +++ b/src/main/java/chess/controller/room/create/CreateRoomRequest.java @@ -0,0 +1,25 @@ +package chess.controller.room.create; + +import chess.controller.main.Request; +import java.util.Optional; + +public class CreateRoomRequest { + + private final int userId; + + private CreateRoomRequest(int userId) { + this.userId = userId; + } + + public static CreateRoomRequest from(Request request) { + Optional userId = request.getUserId(); + if (userId.isEmpty()) { + throw new IllegalArgumentException("로그인이 필요합니다."); + } + return new CreateRoomRequest(userId.get()); + } + + public int getUserId() { + return userId; + } +} diff --git a/src/main/java/chess/controller/room/join/JoinBoard.java b/src/main/java/chess/controller/room/join/JoinBoard.java new file mode 100644 index 00000000000..cff1453db64 --- /dev/null +++ b/src/main/java/chess/controller/room/join/JoinBoard.java @@ -0,0 +1,6 @@ +package chess.controller.room.join; + +public interface JoinBoard { + + void join(int boardId); +} diff --git a/src/main/java/chess/controller/room/join/JoinBoardController.java b/src/main/java/chess/controller/room/join/JoinBoardController.java new file mode 100644 index 00000000000..364337b16bc --- /dev/null +++ b/src/main/java/chess/controller/room/join/JoinBoardController.java @@ -0,0 +1,53 @@ +package chess.controller.room.join; + +import chess.controller.Controller; +import chess.controller.exception.BoardNotFoundException; +import chess.controller.game.BoardOutput; +import chess.controller.main.Request; +import chess.domain.game.ChessGame; +import chess.domain.piece.Piece; +import chess.service.game.LoadChessGameService; +import chess.view.resposne.PieceResponse; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class JoinBoardController implements Controller { + + private final JoinBoard joinBoard; + private final JoinBoardOutput joinBoardOutput; + private final LoadChessGameService loadChessGameService; + private final BoardOutput boardOutput; + + public JoinBoardController(JoinBoard joinBoard, JoinBoardOutput joinBoardOutput, + LoadChessGameService loadChessGameService, BoardOutput boardOutput) { + this.joinBoard = joinBoard; + this.joinBoardOutput = joinBoardOutput; + this.loadChessGameService = loadChessGameService; + this.boardOutput = boardOutput; + } + + @Override + public void run(Request request) { + JoinBoardRequest joinBoardRequest = JoinBoardRequest.from(request); + joinBoard.join(joinBoardRequest.getRoomId()); + Optional chessGame = loadChessGameService.load(joinBoardRequest.getRoomId()); + if (chessGame.isEmpty()) { + throw new BoardNotFoundException(); + } + joinBoardOutput.printJoinBoardSuccess(chessGame.get().getStatusType()); + boardOutput.printBoard(makeBoardResponse(chessGame.get().getPieces())); + } + + private List> makeBoardResponse(List> boardResult) { + return boardResult.stream() + .map(this::makeRankResponse) + .collect(Collectors.toList()); + } + + private List makeRankResponse(List row) { + return row.stream() + .map(PieceResponse::from) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/chess/controller/room/join/JoinBoardOutput.java b/src/main/java/chess/controller/room/join/JoinBoardOutput.java new file mode 100644 index 00000000000..ef3ac9192a6 --- /dev/null +++ b/src/main/java/chess/controller/room/join/JoinBoardOutput.java @@ -0,0 +1,8 @@ +package chess.controller.room.join; + +import chess.domain.game.state.StatusType; + +public interface JoinBoardOutput { + + void printJoinBoardSuccess(StatusType statusType); +} diff --git a/src/main/java/chess/controller/room/join/JoinBoardRequest.java b/src/main/java/chess/controller/room/join/JoinBoardRequest.java new file mode 100644 index 00000000000..3b9918f321b --- /dev/null +++ b/src/main/java/chess/controller/room/join/JoinBoardRequest.java @@ -0,0 +1,36 @@ +package chess.controller.room.join; + +import chess.controller.exception.LoginException; +import chess.controller.main.Request; +import java.util.List; + +public class JoinBoardRequest { + + private static final int ROOM_ID_INDEX = 1; + + private final int roomId; + + public JoinBoardRequest(int roomId) { + this.roomId = roomId; + } + + public static JoinBoardRequest from(Request request) { + List commands = request.commands(); + validateCommands(commands); + if (request.getUserId().isEmpty()) { + throw new LoginException(); + } + int roomId = Integer.parseInt(commands.get(ROOM_ID_INDEX)); + return new JoinBoardRequest(roomId); + } + + private static void validateCommands(List commands) { + if (commands.size() != 2) { + throw new IllegalArgumentException("잘못된 명령어입니다. join [방번호] 형식으로 입력해주세요."); + } + } + + public int getRoomId() { + return roomId; + } +} diff --git a/src/main/java/chess/controller/user/Login.java b/src/main/java/chess/controller/user/Login.java new file mode 100644 index 00000000000..f889b4266bb --- /dev/null +++ b/src/main/java/chess/controller/user/Login.java @@ -0,0 +1,6 @@ +package chess.controller.user; + +public interface Login { + + void login(int userId); +} diff --git a/src/main/java/chess/controller/user/LoginController.java b/src/main/java/chess/controller/user/LoginController.java new file mode 100644 index 00000000000..b030d5cb9cf --- /dev/null +++ b/src/main/java/chess/controller/user/LoginController.java @@ -0,0 +1,26 @@ +package chess.controller.user; + +import chess.controller.Controller; +import chess.controller.main.Request; +import chess.service.user.LoginService; + +public class LoginController implements Controller { + + private final Login login; + private final LoginOutput loginOutput; + private final LoginService loginService; + + public LoginController(Login login, LoginOutput loginOutput, LoginService loginService) { + this.login = login; + this.loginOutput = loginOutput; + this.loginService = loginService; + } + + @Override + public void run(Request request) { + LoginRequest loginRequest = LoginRequest.from(request); + int userId = loginService.login(loginRequest.getUserName()); + login.login(userId); + loginOutput.printLoginSuccess(); + } +} diff --git a/src/main/java/chess/controller/user/LoginOutput.java b/src/main/java/chess/controller/user/LoginOutput.java new file mode 100644 index 00000000000..5cec3346c45 --- /dev/null +++ b/src/main/java/chess/controller/user/LoginOutput.java @@ -0,0 +1,6 @@ +package chess.controller.user; + +public interface LoginOutput { + + void printLoginSuccess(); +} diff --git a/src/main/java/chess/controller/user/LoginRequest.java b/src/main/java/chess/controller/user/LoginRequest.java new file mode 100644 index 00000000000..fa730df47d8 --- /dev/null +++ b/src/main/java/chess/controller/user/LoginRequest.java @@ -0,0 +1,29 @@ +package chess.controller.user; + +import chess.controller.main.Request; +import java.util.List; + +public class LoginRequest { + + private static final int USER_NAME_INDEX = 1; + private static final int LOGIN_SIZE = 2; + + private final String userName; + + private LoginRequest(String userName) { + this.userName = userName; + } + + public static LoginRequest from(Request request) { + List commands = request.commands(); + if (commands.size() != LOGIN_SIZE) { + throw new IllegalArgumentException("로그인 요청은 login [유저 이름] 형식으로 작성되어야 합니다"); + } + String userName = commands.get(USER_NAME_INDEX); + return new LoginRequest(userName); + } + + public String getUserName() { + return userName; + } +} diff --git a/src/main/java/chess/domain/game/Board.java b/src/main/java/chess/domain/game/Board.java new file mode 100644 index 00000000000..9f3e527ea1a --- /dev/null +++ b/src/main/java/chess/domain/game/Board.java @@ -0,0 +1,78 @@ +package chess.domain.game; + +import chess.domain.game.constant.ChessPosition; +import chess.domain.game.exception.ChessGameException; +import chess.domain.piece.Color; +import chess.domain.piece.Piece; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class Board { + + private static final Piece EMPTY_PIECE = Piece.empty(); + + private final Map piecePosition = ChessPosition.initialPiecePositions(); + private Turn turn = new Turn(); + + public void movePiece(Position origin, Position destination) { + validateMoveRequest(origin, destination); + Piece targetPiece = piecePosition.get(origin); + Piece movedPiece = targetPiece.move(origin.getFileDifference(destination), + origin.getRankDifference(destination), + piecePosition.get(destination)); + piecePosition.put(destination, movedPiece); + piecePosition.put(origin, Piece.empty()); + turn = turn.changeTurn(); + } + + private void validateMoveRequest(Position origin, Position destination) { + Piece piece = piecePosition.get(origin); + if (piece == EMPTY_PIECE) { + throw new ChessGameException("이동할 말이 없습니다."); + } + if (piece.isNotSameColor(turn.getCurrentTurn())) { + throw new ChessGameException("상대 말을 움직일 수 없습니다."); + } + checkPath(origin, destination); + } + + private void checkPath(Position origin, Position destination) { + List straightPath = origin.createStraightPath(destination); + boolean alreadyExist = straightPath.stream() + .map(piecePosition::get) + .anyMatch(piece -> piece != EMPTY_PIECE); + if (alreadyExist) { + throw new ChessGameException("말이 있는 경로로는 이동할 수 없습니다."); + } + } + + public List> getPieces() { + return Arrays.stream(Rank.values()) + .map(this::getRankPieces) + .collect(Collectors.toList()); + } + + private List getRankPieces(Rank rank) { + return Arrays.stream(File.values()) + .map(file -> Position.of(file, rank)) + .map(piecePosition::get) + .collect(Collectors.toList()); + } + + public Map getStatus() { + return new ScoreCalculator(piecePosition).calculateScore(); + } + + public boolean isKingDead() { + return piecePosition.values() + .stream() + .filter(Piece::isKing) + .count() != 2; + } + + public Turn getTurn() { + return turn; + } +} diff --git a/src/main/java/chess/domain/game/ChessGame.java b/src/main/java/chess/domain/game/ChessGame.java new file mode 100644 index 00000000000..819bb00327f --- /dev/null +++ b/src/main/java/chess/domain/game/ChessGame.java @@ -0,0 +1,81 @@ +package chess.domain.game; + +import chess.domain.game.exception.ChessGameException; +import chess.domain.game.state.GameState; +import chess.domain.game.state.MovingState; +import chess.domain.game.state.StartState; +import chess.domain.game.state.StatusType; +import chess.domain.piece.Color; +import chess.domain.piece.Piece; +import chess.domain.piece.exception.IllegalPieceMoveException; +import java.util.List; +import java.util.Map; + +public class ChessGame { + + private Board board; + private GameState gameState; + + public ChessGame() { + gameState = StartState.getInstance(); + } + + public ChessGame(List> moves, GameState gameState) { + this.gameState = MovingState.getInstance(); + board = new Board(); + moves.forEach(move -> move(move.get(0), move.get(1))); + this.gameState = gameState; + } + + public void start() { + gameState = gameState.start(); + board = new Board(); + } + + public void move(Position origin, Position destination) { + gameState = gameState.run(); + movePiece(origin, destination); + if (board.isKingDead()) { + end(); + } + } + + private void movePiece(Position origin, Position destination) { + try { + board.movePiece(origin, destination); + } catch (IllegalPieceMoveException e) { + throw new ChessGameException(e.getMessage(), e); + } + } + + public List> getPieces() { + if (gameState.notStarted()) { + throw new ChessGameException("게임이 시작되지 않았습니다."); + } + return board.getPieces(); + } + + public void end() { + gameState = gameState.end(); + } + + public Map getStatus() { + if (gameState.notStarted()) { + throw new ChessGameException("게임이 시작되지 않았습니다."); + } + return board.getStatus(); + } + + public Turn getTurn() { + gameState = gameState.run(); + return board.getTurn(); + } + + public StatusType getStatusType() { + return gameState.getStatusType(); + } + + public GameState getGameState() { + return gameState; + } +} diff --git a/src/main/java/chess/domain/game/File.java b/src/main/java/chess/domain/game/File.java new file mode 100644 index 00000000000..56e37bde651 --- /dev/null +++ b/src/main/java/chess/domain/game/File.java @@ -0,0 +1,58 @@ +package chess.domain.game; + +import chess.domain.game.exception.ChessGameException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public enum File { + A('a', 1), + B('b', 2), + C('c', 3), + D('d', 4), + E('e', 5), + F('f', 6), + G('g', 7), + H('h', 8); + + private static final int SKIP_FIRST = 1; + + private final char fileName; + private final int order; + + File(char fileName, int order) { + this.fileName = fileName; + this.order = order; + } + + public static File from(char fileName) { + return Arrays.stream(File.values()) + .filter(it -> it.fileName == fileName) + .findAny() + .orElseThrow(() -> new ChessGameException("존재하지 않는 file 입니다")); + } + + public static File from(int order) { + return Arrays.stream(File.values()) + .filter(it -> it.order == order) + .findAny() + .orElseThrow(() -> new ChessGameException("존재하지 않는 file 입니다")); + } + + public int getDifference(File other) { + return other.order - order; + } + + public List createPath(File other) { + List files = IntStream.range(Math.min(order, other.order), Math.max(order, other.order)) + .skip(SKIP_FIRST) + .mapToObj(File::from) + .collect(Collectors.toList()); + if (order > other.order) { + Collections.reverse(files); + } + return files; + } +} diff --git a/src/main/java/chess/domain/game/Position.java b/src/main/java/chess/domain/game/Position.java new file mode 100644 index 00000000000..ac65b5d8f30 --- /dev/null +++ b/src/main/java/chess/domain/game/Position.java @@ -0,0 +1,121 @@ +package chess.domain.game; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class Position { + + private static final Map> cache = new EnumMap<>(File.class); + private static final int FILE_INDEX = 0; + private static final int RANK_INDEX = 1; + + static { + for (File file : File.values()) { + Map rankPositionHashMap = new EnumMap<>(Rank.class); + for (Rank rank : Rank.values()) { + rankPositionHashMap.put(rank, new Position(file, rank)); + } + cache.put(file, rankPositionHashMap); + } + } + + private final File file; + private final Rank rank; + + private Position(File file, Rank rank) { + this.file = file; + this.rank = rank; + } + + public static Position from(String position) { + return of(position.charAt(FILE_INDEX), position.charAt(RANK_INDEX)); + } + + public static Position of(char fileName, char rankName) { + File file = File.from(fileName); + Rank rank = Rank.from(rankName); + Map rankToPosition = cache.get(file); + return rankToPosition.get(rank); + } + + public static Position of(File file, Rank rank) { + Map rankToPosition = cache.get(file); + return rankToPosition.get(rank); + } + + public List createStraightPath(Position destination) { + if (isNotStraight(destination)) { + return Collections.emptyList(); + } + if (isDiagonal(destination)) { + return createDiagonalPath(destination); + } + return createCrossPath(destination); + } + + private boolean isNotStraight(Position destination) { + int rankDifference = getRankDifference(destination); + int fileDifference = getFileDifference(destination); + if (rankDifference == 0 || fileDifference == 0) { + return false; + } + return Math.abs(rankDifference) != Math.abs(fileDifference); + } + + private boolean isDiagonal(Position destination) { + int rankDifference = rank.getDifference(destination.rank); + int fileDifference = file.getDifference(destination.file); + return Math.abs(rankDifference) == Math.abs(fileDifference); + } + + private List createDiagonalPath(Position destination) { + List ranks = rank.createPath(destination.rank); + List files = file.createPath(destination.file); + List result = new ArrayList<>(); + for (int i = 0; i < ranks.size(); i++) { + result.add(Position.of(files.get(i), ranks.get(i))); + } + return result; + } + + private List createCrossPath(Position destination) { + if (getRankDifference(destination) == 0) { + return createFilePath(destination); + } + return createRankPath(destination); + } + + private List createFilePath(Position destination) { + List files = file.createPath(destination.file); + return files.stream() + .map(it -> Position.of(it, rank)) + .collect(Collectors.toList()); + } + + private List createRankPath(Position destination) { + List ranks = rank.createPath(destination.rank); + return ranks.stream() + .map(it -> Position.of(file, it)) + .collect(Collectors.toList()); + } + + @Override + public String toString() { + return "Position{" + + "file=" + file + + ", rank=" + rank + + '}'; + } + + public int getRankDifference(Position other) { + return rank.getDifference(other.rank); + } + + public int getFileDifference(Position other) { + return file.getDifference(other.file); + } +} diff --git a/src/main/java/chess/domain/game/Rank.java b/src/main/java/chess/domain/game/Rank.java new file mode 100644 index 00000000000..ecb19a7b9f5 --- /dev/null +++ b/src/main/java/chess/domain/game/Rank.java @@ -0,0 +1,58 @@ +package chess.domain.game; + +import chess.domain.game.exception.ChessGameException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public enum Rank { + ONE('1', 1), + TWO('2', 2), + THREE('3', 3), + FOUR('4', 4), + FIVE('5', 5), + SIX('6', 6), + SEVEN('7', 7), + EIGHT('8', 8); + + private static final int SKIP_FIRST = 1; + + private final char rankName; + private final int order; + + Rank(char rankName, int order) { + this.rankName = rankName; + this.order = order; + } + + public static Rank from(char rankName) { + return Arrays.stream(Rank.values()) + .filter(it -> it.rankName == rankName) + .findAny() + .orElseThrow(() -> new ChessGameException("존재하지 않는 rank 입니다")); + } + + public static Rank from(int rankOrder) { + return Arrays.stream(Rank.values()) + .filter(it -> it.order == rankOrder) + .findAny() + .orElseThrow(() -> new ChessGameException("존재하지 않는 rank 입니다")); + } + + public int getDifference(Rank other) { + return other.order - order; + } + + public List createPath(Rank other) { + List ranks = IntStream.range(Math.min(order, other.order), Math.max(order, other.order)) + .skip(SKIP_FIRST) + .mapToObj(Rank::from) + .collect(Collectors.toList()); + if (order > other.order) { + Collections.reverse(ranks); + } + return ranks; + } +} diff --git a/src/main/java/chess/domain/game/ScoreCalculator.java b/src/main/java/chess/domain/game/ScoreCalculator.java new file mode 100644 index 00000000000..35489adefb1 --- /dev/null +++ b/src/main/java/chess/domain/game/ScoreCalculator.java @@ -0,0 +1,55 @@ +package chess.domain.game; + +import chess.domain.piece.Color; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceType; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class ScoreCalculator { + + private static final double SAME_FILE_PAWN_MINUS_SCORE = 0.5; + private static final int MINIMUM_SCORE_MINUS_COUNT = 1; + + private final Map pieces; + + public ScoreCalculator(Map pieces) { + this.pieces = pieces; + } + + public Map calculateScore() { + return Arrays.stream(Color.values()) + .collect(Collectors.toMap(Function.identity(), this::calculateScore)); + } + + private double calculateScore(Color color) { + double defaultScore = calculateDefaultScore(color); + return defaultScore - sameFilePawnScore(color); + } + + private double calculateDefaultScore(Color color) { + return pieces.values() + .stream() + .filter(piece -> piece.isSameColor(color)) + .mapToDouble(Piece::getScore) + .sum(); + } + + + private double sameFilePawnScore(Color color) { + return Arrays.stream(File.values()) + .mapToInt(file -> countSameFilePawn(color, file)) + .filter(count -> count > MINIMUM_SCORE_MINUS_COUNT) + .mapToDouble(count -> SAME_FILE_PAWN_MINUS_SCORE * (count - MINIMUM_SCORE_MINUS_COUNT)) + .sum(); + } + + private int countSameFilePawn(Color color, File file) { + return (int) Arrays.stream(Rank.values()) + .map(rank -> pieces.get(Position.of(file, rank))) + .filter(piece -> piece.getType() == PieceType.PAWN && piece.getColor() == color) + .count(); + } +} diff --git a/src/main/java/chess/domain/game/Turn.java b/src/main/java/chess/domain/game/Turn.java new file mode 100644 index 00000000000..0087c52193b --- /dev/null +++ b/src/main/java/chess/domain/game/Turn.java @@ -0,0 +1,31 @@ +package chess.domain.game; + +import chess.domain.piece.Color; + +public class Turn { + + private final int value; + + public Turn() { + this(1); + } + + public Turn(int value) { + this.value = value; + } + + public Color getCurrentTurn() { + if (value % 2 == 0) { + return Color.BLACK; + } + return Color.WHITE; + } + + public Turn changeTurn() { + return new Turn(value + 1); + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/chess/domain/game/command/Command.java b/src/main/java/chess/domain/game/command/Command.java new file mode 100644 index 00000000000..b9704df535b --- /dev/null +++ b/src/main/java/chess/domain/game/command/Command.java @@ -0,0 +1,8 @@ +package chess.domain.game.command; + +import chess.domain.game.ChessGame; + +public interface Command { + + void execute(ChessGame chessGame); +} diff --git a/src/main/java/chess/domain/game/command/EndCommand.java b/src/main/java/chess/domain/game/command/EndCommand.java new file mode 100644 index 00000000000..6d3e3caed60 --- /dev/null +++ b/src/main/java/chess/domain/game/command/EndCommand.java @@ -0,0 +1,11 @@ +package chess.domain.game.command; + +import chess.domain.game.ChessGame; + +public class EndCommand implements Command { + + @Override + public void execute(ChessGame chessGame) { + chessGame.end(); + } +} diff --git a/src/main/java/chess/domain/game/command/MoveCommand.java b/src/main/java/chess/domain/game/command/MoveCommand.java new file mode 100644 index 00000000000..05c468c99ef --- /dev/null +++ b/src/main/java/chess/domain/game/command/MoveCommand.java @@ -0,0 +1,24 @@ +package chess.domain.game.command; + +import chess.domain.game.ChessGame; +import chess.domain.game.Position; + +public class MoveCommand implements Command { + + private final Position origin; + private final Position destination; + + private MoveCommand(Position origin, Position destination) { + this.origin = origin; + this.destination = destination; + } + + public static MoveCommand of(String origin, String destination) { + return new MoveCommand(Position.from(origin), Position.from(destination)); + } + + @Override + public void execute(ChessGame chessGame) { + chessGame.move(origin, destination); + } +} diff --git a/src/main/java/chess/domain/game/command/StartCommand.java b/src/main/java/chess/domain/game/command/StartCommand.java new file mode 100644 index 00000000000..e5743b0ae3a --- /dev/null +++ b/src/main/java/chess/domain/game/command/StartCommand.java @@ -0,0 +1,11 @@ +package chess.domain.game.command; + +import chess.domain.game.ChessGame; + +public class StartCommand implements Command { + + @Override + public void execute(ChessGame chessGame) { + chessGame.start(); + } +} diff --git a/src/main/java/chess/domain/game/constant/ChessPosition.java b/src/main/java/chess/domain/game/constant/ChessPosition.java new file mode 100644 index 00000000000..6c5340736a6 --- /dev/null +++ b/src/main/java/chess/domain/game/constant/ChessPosition.java @@ -0,0 +1,75 @@ +package chess.domain.game.constant; + +import chess.domain.game.File; +import chess.domain.game.Position; +import chess.domain.game.Rank; +import chess.domain.piece.Color; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceFactory; +import chess.domain.piece.PieceType; +import java.util.HashMap; +import java.util.Map; + +public enum ChessPosition { + A1(Position.of(File.A, Rank.ONE), PieceType.ROOK, Color.WHITE), + B1(Position.of(File.B, Rank.ONE), PieceType.KNIGHT, Color.WHITE), + C1(Position.of(File.C, Rank.ONE), PieceType.BISHOP, Color.WHITE), + D1(Position.of(File.D, Rank.ONE), PieceType.QUEEN, Color.WHITE), + E1(Position.of(File.E, Rank.ONE), PieceType.KING, Color.WHITE), + F1(Position.of(File.F, Rank.ONE), PieceType.BISHOP, Color.WHITE), + G1(Position.of(File.G, Rank.ONE), PieceType.KNIGHT, Color.WHITE), + H1(Position.of(File.H, Rank.ONE), PieceType.ROOK, Color.WHITE), + A2(Position.of(File.A, Rank.TWO), PieceType.PAWN, Color.WHITE), + B2(Position.of(File.B, Rank.TWO), PieceType.PAWN, Color.WHITE), + C2(Position.of(File.C, Rank.TWO), PieceType.PAWN, Color.WHITE), + D2(Position.of(File.D, Rank.TWO), PieceType.PAWN, Color.WHITE), + E2(Position.of(File.E, Rank.TWO), PieceType.PAWN, Color.WHITE), + F2(Position.of(File.F, Rank.TWO), PieceType.PAWN, Color.WHITE), + G2(Position.of(File.G, Rank.TWO), PieceType.PAWN, Color.WHITE), + H2(Position.of(File.H, Rank.TWO), PieceType.PAWN, Color.WHITE), + A7(Position.of(File.A, Rank.SEVEN), PieceType.PAWN, Color.BLACK), + B7(Position.of(File.B, Rank.SEVEN), PieceType.PAWN, Color.BLACK), + C7(Position.of(File.C, Rank.SEVEN), PieceType.PAWN, Color.BLACK), + D7(Position.of(File.D, Rank.SEVEN), PieceType.PAWN, Color.BLACK), + E7(Position.of(File.E, Rank.SEVEN), PieceType.PAWN, Color.BLACK), + F7(Position.of(File.F, Rank.SEVEN), PieceType.PAWN, Color.BLACK), + G7(Position.of(File.G, Rank.SEVEN), PieceType.PAWN, Color.BLACK), + H7(Position.of(File.H, Rank.SEVEN), PieceType.PAWN, Color.BLACK), + A8(Position.of(File.A, Rank.EIGHT), PieceType.ROOK, Color.BLACK), + B8(Position.of(File.B, Rank.EIGHT), PieceType.KNIGHT, Color.BLACK), + C8(Position.of(File.C, Rank.EIGHT), PieceType.BISHOP, Color.BLACK), + D8(Position.of(File.D, Rank.EIGHT), PieceType.QUEEN, Color.BLACK), + E8(Position.of(File.E, Rank.EIGHT), PieceType.KING, Color.BLACK), + F8(Position.of(File.F, Rank.EIGHT), PieceType.BISHOP, Color.BLACK), + G8(Position.of(File.G, Rank.EIGHT), PieceType.KNIGHT, Color.BLACK), + H8(Position.of(File.H, Rank.EIGHT), PieceType.ROOK, Color.BLACK); + private static final Piece EMPTY_PIECE = Piece.empty(); + + private final Position position; + private final PieceType pieceType; + private final Color color; + + ChessPosition(Position position, PieceType pieceType, Color color) { + this.position = position; + this.pieceType = pieceType; + this.color = color; + } + + public static Map initialPiecePositions() { + Map piecePositions = new HashMap<>(); + for (ChessPosition positionToPiece : ChessPosition.values()) { + piecePositions.put(positionToPiece.position, + PieceFactory.getInstance(positionToPiece.pieceType, positionToPiece.color)); + } + makeEmptyPiece(piecePositions); + return piecePositions; + } + + private static void makeEmptyPiece(Map piecePositions) { + for (File file : File.values()) { + for (Rank rank : Rank.values()) { + piecePositions.computeIfAbsent(Position.of(file, rank), ignored -> EMPTY_PIECE); + } + } + } +} diff --git a/src/main/java/chess/domain/game/exception/ChessGameException.java b/src/main/java/chess/domain/game/exception/ChessGameException.java new file mode 100644 index 00000000000..c71ed6b2687 --- /dev/null +++ b/src/main/java/chess/domain/game/exception/ChessGameException.java @@ -0,0 +1,12 @@ +package chess.domain.game.exception; + +public class ChessGameException extends RuntimeException { + + public ChessGameException(String message) { + super(message); + } + + public ChessGameException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/chess/domain/game/state/EndState.java b/src/main/java/chess/domain/game/state/EndState.java new file mode 100644 index 00000000000..e9d5c54020b --- /dev/null +++ b/src/main/java/chess/domain/game/state/EndState.java @@ -0,0 +1,42 @@ +package chess.domain.game.state; + +import chess.domain.game.exception.ChessGameException; + +public class EndState implements GameState { + + private static final StatusType STATUS_TYPE = StatusType.END; + private static final EndState INSTANCE = new EndState(); + private static final String GAME_ALREADY_OVER_MESSAGE = "게임이 종료되었습니다."; + + private EndState() { + } + + public static EndState getInstance() { + return INSTANCE; + } + + @Override + public GameState start() { + return StartState.getInstance(); + } + + @Override + public GameState end() { + throw new ChessGameException(GAME_ALREADY_OVER_MESSAGE); + } + + @Override + public GameState run() { + throw new ChessGameException(GAME_ALREADY_OVER_MESSAGE); + } + + @Override + public StatusType getStatusType() { + return STATUS_TYPE; + } + + @Override + public boolean isStarted() { + return true; + } +} diff --git a/src/main/java/chess/domain/game/state/GameState.java b/src/main/java/chess/domain/game/state/GameState.java new file mode 100644 index 00000000000..5d6d872ac0d --- /dev/null +++ b/src/main/java/chess/domain/game/state/GameState.java @@ -0,0 +1,23 @@ +package chess.domain.game.state; + +public interface GameState { + + + GameState start(); + + GameState end(); + + GameState run(); + + StatusType getStatusType(); + + default String getStateName() { + return getStatusType().getStatusName(); + } + + boolean isStarted(); + + default boolean notStarted() { + return !isStarted(); + } +} diff --git a/src/main/java/chess/domain/game/state/MovingState.java b/src/main/java/chess/domain/game/state/MovingState.java new file mode 100644 index 00000000000..c60080f9012 --- /dev/null +++ b/src/main/java/chess/domain/game/state/MovingState.java @@ -0,0 +1,39 @@ +package chess.domain.game.state; + +public class MovingState implements GameState { + + private static final StatusType STATUS_TYPE = StatusType.PLAYING; + private static final MovingState INSTANCE = new MovingState(); + + private MovingState() { + } + + public static MovingState getInstance() { + return INSTANCE; + } + + @Override + public GameState start() { + return StartState.getInstance(); + } + + @Override + public GameState end() { + return EndState.getInstance(); + } + + @Override + public GameState run() { + return this; + } + + @Override + public StatusType getStatusType() { + return STATUS_TYPE; + } + + @Override + public boolean isStarted() { + return true; + } +} diff --git a/src/main/java/chess/domain/game/state/StartState.java b/src/main/java/chess/domain/game/state/StartState.java new file mode 100644 index 00000000000..e64fdec9cc2 --- /dev/null +++ b/src/main/java/chess/domain/game/state/StartState.java @@ -0,0 +1,41 @@ +package chess.domain.game.state; + +import chess.domain.game.exception.ChessGameException; + +public class StartState implements GameState { + + private static final StatusType STATUS_TYPE = StatusType.START; + private static final StartState INSTANCE = new StartState(); + + private StartState() { + } + + public static StartState getInstance() { + return INSTANCE; + } + + @Override + public GameState start() { + return MovingState.getInstance(); + } + + @Override + public GameState end() { + return EndState.getInstance(); + } + + @Override + public GameState run() { + throw new ChessGameException("게임이 시작되지 않았습니다."); + } + + @Override + public StatusType getStatusType() { + return STATUS_TYPE; + } + + @Override + public boolean isStarted() { + return false; + } +} diff --git a/src/main/java/chess/domain/game/state/StatusType.java b/src/main/java/chess/domain/game/state/StatusType.java new file mode 100644 index 00000000000..b5272ebc12a --- /dev/null +++ b/src/main/java/chess/domain/game/state/StatusType.java @@ -0,0 +1,17 @@ +package chess.domain.game.state; + +public enum StatusType { + END("end"), + PLAYING("playing"), + START("start"); + + private final String statusName; + + StatusType(String start) { + statusName = start; + } + + public String getStatusName() { + return statusName; + } +} diff --git a/src/main/java/chess/domain/piece/Color.java b/src/main/java/chess/domain/piece/Color.java new file mode 100644 index 00000000000..c4e726d2ef1 --- /dev/null +++ b/src/main/java/chess/domain/piece/Color.java @@ -0,0 +1,17 @@ +package chess.domain.piece; + +public enum Color { + BLACK(-1), + WHITE(1), + NONE(0); + + private final int forwardDirection; + + Color(int forwardDirection) { + this.forwardDirection = forwardDirection; + } + + public int colorForwardDirection(int amount) { + return amount * forwardDirection; + } +} diff --git a/src/main/java/chess/domain/piece/ColorCompareResult.java b/src/main/java/chess/domain/piece/ColorCompareResult.java new file mode 100644 index 00000000000..86f53e7d9b8 --- /dev/null +++ b/src/main/java/chess/domain/piece/ColorCompareResult.java @@ -0,0 +1,17 @@ +package chess.domain.piece; + +public enum ColorCompareResult { + SAME_COLOR, + DIFFERENT_COLOR, + EMPTY; + + public static ColorCompareResult of(Color color1, Color color2) { + if (color1 == Color.NONE || color2 == Color.NONE) { + return EMPTY; + } + if (color1 == color2) { + return SAME_COLOR; + } + return DIFFERENT_COLOR; + } +} diff --git a/src/main/java/chess/domain/piece/Piece.java b/src/main/java/chess/domain/piece/Piece.java new file mode 100644 index 00000000000..00f856cd837 --- /dev/null +++ b/src/main/java/chess/domain/piece/Piece.java @@ -0,0 +1,65 @@ +package chess.domain.piece; + +import chess.domain.piece.exception.IllegalPieceMoveException; +import chess.domain.piece.state.MoveState; + +public class Piece { + + private static final Piece empty = PieceFactory.getInstance(PieceType.EMPTY, Color.NONE); + + private final Color color; + private final MoveState moveState; + + Piece(Color color, MoveState moveState) { + this.color = color; + this.moveState = moveState; + } + + public static Piece empty() { + return empty; + } + + public Piece move(int x, int y, Piece piece) { + boolean canMove = moveState.canMove(x, color.colorForwardDirection(y), piece.compareColor(color)); + if (!canMove) { + throw new IllegalPieceMoveException(); + } + return new Piece(color, moveState.getNextState()); + } + + private ColorCompareResult compareColor(Color color) { + return ColorCompareResult.of(this.color, color); + } + + public boolean isSameColor(Color color) { + return this.color == color; + } + + public boolean isNotSameColor(Color color) { + return !isSameColor(color); + } + + @Override + public String toString() { + return "Piece{" + + "color=" + color + + ", moveState=" + moveState + + '}'; + } + + public PieceType getType() { + return moveState.getType(); + } + + public Color getColor() { + return color; + } + + public double getScore() { + return moveState.getScore(); + } + + public boolean isKing() { + return moveState.isKing(); + } +} diff --git a/src/main/java/chess/domain/piece/PieceFactory.java b/src/main/java/chess/domain/piece/PieceFactory.java new file mode 100644 index 00000000000..c7378815a47 --- /dev/null +++ b/src/main/java/chess/domain/piece/PieceFactory.java @@ -0,0 +1,31 @@ +package chess.domain.piece; + +import chess.domain.piece.state.BishopState; +import chess.domain.piece.state.EmptyState; +import chess.domain.piece.state.InitialPawnState; +import chess.domain.piece.state.KingState; +import chess.domain.piece.state.KnightState; +import chess.domain.piece.state.MoveState; +import chess.domain.piece.state.QueenState; +import chess.domain.piece.state.RookState; +import java.util.Map; + +public class PieceFactory { + + private static final Map pieceToInitialState = Map.of( + PieceType.PAWN, InitialPawnState.getInstance(), + PieceType.KNIGHT, KnightState.getInstance(), + PieceType.ROOK, RookState.getInstance(), + PieceType.BISHOP, BishopState.getInstance(), + PieceType.QUEEN, QueenState.getInstance(), + PieceType.KING, KingState.getInstance(), + PieceType.EMPTY, EmptyState.getInstance() + ); + + private PieceFactory() { + } + + public static Piece getInstance(PieceType pieceType, Color color) { + return new Piece(color, pieceToInitialState.get(pieceType)); + } +} diff --git a/src/main/java/chess/domain/piece/PieceType.java b/src/main/java/chess/domain/piece/PieceType.java new file mode 100644 index 00000000000..14ab7a976aa --- /dev/null +++ b/src/main/java/chess/domain/piece/PieceType.java @@ -0,0 +1,21 @@ +package chess.domain.piece; + +public enum PieceType { + ROOK(5), + KNIGHT(2.5), + BISHOP(3), + QUEEN(9), + KING(0), + PAWN(1), + EMPTY(0); + + private final double score; + + PieceType(double score) { + this.score = score; + } + + public double getScore() { + return score; + } +} diff --git a/src/main/java/chess/domain/piece/exception/IllegalPieceMoveException.java b/src/main/java/chess/domain/piece/exception/IllegalPieceMoveException.java new file mode 100644 index 00000000000..4936a048517 --- /dev/null +++ b/src/main/java/chess/domain/piece/exception/IllegalPieceMoveException.java @@ -0,0 +1,8 @@ +package chess.domain.piece.exception; + +public class IllegalPieceMoveException extends RuntimeException { + + public IllegalPieceMoveException() { + super("잘못된 기물 움직임 요청입니다."); + } +} diff --git a/src/main/java/chess/domain/piece/state/BishopState.java b/src/main/java/chess/domain/piece/state/BishopState.java new file mode 100644 index 00000000000..7685a3542dc --- /dev/null +++ b/src/main/java/chess/domain/piece/state/BishopState.java @@ -0,0 +1,28 @@ +package chess.domain.piece.state; + +import chess.domain.piece.ColorCompareResult; +import chess.domain.piece.PieceType; + +public class BishopState implements MoveState { + + private static final BishopState instance = new BishopState(); + private static final PieceType pieceType = PieceType.BISHOP; + + private BishopState() { + } + + public static BishopState getInstance() { + return instance; + } + + @Override + public boolean canMove(int fileDifference, int rankDifference, ColorCompareResult colorCompareResult) { + return Math.abs(fileDifference) == Math.abs(rankDifference) + && colorCompareResult != ColorCompareResult.SAME_COLOR; + } + + @Override + public PieceType getType() { + return pieceType; + } +} diff --git a/src/main/java/chess/domain/piece/state/EmptyState.java b/src/main/java/chess/domain/piece/state/EmptyState.java new file mode 100644 index 00000000000..a9f1c459e72 --- /dev/null +++ b/src/main/java/chess/domain/piece/state/EmptyState.java @@ -0,0 +1,33 @@ +package chess.domain.piece.state; + +import chess.domain.piece.ColorCompareResult; +import chess.domain.piece.PieceType; +import chess.domain.piece.exception.IllegalPieceMoveException; + +public class EmptyState implements MoveState { + + private static final EmptyState instance = new EmptyState(); + private static final PieceType pieceType = PieceType.EMPTY; + + private EmptyState() { + } + + public static EmptyState getInstance() { + return instance; + } + + @Override + public boolean canMove(int fileDifference, int rankDifference, ColorCompareResult colorCompareResult) { + throw new IllegalPieceMoveException(); + } + + @Override + public MoveState getNextState() { + throw new IllegalPieceMoveException(); + } + + @Override + public PieceType getType() { + return pieceType; + } +} diff --git a/src/main/java/chess/domain/piece/state/InitialPawnState.java b/src/main/java/chess/domain/piece/state/InitialPawnState.java new file mode 100644 index 00000000000..cadd77d74b2 --- /dev/null +++ b/src/main/java/chess/domain/piece/state/InitialPawnState.java @@ -0,0 +1,46 @@ +package chess.domain.piece.state; + +import chess.domain.piece.ColorCompareResult; +import chess.domain.piece.PieceType; + +public class InitialPawnState implements MoveState { + + private static final InitialPawnState instance = new InitialPawnState(); + private static final PieceType pieceType = PieceType.PAWN; + + private InitialPawnState() { + } + + public static InitialPawnState getInstance() { + return instance; + } + + @Override + public boolean canMove(int fileDifference, int rankDifference, ColorCompareResult colorCompareResult) { + if (isOneOrTwoStraightMove(fileDifference, rankDifference)) { + return colorCompareResult == ColorCompareResult.EMPTY; + } + if (isOneDiagonalMove(fileDifference, rankDifference)) { + return colorCompareResult == ColorCompareResult.DIFFERENT_COLOR; + } + return false; + } + + private boolean isOneOrTwoStraightMove(int x, int y) { + return x == 0 && (y == 1 || y == 2); + } + + private boolean isOneDiagonalMove(int x, int y) { + return Math.abs(x) == 1 && y == 1; + } + + @Override + public MoveState getNextState() { + return MovedPawnState.getInstance(); + } + + @Override + public PieceType getType() { + return pieceType; + } +} diff --git a/src/main/java/chess/domain/piece/state/KingState.java b/src/main/java/chess/domain/piece/state/KingState.java new file mode 100644 index 00000000000..e6bc9cdf068 --- /dev/null +++ b/src/main/java/chess/domain/piece/state/KingState.java @@ -0,0 +1,39 @@ +package chess.domain.piece.state; + +import chess.domain.piece.ColorCompareResult; +import chess.domain.piece.PieceType; + +public class KingState implements MoveState { + + private static final KingState instance = new KingState(); + private static final PieceType pieceType = PieceType.KING; + + private KingState() { + } + + public static KingState getInstance() { + return instance; + } + + @Override + public boolean canMove(int fileDifference, int rankDifference, ColorCompareResult colorCompareResult) { + return validMoveRequest(fileDifference, rankDifference) && colorCompareResult != ColorCompareResult.SAME_COLOR; + } + + private boolean validMoveRequest(int x, int y) { + if (x == 0 && y == 0) { + return false; + } + return Math.abs(x) <= 1 && Math.abs(y) <= 1; + } + + @Override + public PieceType getType() { + return pieceType; + } + + @Override + public boolean isKing() { + return true; + } +} diff --git a/src/main/java/chess/domain/piece/state/KnightState.java b/src/main/java/chess/domain/piece/state/KnightState.java new file mode 100644 index 00000000000..64915e464e5 --- /dev/null +++ b/src/main/java/chess/domain/piece/state/KnightState.java @@ -0,0 +1,34 @@ +package chess.domain.piece.state; + +import chess.domain.piece.ColorCompareResult; +import chess.domain.piece.PieceType; + +public class KnightState implements MoveState { + + private static final KnightState instance = new KnightState(); + private static final PieceType pieceType = PieceType.KNIGHT; + + private KnightState() { + } + + public static KnightState getInstance() { + return instance; + } + + @Override + public boolean canMove(int fileDifference, int rankDifference, ColorCompareResult colorCompareResult) { + return validMoveRequest(fileDifference, rankDifference) && ColorCompareResult.SAME_COLOR != colorCompareResult; + } + + private boolean validMoveRequest(int x, int y) { + if (Math.abs(x) == 2 && Math.abs(y) == 1) { + return true; + } + return Math.abs(x) == 1 && Math.abs(y) == 2; + } + + @Override + public PieceType getType() { + return pieceType; + } +} diff --git a/src/main/java/chess/domain/piece/state/MoveState.java b/src/main/java/chess/domain/piece/state/MoveState.java new file mode 100644 index 00000000000..6ed320b04e7 --- /dev/null +++ b/src/main/java/chess/domain/piece/state/MoveState.java @@ -0,0 +1,23 @@ +package chess.domain.piece.state; + +import chess.domain.piece.ColorCompareResult; +import chess.domain.piece.PieceType; + +public interface MoveState { + + boolean canMove(int fileDifference, int rankDifference, ColorCompareResult colorCompareResult); + + default MoveState getNextState() { + return this; + } + + default double getScore() { + return getType().getScore(); + } + + PieceType getType(); + + default boolean isKing() { + return false; + } +} diff --git a/src/main/java/chess/domain/piece/state/MovedPawnState.java b/src/main/java/chess/domain/piece/state/MovedPawnState.java new file mode 100644 index 00000000000..32b1b1c9ac8 --- /dev/null +++ b/src/main/java/chess/domain/piece/state/MovedPawnState.java @@ -0,0 +1,41 @@ +package chess.domain.piece.state; + +import chess.domain.piece.ColorCompareResult; +import chess.domain.piece.PieceType; + +public class MovedPawnState implements MoveState { + + private static final MovedPawnState instance = new MovedPawnState(); + private static final PieceType pieceType = PieceType.PAWN; + + private MovedPawnState() { + } + + public static MovedPawnState getInstance() { + return instance; + } + + @Override + public boolean canMove(int fileDifference, int rankDifference, ColorCompareResult colorCompareResult) { + if (isOneStraightMove(fileDifference, rankDifference)) { + return colorCompareResult == ColorCompareResult.EMPTY; + } + if (isOneDiagonalMove(fileDifference, rankDifference)) { + return colorCompareResult == ColorCompareResult.DIFFERENT_COLOR; + } + return false; + } + + private boolean isOneStraightMove(int x, int y) { + return x == 0 && y == 1; + } + + private boolean isOneDiagonalMove(int x, int y) { + return Math.abs(x) == 1 && y == 1; + } + + @Override + public PieceType getType() { + return pieceType; + } +} diff --git a/src/main/java/chess/domain/piece/state/QueenState.java b/src/main/java/chess/domain/piece/state/QueenState.java new file mode 100644 index 00000000000..ec1bf815cc2 --- /dev/null +++ b/src/main/java/chess/domain/piece/state/QueenState.java @@ -0,0 +1,41 @@ +package chess.domain.piece.state; + +import chess.domain.piece.ColorCompareResult; +import chess.domain.piece.PieceType; + +public class QueenState implements MoveState { + + private static final QueenState instance = new QueenState(); + private static final PieceType pieceType = PieceType.QUEEN; + + private QueenState() { + } + + public static QueenState getInstance() { + return instance; + } + + @Override + public boolean canMove(int fileDifference, int rankDifference, ColorCompareResult colorCompareResult) { + return isValidRequest(fileDifference, rankDifference) && colorCompareResult != ColorCompareResult.SAME_COLOR; + } + + private boolean isValidRequest(int x, int y) { + if (x == 0 || y == 0) { + return isValidStraightRequest(x, y); + } + return Math.abs(x) == Math.abs(y); + } + + private boolean isValidStraightRequest(int x, int y) { + if (x == 0 && y != 0) { + return true; + } + return x != 0 && y == 0; + } + + @Override + public PieceType getType() { + return pieceType; + } +} diff --git a/src/main/java/chess/domain/piece/state/RookState.java b/src/main/java/chess/domain/piece/state/RookState.java new file mode 100644 index 00000000000..91f8893025f --- /dev/null +++ b/src/main/java/chess/domain/piece/state/RookState.java @@ -0,0 +1,34 @@ +package chess.domain.piece.state; + +import chess.domain.piece.ColorCompareResult; +import chess.domain.piece.PieceType; + +public class RookState implements MoveState { + + private static final RookState instance = new RookState(); + private static final PieceType pieceType = PieceType.ROOK; + + private RookState() { + } + + public static RookState getInstance() { + return instance; + } + + @Override + public boolean canMove(int fileDifference, int rankDifference, ColorCompareResult colorCompareResult) { + return isValidVariance(fileDifference, rankDifference) && colorCompareResult != ColorCompareResult.SAME_COLOR; + } + + private boolean isValidVariance(int x, int y) { + if (x != 0 && y == 0) { + return true; + } + return x == 0 && y != 0; + } + + @Override + public PieceType getType() { + return pieceType; + } +} diff --git a/src/main/java/chess/mysql/ConnectionGenerator.java b/src/main/java/chess/mysql/ConnectionGenerator.java new file mode 100644 index 00000000000..d6af35804e7 --- /dev/null +++ b/src/main/java/chess/mysql/ConnectionGenerator.java @@ -0,0 +1,25 @@ +package chess.mysql; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +class ConnectionGenerator { + + private static final String SERVER = "localhost:13306"; // MySQL 서버 주소 + private static final String DATABASE = "chess"; // MySQL DATABASE 이름 + private static final String OPTION = "?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true"; + private static final String USERNAME = "user"; // MySQL 서버 아이디 + private static final String PASSWORD = "password"; // MySQL 서버 비밀번호 + + public static Connection getConnection() { + // 드라이버 연결 + try { + return DriverManager.getConnection("jdbc:mysql://" + SERVER + "/" + DATABASE + OPTION, USERNAME, PASSWORD); + } catch (SQLException e) { + System.err.println("DB 연결 오류:" + e.getMessage()); + e.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/chess/mysql/ConnectionPool.java b/src/main/java/chess/mysql/ConnectionPool.java new file mode 100644 index 00000000000..c23de7ec331 --- /dev/null +++ b/src/main/java/chess/mysql/ConnectionPool.java @@ -0,0 +1,8 @@ +package chess.mysql; + +import java.sql.Connection; + +public interface ConnectionPool { + + Connection getConnection(); +} diff --git a/src/main/java/chess/mysql/ConnectionPoolImpl.java b/src/main/java/chess/mysql/ConnectionPoolImpl.java new file mode 100644 index 00000000000..ad4b2cf75ff --- /dev/null +++ b/src/main/java/chess/mysql/ConnectionPoolImpl.java @@ -0,0 +1,37 @@ +package chess.mysql; + +import java.sql.Connection; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ConnectionPoolImpl implements ConnectionPool { + + private static final int DEFAULT_CONNECTION_COUNT = 5; + private static final ConnectionPoolImpl instance = new ConnectionPoolImpl(); + + private final AtomicInteger index; + private final List connections; + + private ConnectionPoolImpl() { + this(DEFAULT_CONNECTION_COUNT); + } + + private ConnectionPoolImpl(int connectionCount) { + index = new AtomicInteger(0); + connections = Stream.generate(ConnectionGenerator::getConnection) + .limit(connectionCount) + .collect(Collectors.toList()); + } + + public static ConnectionPoolImpl getInstance() { + return instance; + } + + @Override + public Connection getConnection() { + int currentIndex = index.getAndIncrement(); + return connections.get(currentIndex % connections.size()); + } +} diff --git a/src/main/java/chess/mysql/JdbcTemplate.java b/src/main/java/chess/mysql/JdbcTemplate.java new file mode 100644 index 00000000000..4c3085cfead --- /dev/null +++ b/src/main/java/chess/mysql/JdbcTemplate.java @@ -0,0 +1,60 @@ +package chess.mysql; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class JdbcTemplate { + + private final ConnectionPool connectionPool; + + public JdbcTemplate(ConnectionPool connectionPool) { + this.connectionPool = connectionPool; + } + + public int save(String query, Object... parameters) { + try (PreparedStatement preparedStatement = getConnection().prepareStatement(query, + Statement.RETURN_GENERATED_KEYS)) { + for (int i = 1; i <= parameters.length; i++) { + preparedStatement.setObject(i, parameters[i - 1]); + } + preparedStatement.executeUpdate(); + ResultSet resultSet = preparedStatement.getGeneratedKeys(); + if (resultSet.next()) { + return resultSet.getInt(1); + } + throw new IllegalArgumentException("입력이 올바르지 않습니다."); + } catch (SQLException ex) { + throw new IllegalArgumentException("입력이 올바르지 않습니다."); + } + } + + public void executeUpdate(String query, Object... parameters) { + try (PreparedStatement preparedStatement = getConnection().prepareStatement(query)) { + for (int i = 1; i <= parameters.length; i++) { + preparedStatement.setObject(i, parameters[i - 1]); + } + preparedStatement.executeUpdate(); + } catch (SQLException ex) { + throw new IllegalArgumentException("입력이 올바르지 않습니다."); + } + } + + public T query(String query, RowMapper rowMapper, Object... parameters) { + try (PreparedStatement preparedStatement = getConnection().prepareStatement(query)) { + for (int i = 1; i <= parameters.length; i++) { + preparedStatement.setObject(i, parameters[i - 1]); + } + ResultSet resultSet = preparedStatement.executeQuery(); + return rowMapper.mapRow(resultSet); + } catch (SQLException e) { + throw new IllegalArgumentException("입력이 올바르지 않습니다."); + } + } + + private Connection getConnection() { + return connectionPool.getConnection(); + } +} diff --git a/src/main/java/chess/mysql/RowMapper.java b/src/main/java/chess/mysql/RowMapper.java new file mode 100644 index 00000000000..1eb97261d48 --- /dev/null +++ b/src/main/java/chess/mysql/RowMapper.java @@ -0,0 +1,9 @@ +package chess.mysql; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public interface RowMapper { + + T mapRow(ResultSet resultSet) throws SQLException; +} diff --git a/src/main/java/chess/repository/chess/ChessGameDao.java b/src/main/java/chess/repository/chess/ChessGameDao.java new file mode 100644 index 00000000000..9e4ea304169 --- /dev/null +++ b/src/main/java/chess/repository/chess/ChessGameDao.java @@ -0,0 +1,61 @@ +package chess.repository.chess; + +import chess.domain.game.state.GameState; +import chess.mysql.JdbcTemplate; +import chess.mysql.RowMapper; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class ChessGameDao { + + private final JdbcTemplate jdbcTemplate; + + public ChessGameDao(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public int save(int userId, GameState gameState) { + String query = "INSERT INTO board (user_id, status) VALUES (?, ?)"; + return jdbcTemplate.save(query, userId, gameState.getStateName()); + } + + public List findBoardIdsByUserId(int userId) { + String query = "SELECT id FROM board WHERE user_id = ?"; + return jdbcTemplate.query(query, boardIdsMapper(), userId); + } + + private RowMapper> boardIdsMapper() { + return resultSet -> { + List boardIds = new ArrayList<>(); + while (resultSet.next()) { + boardIds.add(resultSet.getInt("id")); + } + return boardIds; + }; + } + + public void delete(int boardId) { + String query = "DELETE FROM board WHERE id = ?"; + jdbcTemplate.executeUpdate(query, boardId); + } + + public void update(int boardId, GameState gameState) { + String query = "UPDATE board SET status = ? WHERE id = ?"; + jdbcTemplate.executeUpdate(query, gameState.getStateName(), boardId); + } + + public Optional findStatusByBoardId(int boardId) { + String query = "SELECT status FROM board WHERE id = ?"; + return jdbcTemplate.query(query, statusMapper(), boardId); + } + + private RowMapper> statusMapper() { + return resultSet -> { + if (resultSet.next()) { + return Optional.of(resultSet.getString("status")); + } + return Optional.empty(); + }; + } +} diff --git a/src/main/java/chess/repository/chess/ChessGameRepository.java b/src/main/java/chess/repository/chess/ChessGameRepository.java new file mode 100644 index 00000000000..b7eb0643fec --- /dev/null +++ b/src/main/java/chess/repository/chess/ChessGameRepository.java @@ -0,0 +1,19 @@ +package chess.repository.chess; + +import chess.domain.game.ChessGame; +import chess.domain.game.state.GameState; +import java.util.List; +import java.util.Optional; + +public interface ChessGameRepository { + + int create(int userId); + + Optional findById(int id); + + List findAllIdsByUserId(int userId); + + void saveGameState(int boardId, GameState gameState); + + void saveMoves(int boardId, String origin, String destination, int turn); +} diff --git a/src/main/java/chess/repository/chess/ChessGameRepositoryImpl.java b/src/main/java/chess/repository/chess/ChessGameRepositoryImpl.java new file mode 100644 index 00000000000..e38b17828f5 --- /dev/null +++ b/src/main/java/chess/repository/chess/ChessGameRepositoryImpl.java @@ -0,0 +1,86 @@ +package chess.repository.chess; + +import chess.domain.game.ChessGame; +import chess.domain.game.Position; +import chess.domain.game.state.EndState; +import chess.domain.game.state.GameState; +import chess.domain.game.state.MovingState; +import chess.domain.game.state.StartState; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class ChessGameRepositoryImpl implements ChessGameRepository { + + private final ChessGameDao chessGameDao; + private final MoveDao moveDao; + + public ChessGameRepositoryImpl(ChessGameDao chessGameDao, MoveDao moveDao) { + this.chessGameDao = chessGameDao; + this.moveDao = moveDao; + } + + @Override + public int create(int userId) { + return chessGameDao.save(userId, StartState.getInstance()); + } + + @Override + public Optional findById(int id) { + return loadChessGame(id); + } + + @Override + public List findAllIdsByUserId(int userId) { + return chessGameDao.findBoardIdsByUserId(userId); + } + + private Optional loadChessGame(int boardId) { + Optional gameState = loadGameState(boardId); + if (gameState.isEmpty()) { + return Optional.empty(); + } + List movesByBoardId = moveDao.findMovesByBoardId(boardId); + List> movesWithPosition = convertToPosition(movesByBoardId); + return Optional.of(new ChessGame(movesWithPosition, gameState.get())); + } + + private Optional loadGameState(int boardId) { + Optional status = chessGameDao.findStatusByBoardId(boardId); + if (status.isEmpty()) { + return Optional.empty(); + } + switch (status.get()) { + case "start": + return Optional.of(StartState.getInstance()); + case "playing": + return Optional.of(MovingState.getInstance()); + case "end": + return Optional.of(EndState.getInstance()); + default: + return Optional.empty(); + } + } + + private List> convertToPosition(List moves) { + return moves.stream() + .map(move -> { + List moveWithPosition = new ArrayList<>(); + moveWithPosition.add(move.getOrigin()); + moveWithPosition.add(move.getDestination()); + return moveWithPosition; + }) + .collect(Collectors.toList()); + } + + @Override + public void saveGameState(int boardId, GameState gameState) { + chessGameDao.update(boardId, gameState); + } + + @Override + public void saveMoves(int boardId, String origin, String destination, int turn) { + moveDao.save(boardId, origin, destination, turn); + } +} diff --git a/src/main/java/chess/repository/chess/MoveDao.java b/src/main/java/chess/repository/chess/MoveDao.java new file mode 100644 index 00000000000..16e3a02e288 --- /dev/null +++ b/src/main/java/chess/repository/chess/MoveDao.java @@ -0,0 +1,49 @@ +package chess.repository.chess; + +import chess.mysql.JdbcTemplate; +import chess.mysql.RowMapper; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class MoveDao { + + private final JdbcTemplate jdbcTemplate; + + public MoveDao(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public int save(int boardId, String origin, String destination, int turn) { + String query = "INSERT INTO move (board_id, origin, destination, turn) VALUES (?, ?, ?, ?)"; + return jdbcTemplate.save(query, boardId, origin, destination, turn); + } + + public List findMovesByBoardId(int boardId) { + String query = "SELECT board_id, origin, destination, turn FROM move WHERE board_id = ?"; + List moves = jdbcTemplate.query(query, movesMapper(), boardId); + moves.sort(Comparator.comparingInt(MoveDto::getTurn)); + return moves; + } + + private RowMapper> movesMapper() { + return resultSet -> { + List moves = new ArrayList<>(); + while (resultSet.next()) { + MoveDto moveDto = MoveDto.of( + resultSet.getInt("board_id"), + resultSet.getString("origin"), + resultSet.getString("destination"), + resultSet.getInt("turn") + ); + moves.add(moveDto); + } + return moves; + }; + } + + public void deleteByBoardId(int boardId) { + String query = "DELETE FROM move WHERE board_id = ?"; + jdbcTemplate.executeUpdate(query, boardId); + } +} diff --git a/src/main/java/chess/repository/chess/MoveDto.java b/src/main/java/chess/repository/chess/MoveDto.java new file mode 100644 index 00000000000..fb78cd2a2f0 --- /dev/null +++ b/src/main/java/chess/repository/chess/MoveDto.java @@ -0,0 +1,53 @@ +package chess.repository.chess; + +import chess.domain.game.Position; +import java.util.Objects; + +public class MoveDto { + + private final int gameId; + private final int turn; + private final Position origin; + private final Position destination; + + private MoveDto(int gameId, int turn, Position origin, Position destination) { + this.gameId = gameId; + this.origin = origin; + this.destination = destination; + this.turn = turn; + } + + public static MoveDto of(int gameId, String origin, String destination, int turn) { + return new MoveDto(gameId, turn, Position.from(origin), Position.from(destination)); + } + + public Position getOrigin() { + return origin; + } + + public Position getDestination() { + return destination; + } + + public int getTurn() { + return turn; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MoveDto moveDto = (MoveDto) o; + return gameId == moveDto.gameId && turn == moveDto.turn && Objects.equals(origin, moveDto.origin) + && Objects.equals(destination, moveDto.destination); + } + + @Override + public int hashCode() { + return Objects.hash(gameId, turn, origin, destination); + } +} diff --git a/src/main/java/chess/repository/user/UserDao.java b/src/main/java/chess/repository/user/UserDao.java new file mode 100644 index 00000000000..f2d3cd9ac95 --- /dev/null +++ b/src/main/java/chess/repository/user/UserDao.java @@ -0,0 +1,39 @@ +package chess.repository.user; + +import chess.mysql.JdbcTemplate; +import chess.mysql.RowMapper; +import java.util.Optional; + +public class UserDao { + + private final JdbcTemplate jdbcTemplate; + + public UserDao(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public int save(String userName) { + String query = "INSERT INTO user (user_name) VALUES (?)"; + return jdbcTemplate.save(query, userName); + } + + public Optional findUserIdIfExist(String userName) { + String query = "SELECT * FROM user WHERE user_name = ?"; + return jdbcTemplate.query(query, userIdMapper(), userName); + } + + private RowMapper> userIdMapper() { + return resultSet -> { + if (resultSet.next()) { + int userId = resultSet.getInt("id"); + return Optional.of(new UserDto(userId)); + } + return Optional.empty(); + }; + } + + public void deleteByUserName(String userName) { + String query = "DELETE FROM user WHERE user_name = ?"; + jdbcTemplate.executeUpdate(query, userName); + } +} diff --git a/src/main/java/chess/repository/user/UserDto.java b/src/main/java/chess/repository/user/UserDto.java new file mode 100644 index 00000000000..574fe50c6ee --- /dev/null +++ b/src/main/java/chess/repository/user/UserDto.java @@ -0,0 +1,14 @@ +package chess.repository.user; + +public class UserDto { + + private final int userId; + + public UserDto(int userId) { + this.userId = userId; + } + + public int getUserId() { + return userId; + } +} diff --git a/src/main/java/chess/repository/user/UserRepository.java b/src/main/java/chess/repository/user/UserRepository.java new file mode 100644 index 00000000000..0c2a321e7df --- /dev/null +++ b/src/main/java/chess/repository/user/UserRepository.java @@ -0,0 +1,6 @@ +package chess.repository.user; + +public interface UserRepository { + + int saveIfNotExist(String userName); +} diff --git a/src/main/java/chess/repository/user/UserRepositoryImpl.java b/src/main/java/chess/repository/user/UserRepositoryImpl.java new file mode 100644 index 00000000000..ab85e6e7181 --- /dev/null +++ b/src/main/java/chess/repository/user/UserRepositoryImpl.java @@ -0,0 +1,19 @@ +package chess.repository.user; + +import java.util.Optional; + +public class UserRepositoryImpl implements UserRepository { + + private final UserDao userDao; + + public UserRepositoryImpl(UserDao userDao) { + this.userDao = userDao; + } + + @Override + public int saveIfNotExist(String userName) { + Optional user = userDao.findUserIdIfExist(userName); + return user.map(UserDto::getUserId) + .orElseGet(() -> userDao.save(userName)); + } +} diff --git a/src/main/java/chess/service/ServiceFactory.java b/src/main/java/chess/service/ServiceFactory.java new file mode 100644 index 00000000000..4b585fcfe82 --- /dev/null +++ b/src/main/java/chess/service/ServiceFactory.java @@ -0,0 +1,94 @@ +package chess.service; + +import chess.mysql.ConnectionPoolImpl; +import chess.mysql.JdbcTemplate; +import chess.repository.chess.ChessGameDao; +import chess.repository.chess.ChessGameRepository; +import chess.repository.chess.ChessGameRepositoryImpl; +import chess.repository.chess.MoveDao; +import chess.repository.user.UserDao; +import chess.repository.user.UserRepositoryImpl; +import chess.service.game.EndChessGameService; +import chess.service.game.GamesService; +import chess.service.game.LoadChessGameService; +import chess.service.game.MoveChessGameService; +import chess.service.game.StartChessGameService; +import chess.service.game.StatusChessGameService; +import chess.service.room.CreateRoomService; +import chess.service.user.LoginService; + +public class ServiceFactory { + + private static final ServiceFactory instance = new ServiceFactory(); + + private final JdbcTemplate jdbcTemplate; + private final StartChessGameService startChessGameService; + private final EndChessGameService endChessGameService; + private final MoveChessGameService moveChessGameService; + private final LoadChessGameService loadChessGameService; + private final StatusChessGameService statusChessGameService; + private final CreateRoomService createRoomService; + private final LoginService loginService; + private final GamesService gamesService; + + private ServiceFactory() { + jdbcTemplate = new JdbcTemplate(ConnectionPoolImpl.getInstance()); + ChessGameRepository chessGameRepository = new ChessGameRepositoryImpl(createChessGameDao(), createMoveDao()); + loadChessGameService = new LoadChessGameService(chessGameRepository); + startChessGameService = new StartChessGameService(loadChessGameService, chessGameRepository); + endChessGameService = new EndChessGameService(loadChessGameService, chessGameRepository); + moveChessGameService = new MoveChessGameService(loadChessGameService, chessGameRepository); + statusChessGameService = new StatusChessGameService(loadChessGameService); + gamesService = new GamesService(chessGameRepository); + createRoomService = new CreateRoomService(chessGameRepository); + loginService = new LoginService(createUserRepository()); + } + + public static ServiceFactory getInstance() { + return instance; + } + + public StatusChessGameService getStatusChessGameService() { + return statusChessGameService; + } + + public CreateRoomService getCreateRoomService() { + return createRoomService; + } + + public LoginService getLoginService() { + return loginService; + } + + public GamesService getGamesService() { + return gamesService; + } + + private ChessGameDao createChessGameDao() { + return new ChessGameDao(jdbcTemplate); + } + + private UserRepositoryImpl createUserRepository() { + return new UserRepositoryImpl(new UserDao(jdbcTemplate)); + } + + private MoveDao createMoveDao() { + return new MoveDao(jdbcTemplate); + } + + public StartChessGameService getStartChessGameService() { + return startChessGameService; + } + + public EndChessGameService getEndChessGameService() { + return endChessGameService; + } + + public MoveChessGameService getMoveChessGameService() { + return moveChessGameService; + } + + public LoadChessGameService getLoadChessGameService() { + return loadChessGameService; + } +} diff --git a/src/main/java/chess/service/game/EndChessGameService.java b/src/main/java/chess/service/game/EndChessGameService.java new file mode 100644 index 00000000000..9f2de6824dc --- /dev/null +++ b/src/main/java/chess/service/game/EndChessGameService.java @@ -0,0 +1,30 @@ +package chess.service.game; + +import chess.controller.exception.BoardNotFoundException; +import chess.domain.game.ChessGame; +import chess.domain.game.command.Command; +import chess.domain.game.command.EndCommand; +import chess.domain.game.state.EndState; +import chess.repository.chess.ChessGameRepository; +import java.util.Optional; + +public class EndChessGameService { + + private final LoadChessGameService loadChessGameService; + private final ChessGameRepository chessGameRepository; + + public EndChessGameService(LoadChessGameService loadChessGameService, ChessGameRepository chessGameRepository) { + this.loadChessGameService = loadChessGameService; + this.chessGameRepository = chessGameRepository; + } + + public void end(int boardId) { + Command command = new EndCommand(); + Optional chessGame = loadChessGameService.load(boardId); + if (chessGame.isEmpty()) { + throw new BoardNotFoundException(); + } + command.execute(chessGame.get()); + chessGameRepository.saveGameState(boardId, EndState.getInstance()); + } +} diff --git a/src/main/java/chess/service/game/GamesService.java b/src/main/java/chess/service/game/GamesService.java new file mode 100644 index 00000000000..dad7c12918c --- /dev/null +++ b/src/main/java/chess/service/game/GamesService.java @@ -0,0 +1,17 @@ +package chess.service.game; + +import chess.repository.chess.ChessGameRepository; +import java.util.List; + +public class GamesService { + + private final ChessGameRepository chessGameRepository; + + public GamesService(ChessGameRepository chessGameRepository) { + this.chessGameRepository = chessGameRepository; + } + + public List findAllGamesByUserId(int userId) { + return chessGameRepository.findAllIdsByUserId(userId); + } +} diff --git a/src/main/java/chess/service/game/LoadChessGameService.java b/src/main/java/chess/service/game/LoadChessGameService.java new file mode 100644 index 00000000000..f5c1d512edc --- /dev/null +++ b/src/main/java/chess/service/game/LoadChessGameService.java @@ -0,0 +1,18 @@ +package chess.service.game; + +import chess.domain.game.ChessGame; +import chess.repository.chess.ChessGameRepository; +import java.util.Optional; + +public class LoadChessGameService { + + private final ChessGameRepository chessGameRepository; + + public LoadChessGameService(ChessGameRepository chessGameRepository) { + this.chessGameRepository = chessGameRepository; + } + + public Optional load(int boardId) { + return chessGameRepository.findById(boardId); + } +} diff --git a/src/main/java/chess/service/game/MoveChessGameService.java b/src/main/java/chess/service/game/MoveChessGameService.java new file mode 100644 index 00000000000..e2fc9fa3d18 --- /dev/null +++ b/src/main/java/chess/service/game/MoveChessGameService.java @@ -0,0 +1,29 @@ +package chess.service.game; + +import chess.controller.exception.BoardNotFoundException; +import chess.domain.game.ChessGame; +import chess.domain.game.command.Command; +import chess.domain.game.command.MoveCommand; +import chess.repository.chess.ChessGameRepository; +import java.util.Optional; + +public class MoveChessGameService { + + private final LoadChessGameService loadChessGameService; + private final ChessGameRepository chessGameRepository; + + public MoveChessGameService(LoadChessGameService loadChessGameService, ChessGameRepository chessGameRepository) { + this.loadChessGameService = loadChessGameService; + this.chessGameRepository = chessGameRepository; + } + + public void move(int boardId, String source, String target) { + Command command = MoveCommand.of(source, target); + Optional chessGame = loadChessGameService.load(boardId); + if (chessGame.isEmpty()) { + throw new BoardNotFoundException(); + } + command.execute(chessGame.get()); + chessGameRepository.saveMoves(boardId, source, target, chessGame.get().getTurn().getValue()); + } +} diff --git a/src/main/java/chess/service/game/StartChessGameService.java b/src/main/java/chess/service/game/StartChessGameService.java new file mode 100644 index 00000000000..7b79022a8af --- /dev/null +++ b/src/main/java/chess/service/game/StartChessGameService.java @@ -0,0 +1,29 @@ +package chess.service.game; + +import chess.controller.exception.BoardNotFoundException; +import chess.domain.game.ChessGame; +import chess.domain.game.command.Command; +import chess.domain.game.command.StartCommand; +import chess.repository.chess.ChessGameRepository; +import java.util.Optional; + +public class StartChessGameService { + + private final ChessGameRepository chessGameRepository; + private final LoadChessGameService loadChessGameService; + + public StartChessGameService(LoadChessGameService loadChessGameService, ChessGameRepository chessGameRepository) { + this.loadChessGameService = loadChessGameService; + this.chessGameRepository = chessGameRepository; + } + + public void start(int boardId) { + Command command = new StartCommand(); + Optional chessGame = loadChessGameService.load(boardId); + if (chessGame.isEmpty()) { + throw new BoardNotFoundException(); + } + command.execute(chessGame.get()); + chessGameRepository.saveGameState(boardId, chessGame.get().getGameState()); + } +} diff --git a/src/main/java/chess/service/game/StatusChessGameService.java b/src/main/java/chess/service/game/StatusChessGameService.java new file mode 100644 index 00000000000..9e8026c9b5c --- /dev/null +++ b/src/main/java/chess/service/game/StatusChessGameService.java @@ -0,0 +1,26 @@ +package chess.service.game; + +import chess.controller.exception.BoardNotFoundException; +import chess.controller.game.status.StatusResponse; +import chess.domain.game.ChessGame; +import chess.domain.piece.Color; +import java.util.Map; +import java.util.Optional; + +public class StatusChessGameService { + + private final LoadChessGameService loadChessGameService; + + public StatusChessGameService(LoadChessGameService loadChessGameService) { + this.loadChessGameService = loadChessGameService; + } + + public StatusResponse status(int boardId) { + Optional chessGame = loadChessGameService.load(boardId); + if (chessGame.isEmpty()) { + throw new BoardNotFoundException(); + } + Map status = chessGame.get().getStatus(); + return new StatusResponse(status.get(Color.WHITE), status.get(Color.BLACK)); + } +} diff --git a/src/main/java/chess/service/room/CreateRoomService.java b/src/main/java/chess/service/room/CreateRoomService.java new file mode 100644 index 00000000000..54d863e16f5 --- /dev/null +++ b/src/main/java/chess/service/room/CreateRoomService.java @@ -0,0 +1,16 @@ +package chess.service.room; + +import chess.repository.chess.ChessGameRepository; + +public class CreateRoomService { + + private final ChessGameRepository chessGameRepository; + + public CreateRoomService(ChessGameRepository chessGameRepository) { + this.chessGameRepository = chessGameRepository; + } + + public int createRoom(int userId) { + return chessGameRepository.create(userId); + } +} diff --git a/src/main/java/chess/service/user/LoginService.java b/src/main/java/chess/service/user/LoginService.java new file mode 100644 index 00000000000..3345a8213f7 --- /dev/null +++ b/src/main/java/chess/service/user/LoginService.java @@ -0,0 +1,16 @@ +package chess.service.user; + +import chess.repository.user.UserRepositoryImpl; + +public class LoginService { + + private final UserRepositoryImpl userRepository; + + public LoginService(UserRepositoryImpl userRepository) { + this.userRepository = userRepository; + } + + public int login(String userName) { + return userRepository.saveIfNotExist(userName); + } +} diff --git a/src/main/java/chess/view/InputView.java b/src/main/java/chess/view/InputView.java new file mode 100644 index 00000000000..bd40adfb415 --- /dev/null +++ b/src/main/java/chess/view/InputView.java @@ -0,0 +1,27 @@ +package chess.view; + +import chess.controller.main.Input; +import chess.controller.main.Request; +import chess.view.request.RequestImpl; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +public class InputView implements Input { + + private static final Scanner scanner = new Scanner(System.in); + + private final LoginImpl loginImpl; + private final JoinBoardImpl joinBoard; + + public InputView(LoginImpl loginImpl, JoinBoardImpl joinBoard) { + this.loginImpl = loginImpl; + this.joinBoard = joinBoard; + } + + @Override + public Request inputGameCommand() { + List commands = Arrays.asList(scanner.nextLine().split(" ")); + return new RequestImpl(commands, loginImpl.getUserId(), joinBoard.getBoardId()); + } +} diff --git a/src/main/java/chess/view/JoinBoardImpl.java b/src/main/java/chess/view/JoinBoardImpl.java new file mode 100644 index 00000000000..95fc3ca2195 --- /dev/null +++ b/src/main/java/chess/view/JoinBoardImpl.java @@ -0,0 +1,18 @@ +package chess.view; + +import chess.controller.room.join.JoinBoard; +import java.util.Optional; + +public class JoinBoardImpl implements JoinBoard { + + private Optional boardId = Optional.empty(); + + @Override + public void join(int boardId) { + this.boardId = Optional.of(boardId); + } + + public Optional getBoardId() { + return boardId; + } +} diff --git a/src/main/java/chess/view/LoginImpl.java b/src/main/java/chess/view/LoginImpl.java new file mode 100644 index 00000000000..cf592486e31 --- /dev/null +++ b/src/main/java/chess/view/LoginImpl.java @@ -0,0 +1,18 @@ +package chess.view; + +import chess.controller.user.Login; +import java.util.Optional; + +public class LoginImpl implements Login { + + private Optional userId = Optional.empty(); + + @Override + public void login(int userId) { + this.userId = Optional.of(userId); + } + + public Optional getUserId() { + return userId; + } +} diff --git a/src/main/java/chess/view/ViewFactory.java b/src/main/java/chess/view/ViewFactory.java new file mode 100644 index 00000000000..1b6b48cef7a --- /dev/null +++ b/src/main/java/chess/view/ViewFactory.java @@ -0,0 +1,99 @@ +package chess.view; + +import chess.controller.ErrorOutput; +import chess.controller.game.BoardOutput; +import chess.controller.game.games.GamesOutput; +import chess.controller.game.status.StatusOutput; +import chess.controller.main.InitialOutput; +import chess.controller.room.create.CreateRoomOutput; +import chess.controller.room.join.JoinBoard; +import chess.controller.room.join.JoinBoardOutput; +import chess.controller.user.Login; +import chess.controller.user.LoginOutput; +import chess.view.resposne.BoardOutputView; +import chess.view.resposne.CreateRoomOutputView; +import chess.view.resposne.ErrorOutputView; +import chess.view.resposne.GamesOutputView; +import chess.view.resposne.InitialOutputView; +import chess.view.resposne.JoinBoardOutputView; +import chess.view.resposne.LoginOutputView; +import chess.view.resposne.StatusOutputView; + +public class ViewFactory { + + private static final ViewFactory instance = new ViewFactory(); + + private final LoginImpl login; + private final JoinBoardImpl joinBoard; + private final InputView inputView; + private final BoardOutput boardOutput; + private final CreateRoomOutput createRoomOutput; + private final ErrorOutput errorOutput; + private final GamesOutput gamesOutput; + private final InitialOutput initialOutput; + private final JoinBoardOutput joinBoardOutput; + private final LoginOutput loginOutput; + private final StatusOutput statusOutput; + + private ViewFactory() { + login = new LoginImpl(); + joinBoard = new JoinBoardImpl(); + inputView = new InputView(login, joinBoard); + boardOutput = new BoardOutputView(); + createRoomOutput = new CreateRoomOutputView(); + errorOutput = new ErrorOutputView(); + gamesOutput = new GamesOutputView(); + initialOutput = new InitialOutputView(); + joinBoardOutput = new JoinBoardOutputView(); + loginOutput = new LoginOutputView(); + statusOutput = new StatusOutputView(); + } + + public static ViewFactory getInstance() { + return instance; + } + + public BoardOutput getBoardOutput() { + return boardOutput; + } + + public CreateRoomOutput getCreateRoomOutput() { + return createRoomOutput; + } + + public ErrorOutput getErrorOutput() { + return errorOutput; + } + + public GamesOutput getGamesOutput() { + return gamesOutput; + } + + public InitialOutput getInitialOutput() { + return initialOutput; + } + + public JoinBoardOutput getJoinBoardOutput() { + return joinBoardOutput; + } + + public LoginOutput getLoginOutput() { + return loginOutput; + } + + public StatusOutput getStatusOutput() { + return statusOutput; + } + + public InputView getInputView() { + return inputView; + } + + public Login getLogin() { + return login; + } + + public JoinBoard getJoinBoard() { + return joinBoard; + } +} diff --git a/src/main/java/chess/view/request/ActionTypeMapper.java b/src/main/java/chess/view/request/ActionTypeMapper.java new file mode 100644 index 00000000000..5d4f1dd48a8 --- /dev/null +++ b/src/main/java/chess/view/request/ActionTypeMapper.java @@ -0,0 +1,31 @@ +package chess.view.request; + +import chess.controller.main.ActionType; + +public enum ActionTypeMapper { + START(ActionType.START, "start"), + END(ActionType.END, "end"), + MOVE(ActionType.MOVE, "move"), + STATUS(ActionType.STATUS, "status"), + GAMES(ActionType.GAMES, "games"), + CREATE(ActionType.CREATE, "create"), + JOIN(ActionType.JOIN, "join"), + LOGIN(ActionType.LOGIN, "login"); + + private final ActionType actionType; + private final String action; + + ActionTypeMapper(ActionType actionType, String action) { + this.actionType = actionType; + this.action = action; + } + + public static ActionType map(String action) { + for (ActionTypeMapper actionTypeMapper : ActionTypeMapper.values()) { + if (actionTypeMapper.action.equalsIgnoreCase(action)) { + return actionTypeMapper.actionType; + } + } + throw new IllegalArgumentException("잘못된 요청입니다."); + } +} diff --git a/src/main/java/chess/view/request/RequestImpl.java b/src/main/java/chess/view/request/RequestImpl.java new file mode 100644 index 00000000000..5e9dd7025c6 --- /dev/null +++ b/src/main/java/chess/view/request/RequestImpl.java @@ -0,0 +1,41 @@ +package chess.view.request; + +import chess.controller.main.ActionType; +import chess.controller.main.Request; +import java.util.List; +import java.util.Optional; + +public class RequestImpl implements Request { + + private static final int ACTION_TYPE_INDEX = 0; + + private final List commands; + private final Optional boardId; + private final Optional userId; + + public RequestImpl(List commands, Optional userId, Optional boardId) { + this.commands = commands; + this.userId = userId; + this.boardId = boardId; + } + + @Override + public ActionType getActionType() { + return ActionTypeMapper.map(commands.get(ACTION_TYPE_INDEX)); + } + + @Override + public List commands() { + return commands; + } + + @Override + public Optional getBoardId() { + return boardId; + } + + @Override + public Optional getUserId() { + return userId; + } +} diff --git a/src/main/java/chess/view/resposne/BoardOutputView.java b/src/main/java/chess/view/resposne/BoardOutputView.java new file mode 100644 index 00000000000..72e1369e940 --- /dev/null +++ b/src/main/java/chess/view/resposne/BoardOutputView.java @@ -0,0 +1,24 @@ +package chess.view.resposne; + +import chess.controller.game.BoardOutput; +import java.util.List; + +public class BoardOutputView implements BoardOutput { + + @Override + public void printBoard(List> boardResponse) { + for (int i = boardResponse.size() - 1; i >= 0; i--) { + printRank(boardResponse.get(i)); + } + System.out.println(); + } + + private void printRank(List pieceResponses) { + pieceResponses.forEach(this::printPiece); + System.out.println(); + } + + private void printPiece(PieceResponse pieceResponse) { + System.out.print(pieceResponse.getPiece()); + } +} diff --git a/src/main/java/chess/view/resposne/CreateRoomOutputView.java b/src/main/java/chess/view/resposne/CreateRoomOutputView.java new file mode 100644 index 00000000000..1e94c2b0ef7 --- /dev/null +++ b/src/main/java/chess/view/resposne/CreateRoomOutputView.java @@ -0,0 +1,13 @@ +package chess.view.resposne; + +import chess.controller.room.create.CreateRoomOutput; + +public class CreateRoomOutputView implements CreateRoomOutput { + + @Override + public void printCreateRoomSuccess(int roomId) { + System.out.println("게임을 생성했습니다"); + System.out.println("게임 번호는 " + roomId + " 입니다"); + System.out.println("게임을 참여하려면 join [게임 번호]를 입력하세요"); + } +} diff --git a/src/main/java/chess/view/resposne/ErrorOutputView.java b/src/main/java/chess/view/resposne/ErrorOutputView.java new file mode 100644 index 00000000000..9567338a520 --- /dev/null +++ b/src/main/java/chess/view/resposne/ErrorOutputView.java @@ -0,0 +1,11 @@ +package chess.view.resposne; + +import chess.controller.ErrorOutput; + +public class ErrorOutputView implements ErrorOutput { + + @Override + public void printError(String message) { + System.out.println(message); + } +} diff --git a/src/main/java/chess/view/resposne/GamesOutputView.java b/src/main/java/chess/view/resposne/GamesOutputView.java new file mode 100644 index 00000000000..81bd1ac46c8 --- /dev/null +++ b/src/main/java/chess/view/resposne/GamesOutputView.java @@ -0,0 +1,15 @@ +package chess.view.resposne; + +import chess.controller.game.games.GamesOutput; +import java.util.List; + +public class GamesOutputView implements GamesOutput { + + @Override + public void printGames(List games) { + System.out.println("현재 유저의 게임들"); + games.forEach(System.out::println); + System.out.println("게임을 시작하려면 join 게임번호 를 입력해주세요"); + System.out.println("게임을 생성하려면 create 를 입력해주세요"); + } +} diff --git a/src/main/java/chess/view/resposne/InitialOutputView.java b/src/main/java/chess/view/resposne/InitialOutputView.java new file mode 100644 index 00000000000..d5b774af31d --- /dev/null +++ b/src/main/java/chess/view/resposne/InitialOutputView.java @@ -0,0 +1,18 @@ +package chess.view.resposne; + +import chess.controller.main.InitialOutput; + +public class InitialOutputView implements InitialOutput { + + @Override + public void printInitialMessage() { + System.out.println("> 체스 게임을 시작합니다."); + System.out.println("> 로그인 : login 유저id - 예. login user1"); + System.out.println("> 게임 목록 : games"); + System.out.println("> 진행중인 게임 선택 : join 게임번호 - 예. join 1"); + System.out.println("> 게임 생성 : create"); + System.out.println("> 게임 시작 : start"); + System.out.println("> 게임 종료 : end"); + System.out.println("> 게임 이동 : move source위치 target위치 - 예. move b2 b3"); + } +} diff --git a/src/main/java/chess/view/resposne/JoinBoardOutputView.java b/src/main/java/chess/view/resposne/JoinBoardOutputView.java new file mode 100644 index 00000000000..8cc78a16c97 --- /dev/null +++ b/src/main/java/chess/view/resposne/JoinBoardOutputView.java @@ -0,0 +1,14 @@ +package chess.view.resposne; + +import chess.controller.room.join.JoinBoardOutput; +import chess.domain.game.state.StatusType; + +public class JoinBoardOutputView implements JoinBoardOutput { + + @Override + public void printJoinBoardSuccess(StatusType statusType) { + System.out.println("게임에 참여했습니다"); + System.out.println("현재 게임 상태"); + System.out.println(StatusMapper.map(statusType)); + } +} diff --git a/src/main/java/chess/view/resposne/LoginOutputView.java b/src/main/java/chess/view/resposne/LoginOutputView.java new file mode 100644 index 00000000000..df0c1af9f53 --- /dev/null +++ b/src/main/java/chess/view/resposne/LoginOutputView.java @@ -0,0 +1,12 @@ +package chess.view.resposne; + +import chess.controller.user.LoginOutput; + +public class LoginOutputView implements LoginOutput { + + @Override + public void printLoginSuccess() { + System.out.println("로그인에 성공했습니다"); + System.out.println("현재 유저의 게임들을 보려면 games 를 입력해주세요"); + } +} diff --git a/src/main/java/chess/view/resposne/PieceMapper.java b/src/main/java/chess/view/resposne/PieceMapper.java new file mode 100644 index 00000000000..ba7c457e5ea --- /dev/null +++ b/src/main/java/chess/view/resposne/PieceMapper.java @@ -0,0 +1,41 @@ +package chess.view.resposne; + +import chess.domain.piece.Color; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceType; +import java.util.Arrays; + +public enum PieceMapper { + WHITE_PAWN(PieceType.PAWN, Color.WHITE, "p"), + WHITE_ROOK(PieceType.ROOK, Color.WHITE, "r"), + WHITE_KNIGHT(PieceType.KNIGHT, Color.WHITE, "n"), + WHITE_BISHOP(PieceType.BISHOP, Color.WHITE, "b"), + WHITE_QUEEN(PieceType.QUEEN, Color.WHITE, "q"), + WHITE_KING(PieceType.KING, Color.WHITE, "k"), + EMPTY(PieceType.EMPTY, Color.NONE, "."), + BLACK_PAWN(PieceType.PAWN, Color.BLACK, "P"), + BLACK_ROOK(PieceType.ROOK, Color.BLACK, "R"), + BLACK_KNIGHT(PieceType.KNIGHT, Color.BLACK, "N"), + BLACK_BISHOP(PieceType.BISHOP, Color.BLACK, "B"), + BLACK_QUEEN(PieceType.QUEEN, Color.BLACK, "Q"), + BLACK_KING(PieceType.KING, Color.BLACK, "K"); + + private final PieceType pieceType; + private final Color color; + private final String pieceName; + + PieceMapper(PieceType pieceType, Color color, String pieceName) { + this.pieceType = pieceType; + this.color = color; + this.pieceName = pieceName; + } + + public static String getPieceName(Piece piece) { + return Arrays.stream(values()) + .filter(it -> it.pieceType == piece.getType()) + .filter(it -> it.color == piece.getColor()) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 타입입니다")) + .pieceName; + } +} diff --git a/src/main/java/chess/view/resposne/PieceResponse.java b/src/main/java/chess/view/resposne/PieceResponse.java new file mode 100644 index 00000000000..e6934fc4a1e --- /dev/null +++ b/src/main/java/chess/view/resposne/PieceResponse.java @@ -0,0 +1,20 @@ +package chess.view.resposne; + +import chess.domain.piece.Piece; + +public class PieceResponse { + + private final String piece; + + private PieceResponse(String piece) { + this.piece = piece; + } + + public static PieceResponse from(Piece piece) { + return new PieceResponse(PieceMapper.getPieceName(piece)); + } + + public String getPiece() { + return piece; + } +} diff --git a/src/main/java/chess/view/resposne/StatusMapper.java b/src/main/java/chess/view/resposne/StatusMapper.java new file mode 100644 index 00000000000..20bdd4ad1e4 --- /dev/null +++ b/src/main/java/chess/view/resposne/StatusMapper.java @@ -0,0 +1,26 @@ +package chess.view.resposne; + +import chess.domain.game.state.StatusType; + +public enum StatusMapper { + START("ready", StatusType.START), + PLAYING("playing", StatusType.PLAYING), + END("end", StatusType.END); + + private final String status; + private final StatusType statusType; + + StatusMapper(String status, StatusType statusType) { + this.status = status; + this.statusType = statusType; + } + + public static String map(StatusType statusType) { + for (StatusMapper statusMapper : values()) { + if (statusMapper.statusType == statusType) { + return statusMapper.status; + } + } + throw new IllegalArgumentException("잘못된 status 입니다."); + } +} diff --git a/src/main/java/chess/view/resposne/StatusOutputView.java b/src/main/java/chess/view/resposne/StatusOutputView.java new file mode 100644 index 00000000000..1c0bb2ff074 --- /dev/null +++ b/src/main/java/chess/view/resposne/StatusOutputView.java @@ -0,0 +1,14 @@ +package chess.view.resposne; + +import chess.controller.game.status.StatusOutput; +import chess.controller.game.status.StatusResponse; + +public class StatusOutputView implements StatusOutput { + + @Override + public void printStatus(StatusResponse statusResponse) { + System.out.println("현재 점수"); + System.out.println("흰색 : " + statusResponse.getWhiteScore()); + System.out.println("검정색 : " + statusResponse.getBlackScore()); + } +} diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html deleted file mode 100644 index 3a7dbfecdf3..00000000000 --- a/src/main/resources/templates/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - 로또 - - -Hello World!! - - \ No newline at end of file diff --git a/src/test/java/.gitkeep b/src/test/java/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/test/java/chess/controller/resposne/PieceMapperTest.java b/src/test/java/chess/controller/resposne/PieceMapperTest.java new file mode 100644 index 00000000000..b8c37b43717 --- /dev/null +++ b/src/test/java/chess/controller/resposne/PieceMapperTest.java @@ -0,0 +1,29 @@ +package chess.controller.resposne; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import chess.domain.piece.Color; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceFactory; +import chess.domain.piece.PieceType; +import chess.view.resposne.PieceMapper; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@DisplayNameGeneration(ReplaceUnderscores.class) +class PieceMapperTest { + + @ParameterizedTest + @CsvSource(value = {"PAWN, p", "ROOK, r", "KNIGHT, n", "BISHOP, b", "QUEEN, q", "KING, k"}) + void getPieceName(PieceType pieceType, String expected) { + //given + Piece piece = PieceFactory.getInstance(pieceType, Color.WHITE); + //when + String result = PieceMapper.getPieceName(piece); + + //then + assertEquals(result, expected); + } +} diff --git a/src/test/java/chess/domain/BoardTest.java b/src/test/java/chess/domain/BoardTest.java new file mode 100644 index 00000000000..a78cb6b46a4 --- /dev/null +++ b/src/test/java/chess/domain/BoardTest.java @@ -0,0 +1,175 @@ +package chess.domain; + +import static chess.domain.PositionFixture.A2; +import static chess.domain.PositionFixture.A3; +import static chess.domain.PositionFixture.A4; +import static chess.domain.PositionFixture.A5; +import static chess.domain.PositionFixture.A7; +import static chess.domain.PositionFixture.B2; +import static chess.domain.PositionFixture.B4; +import static chess.domain.PositionFixture.C1; +import static chess.domain.PositionFixture.D1; +import static chess.domain.PositionFixture.D4; +import static chess.domain.PositionFixture.D5; +import static chess.domain.PositionFixture.D7; +import static chess.domain.PositionFixture.E1; +import static chess.domain.PositionFixture.E2; +import static chess.domain.PositionFixture.E3; +import static chess.domain.PositionFixture.E4; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import chess.domain.game.Board; +import chess.domain.game.exception.ChessGameException; +import chess.domain.piece.Color; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceType; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class BoardTest { + + @Test + void 체스판_초기_점수_테스트() { + //given + Board board = new Board(); + + //when + Map boardScore = board.getStatus(); + + //then + assertThat(boardScore) + .containsEntry(Color.BLACK, 38d) + .containsEntry(Color.WHITE, 38d); + } + + @Test + void 첫_번째_줄_테스트() { + //given + Board board = new Board(); + + //when + var result = board.getPieces().get(0); + + //then + var check = result.stream() + .map(Piece::getType) + .collect(Collectors.toList()); + assertThat(check) + .containsExactly(PieceType.ROOK, PieceType.KNIGHT, PieceType.BISHOP, PieceType.QUEEN, + PieceType.KING, PieceType.BISHOP, PieceType.KNIGHT, PieceType.ROOK) + .hasSize(8); + } + + @Test + void 두_번째_줄_테스트() { + //given + Board board = new Board(); + + //when + var result = board.getPieces().get(1); + + //then + var check = result.stream() + .map(Piece::getType) + .collect(Collectors.toList()); + assertThat(check) + .containsOnly(PieceType.PAWN) + .hasSize(8); + } + + @Test + void 같은편_말이_있는_곳으로_이동할_수_없다() { + //given + Board board = new Board(); + + //expect + assertThatThrownBy(() -> board.movePiece(C1, A3)) + .isInstanceOf(ChessGameException.class); + } + + @Test + void 같은편_말이_없는_곳으로_이동할_수_있다() { + //given + Board board = new Board(); + + assertDoesNotThrow(() -> { + board.movePiece(B2, B4); + board.movePiece(A7, A5); + board.movePiece(C1, A3); + }); + } + + @Test + void 같은편_말이_없는_곳으로_이동할_수_있다2() { + //given + Board board = new Board(); + + assertDoesNotThrow(() -> { + board.movePiece(B2, B4); + board.movePiece(A7, A5); + board.movePiece(C1, A3); + board.movePiece(A5, A4); + board.movePiece(D1, C1); + }); + } + + @Test + void 빈_말은_이동할_수_없다() { + //given + Board board = new Board(); + + //expect + assertThatThrownBy(() -> board.movePiece(A3, A4)) + .isInstanceOf(ChessGameException.class) + .hasMessage("이동할 말이 없습니다."); + } + + @Test + void 같은_색_말을_연속해서_움직일_수_없다() { + //given + Board board = new Board(); + board.movePiece(B2, B4); + + //expect + assertThatThrownBy(() -> board.movePiece(A2, A4)) + .isInstanceOf(ChessGameException.class) + .hasMessage("상대 말을 움직일 수 없습니다."); + } + + @Test + void 처음에는_킹이_살아있다() { + //given + Board board = new Board(); + + //when + boolean isKingDead = board.isKingDead(); + + //then + assertThat(isKingDead).isFalse(); + } + + @Test + void 킹이_죽었다() { + //given + Board board = new Board(); + board.movePiece(E2, E4); + board.movePiece(D7, D5); + board.movePiece(E1, E2); + board.movePiece(D5, D4); + board.movePiece(E2, E3); + board.movePiece(D4, E3); + + //when + boolean isKingDead = board.isKingDead(); + + //then + assertThat(isKingDead).isTrue(); + } +} diff --git a/src/test/java/chess/domain/ColorTest.java b/src/test/java/chess/domain/ColorTest.java new file mode 100644 index 00000000000..1c9588d0397 --- /dev/null +++ b/src/test/java/chess/domain/ColorTest.java @@ -0,0 +1,22 @@ +package chess.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import chess.domain.piece.Color; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ColorTest { + + @ParameterizedTest + @CsvSource(value = {"BLACK, -1", "WHITE, 1", "NONE, 0"}) + void 색에_따라_값이_변화하는_테스트(Color color, int result) { + //expect + assertThat(color.colorForwardDirection(1)) + .isEqualTo(result); + } +} diff --git a/src/test/java/chess/domain/PieceTest.java b/src/test/java/chess/domain/PieceTest.java new file mode 100644 index 00000000000..11b447b124d --- /dev/null +++ b/src/test/java/chess/domain/PieceTest.java @@ -0,0 +1,71 @@ +package chess.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import chess.domain.piece.Color; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceFactory; +import chess.domain.piece.PieceType; +import chess.domain.piece.exception.IllegalPieceMoveException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class PieceTest { + + @Test + void 피스_이름_가져오기_테스트() { + //given + Piece piece = PieceFactory.getInstance(PieceType.PAWN, Color.WHITE); + + //when + PieceType result = piece.getType(); + + //then + assertThat(result) + .isEqualTo(PieceType.PAWN); + } + + @ParameterizedTest + @CsvSource(value = {"BLACK, -2", "WHITE, 2"}) + void 폰일_경우_처음에_2칸을_움직일_수_있다(Color color, int yChange) { + //given + Piece piece = PieceFactory.getInstance(PieceType.PAWN, color); + Piece empty = Piece.empty(); + + //expect + assertDoesNotThrow(() -> piece.move(0, yChange, empty)); + } + + @ParameterizedTest + @CsvSource(value = {"PAWN, 1", "KING, 0", "QUEEN, 9", "KNIGHT, 2.5", "ROOK, 5", "BISHOP, 3"}) + void 피스_점수_가져오기_테스트(PieceType pieceType, double score) { + //given + Piece piece = PieceFactory.getInstance(pieceType, Color.WHITE); + + //when + double result = piece.getScore(); + + //then + assertThat(result) + .isEqualTo(score, withPrecision(0.0001)); + } + + @Test + void 움직일_수_없는_장소로_움직이면_예외가_발생한다() { + //given + Piece piece = PieceFactory.getInstance(PieceType.PAWN, Color.WHITE); + Piece empty = Piece.empty(); + + //expect + assertThatThrownBy(() -> piece.move(0, 3, empty)) + .isInstanceOf(IllegalPieceMoveException.class); + } +} diff --git a/src/test/java/chess/domain/PositionFixture.java b/src/test/java/chess/domain/PositionFixture.java new file mode 100644 index 00000000000..a282e279ae2 --- /dev/null +++ b/src/test/java/chess/domain/PositionFixture.java @@ -0,0 +1,29 @@ +package chess.domain; + +import chess.domain.game.File; +import chess.domain.game.Position; +import chess.domain.game.Rank; + +public class PositionFixture { + + public static final Position A1 = Position.of(File.A, Rank.ONE); + public static final Position A2 = Position.of(File.A, Rank.TWO); + public static final Position A3 = Position.of(File.A, Rank.THREE); + public static final Position A4 = Position.of(File.A, Rank.FOUR); + public static final Position A5 = Position.of(File.A, Rank.FIVE); + public static final Position A7 = Position.of(File.A, Rank.SEVEN); + public static final Position B2 = Position.of(File.B, Rank.TWO); + public static final Position B4 = Position.of(File.B, Rank.FOUR); + public static final Position C1 = Position.of(File.C, Rank.ONE); + public static final Position C3 = Position.of(File.C, Rank.THREE); + public static final Position D1 = Position.of(File.D, Rank.ONE); + public static final Position D4 = Position.of(File.D, Rank.FOUR); + public static final Position D5 = Position.of(File.D, Rank.FIVE); + public static final Position D7 = Position.of(File.D, Rank.SEVEN); + public static final Position F1 = Position.of(File.F, Rank.ONE); + public static final Position E1 = Position.of(File.E, Rank.ONE); + public static final Position E2 = Position.of(File.E, Rank.TWO); + public static final Position E3 = Position.of(File.E, Rank.THREE); + public static final Position E4 = Position.of(File.E, Rank.FOUR); + public static final Position E5 = Position.of(File.E, Rank.FIVE); +} diff --git a/src/test/java/chess/domain/game/ChessGameTest.java b/src/test/java/chess/domain/game/ChessGameTest.java new file mode 100644 index 00000000000..2f55606eef2 --- /dev/null +++ b/src/test/java/chess/domain/game/ChessGameTest.java @@ -0,0 +1,151 @@ +package chess.domain.game; + +import static chess.domain.PositionFixture.A2; +import static chess.domain.PositionFixture.A3; +import static chess.domain.PositionFixture.A5; +import static chess.domain.PositionFixture.D4; +import static chess.domain.PositionFixture.D5; +import static chess.domain.PositionFixture.D7; +import static chess.domain.PositionFixture.E1; +import static chess.domain.PositionFixture.E2; +import static chess.domain.PositionFixture.E3; +import static chess.domain.PositionFixture.E4; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import chess.domain.game.exception.ChessGameException; +import chess.domain.game.state.EndState; +import chess.domain.piece.Color; +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class ChessGameTest { + + @Test + void 시작을_하지_않은_상태로_움직이면_예외가_발생한다() { + //given + ChessGame chessGame = new ChessGame(); + + //when + //then + assertThatThrownBy(() -> chessGame.move(A2, A3)) + .isInstanceOf(ChessGameException.class) + .hasMessage("게임이 시작되지 않았습니다."); + } + + @Test + void 시작을_하지_않은_상태로_보드를_가져오면_예외가_발생한다() { + //given + ChessGame chessGame = new ChessGame(); + + //when + //then + assertThatThrownBy(chessGame::getPieces) + .isInstanceOf(ChessGameException.class) + .hasMessage("게임이 시작되지 않았습니다."); + } + + @Test + void 시작을_하지_않은_상태로_상태를_출력하면_예외가_발생한다() { + //given + ChessGame chessGame = new ChessGame(); + + //when + //then + assertThatThrownBy(chessGame::getStatus) + .isInstanceOf(ChessGameException.class) + .hasMessage("게임이 시작되지 않았습니다."); + } + + @Test + void 시작_후에_움직이면_예외가_발생하지_않는다() { + //given + ChessGame chessGame = new ChessGame(); + chessGame.start(); + + //when + //then + assertDoesNotThrow(() -> chessGame.move(A2, A3)); + } + + @Test + void 시작후에_보드_상태를_가져오면_예외가_발생하지_않는다() { + //given + ChessGame chessGame = new ChessGame(); + chessGame.start(); + + //when + //then + assertDoesNotThrow(chessGame::getPieces); + } + + @Test + void 시작후에_보드_점수를_가져오면_점수를_가져올_수_있다() { + //given + ChessGame chessGame = new ChessGame(); + chessGame.start(); + + //when + //then + assertThat(chessGame.getStatus()) + .containsEntry(Color.BLACK, 38d) + .containsEntry(Color.WHITE, 38d); + } + + @Test + void 진행중에_킹이_죽으면_게임이_종료된다() { + //given + ChessGame chessGame = new ChessGame(); + chessGame.start(); + chessGame.move(E2, E4); + chessGame.move(D7, D5); + chessGame.move(E1, E2); + chessGame.move(D5, D4); + chessGame.move(E2, E3); + chessGame.move(D4, E3); + + //when + //then + assertThatThrownBy(() -> chessGame.move(E3, D4)) + .isInstanceOf(ChessGameException.class) + .hasMessage("게임이 종료되었습니다."); + } + + @Test + void 움직일_수_없는_장소로_움직이면_예외가_발생한다() { + //given + ChessGame chessGame = new ChessGame(); + chessGame.start(); + + //when + //then + assertThatThrownBy(() -> chessGame.move(A2, A5)) + .isInstanceOf(ChessGameException.class) + .hasMessage("잘못된 기물 움직임 요청입니다."); + } + + @Test + void 움직임을_바탕으로_보드를_생성할_수_있다() { + //given + List> positions = List.of( + List.of(E2, E4), + List.of(D7, D5), + List.of(E1, E2), + List.of(D5, D4), + List.of(E2, E3), + List.of(D4, E3) + ); + ChessGame chessGame = new ChessGame(positions, EndState.getInstance()); + + //when + //then + assertThatThrownBy(() -> chessGame.move(E3, D4)) + .isInstanceOf(ChessGameException.class) + .hasMessage("게임이 종료되었습니다."); + } +} diff --git a/src/test/java/chess/domain/game/FileTest.java b/src/test/java/chess/domain/game/FileTest.java new file mode 100644 index 00000000000..35c01a5a63d --- /dev/null +++ b/src/test/java/chess/domain/game/FileTest.java @@ -0,0 +1,70 @@ +package chess.domain.game; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class FileTest { + + private static Stream generatePath() { + return Stream.of( + Arguments.of(File.A, File.D, List.of(File.B, File.C)), + Arguments.of(File.E, File.E, List.of()), + Arguments.of(File.H, File.A, List.of(File.G, File.F, File.E, File.D, File.C, File.B))); + } + + @ParameterizedTest + @CsvSource(value = {"a, A", "b, B", "c, C", "d, D", "e, E", "f, F", "g, G", "h, H"}) + void from_을_통해서_Char_로_생성할_수_있다(char input, File expected) { + //given + //when + File result = File.from(input); + + //then + assertEquals(result, expected); + } + + @ParameterizedTest + @CsvSource(value = {"1, A", "2, B", "3, C", "4, D", "5, E", "6, F", "7, G", "8, H"}) + void from_을_통해서_file_order_를_바탕으로_생성_가능(int input, File expected) { + //given + //when + File result = File.from(input); + + //then + assertEquals(result, expected); + } + + @ParameterizedTest + @CsvSource(value = {"A, B, 1", "B, C, 1", "C, D, 1", "D, E, 1", "E, F, 1", "F, G, 1", "G, H, 1", "H, A, -7"}) + void getDifference(File file, File other, int expected) { + //given + //when + int result = file.getDifference(other); + + //then + assertEquals(result, expected); + } + + @ParameterizedTest + @MethodSource("generatePath") + void createPath(File origin, File destination, List expected) { + //given + //when + List result = origin.createPath(destination); + + //then + assertThat(result) + .containsExactlyElementsOf(expected); + } +} diff --git a/src/test/java/chess/domain/game/PositionTest.java b/src/test/java/chess/domain/game/PositionTest.java new file mode 100644 index 00000000000..8b87e714df0 --- /dev/null +++ b/src/test/java/chess/domain/game/PositionTest.java @@ -0,0 +1,119 @@ +package chess.domain.game; + +import static chess.domain.PositionFixture.A1; +import static chess.domain.PositionFixture.A3; +import static chess.domain.PositionFixture.B2; +import static chess.domain.PositionFixture.C3; +import static chess.domain.PositionFixture.D4; +import static chess.domain.PositionFixture.E1; +import static chess.domain.PositionFixture.E2; +import static chess.domain.PositionFixture.E3; +import static chess.domain.PositionFixture.E4; +import static chess.domain.PositionFixture.E5; +import static chess.domain.PositionFixture.F1; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class PositionTest { + + static Stream generatePath() { + return Stream.of( + Arguments.of(A1, List.of(D4, C3, B2)), + Arguments.of(E1, List.of(E4, E3, E2)) + ); + } + + @Test + void 같은_File과_같은_Rank일_경우_동일한_객체가_반환된다() { + //given + Position position2 = Position.of(File.A, Rank.ONE); + + //when + boolean result = position2 == A1; + + //then + assertThat(result) + .isTrue(); + } + + @Test + void 포지션의_랭크_차이_계산_테스트() { + //given + //when + int result = A1.getRankDifference(A3); + + //then + assertThat(result) + .isEqualTo(2); + } + + @Test + void 포지션의_파일_차이_계산_테스트() { + //given + //when + int result = A1.getFileDifference(F1); + + //then + assertThat(result) + .isEqualTo(5); + } + + @ParameterizedTest + @CsvSource(value = {"A, TWO", "F, SIX"}) + void 한_직선상의_Postion_은_Path_를_만들_수_있다(File file, Rank rank) { + //given + Position destination = Position.of(file, rank); + + //expect + assertDoesNotThrow(() -> A1.createStraightPath(destination)); + } + + @ParameterizedTest + @CsvSource(value = {"B, THREE", "F, FIVE"}) + void 한_직선_위의_Position_이_아니라면_Path_빈_Path_를_반환한다(File file, Rank rank) { + //given + Position destination = Position.of(file, rank); + + //expect + List result = A1.createStraightPath(destination); + + assertThat(result) + .isEmpty(); + } + + @ParameterizedTest + @MethodSource("generatePath") + void 이동_경로상의_포지션들을_반환한다(Position destination, List expected) { + //given + List result = E5.createStraightPath(destination); + + //expected + assertThat(result) + .containsExactlyElementsOf(expected); + } + + @ParameterizedTest + @CsvSource(value = {"a2, A, TWO", "f6, F, SIX"}) + void 포지션을_String_값으로_생성할_수_있다(String position, File file, Rank rank) { + //given + //when + Position result = Position.from(position); + Position expected = Position.of(file, rank); + + //then + assertThat(result) + .isEqualTo(expected); + } +} diff --git a/src/test/java/chess/domain/game/RankTest.java b/src/test/java/chess/domain/game/RankTest.java new file mode 100644 index 00000000000..0868510cd61 --- /dev/null +++ b/src/test/java/chess/domain/game/RankTest.java @@ -0,0 +1,71 @@ +package chess.domain.game; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class RankTest { + + private static Stream generatePath() { + return Stream.of( + Arguments.of(Rank.ONE, Rank.FOUR, List.of(Rank.TWO, Rank.THREE)), + Arguments.of(Rank.SIX, Rank.EIGHT, List.of(Rank.SEVEN)), + Arguments.of(Rank.EIGHT, Rank.TWO, List.of(Rank.SEVEN, Rank.SIX, Rank.FIVE, Rank.FOUR, Rank.THREE)) + ); + } + + @ParameterizedTest + @CsvSource(value = {"1, ONE", "2, TWO", "3, THREE", "4, FOUR", "5, FIVE", "6, SIX", "7, SEVEN", "8, EIGHT"}) + void from_으로_문자로부터_Rank_를_생성할_수_있다(char input, Rank expected) { + //given + //when + Rank result = Rank.from(input); + + //then + assertEquals(result, expected); + } + + @ParameterizedTest + @CsvSource(value = {"1, ONE", "2, TWO", "3, THREE", "4, FOUR", "5, FIVE", "6, SIX", "7, SEVEN", "8, EIGHT"}) + void from으로_order로부터_Rank를_생성할_수_있다(int input, Rank expected) { + //given + //when + Rank result = Rank.from(input); + + //then + assertEquals(result, expected); + } + + @ParameterizedTest + @CsvSource(value = {"ONE, TWO, 1", "SIX, SEVEN, 1", "SEVEN, EIGHT, 1", "EIGHT, ONE, -7"}) + void getDifference(Rank origin, Rank destination, int expected) { + //given + //when + int result = origin.getDifference(destination); + + //then + assertEquals(result, expected); + } + + @ParameterizedTest + @MethodSource("generatePath") + void createPath_로_중간_경로를_구할_수_있다(Rank origin, Rank destination, List expected) { + //given + //when + List result = origin.createPath(destination); + + //then + assertThat(result) + .containsExactlyElementsOf(expected); + } +} diff --git a/src/test/java/chess/domain/game/ScoreCalculatorTest.java b/src/test/java/chess/domain/game/ScoreCalculatorTest.java new file mode 100644 index 00000000000..9d61858ebaa --- /dev/null +++ b/src/test/java/chess/domain/game/ScoreCalculatorTest.java @@ -0,0 +1,43 @@ +package chess.domain.game; + +import static org.assertj.core.api.Assertions.assertThat; + +import chess.domain.game.constant.ChessPosition; +import chess.domain.piece.Color; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceFactory; +import chess.domain.piece.PieceType; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class ScoreCalculatorTest { + + private final Piece WHITE_PAWN = PieceFactory.getInstance(PieceType.PAWN, Color.WHITE); + + private Map initialPiecePositions; + + @BeforeEach + void setUp() { + initialPiecePositions = ChessPosition.initialPiecePositions(); + } + + @Test + void 점수_계산_테스트() { + //given + initialPiecePositions.put(Position.of(File.A, Rank.THREE), WHITE_PAWN); + ScoreCalculator scoreCalculator = new ScoreCalculator(initialPiecePositions); + + //when + Map result = scoreCalculator.calculateScore(); + + //then + assertThat(result) + .containsEntry(Color.WHITE, 38.5d) + .containsEntry(Color.BLACK, 38d); + } +} diff --git a/src/test/java/chess/domain/game/TurnTest.java b/src/test/java/chess/domain/game/TurnTest.java new file mode 100644 index 00000000000..691389e6373 --- /dev/null +++ b/src/test/java/chess/domain/game/TurnTest.java @@ -0,0 +1,75 @@ +package chess.domain.game; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import chess.domain.piece.Color; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class TurnTest { + + @Test + void 초기는_흰색_턴이다() { + //given + Turn turn = new Turn(); + + //when + Color currentTurn = turn.getCurrentTurn(); + + //then + assertEquals(Color.WHITE, currentTurn); + } + + @Test + void 흰색_턴이_끝나면_검은색_턴이_된다() { + //given + Turn turn = new Turn(); + + //when + turn = turn.changeTurn(); + + //then + assertEquals(Color.BLACK, turn.getCurrentTurn()); + } + + @Test + void 검은색_턴이_끝나면_흰색_턴이_된다() { + //given + Turn turn = new Turn(); + + //when + turn = turn.changeTurn(); + turn = turn.changeTurn(); + + //then + assertEquals(Color.WHITE, turn.getCurrentTurn()); + } + + @Test + void getTurn을_통해_현재_턴을_구할_수_있다() { + //given + Turn turn = new Turn(); + + //when + int currentTurn = turn.getValue(); + + //then + assertEquals(1, currentTurn); + } + + @Test + void getTurn을_통해_현재_턴을_구할_수_있다2() { + //given + Turn turn = new Turn(); + + //when + turn = turn.changeTurn(); + int currentTurn = turn.getValue(); + + //then + assertEquals(2, currentTurn); + } +} diff --git a/src/test/java/chess/domain/game/state/EndStateTest.java b/src/test/java/chess/domain/game/state/EndStateTest.java new file mode 100644 index 00000000000..9aa3d4b9009 --- /dev/null +++ b/src/test/java/chess/domain/game/state/EndStateTest.java @@ -0,0 +1,56 @@ +package chess.domain.game.state; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import chess.domain.game.exception.ChessGameException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class EndStateTest { + + private final EndState endState = EndState.getInstance(); + + @Test + void getInstance_를_통해_가져오면_같은_객체를_가져온다() { + //given + EndState resultState = EndState.getInstance(); + + //when + boolean result = endState == resultState; + + //then + assertTrue(result); + } + + @Test + void start_를_시도하면_처음_상태로_돌아간다() { + //given + GameState resultState = endState.start(); + + //when + boolean result = resultState == StartState.getInstance(); + + //then + assertTrue(result); + } + + @Test + void end_를_시도하면_예외가_발생한다() { + //expect + assertThatThrownBy(endState::end) + .isInstanceOf(ChessGameException.class) + .hasMessage("게임이 종료되었습니다."); + } + + @Test + void run_을_시도하면_예외가_발생한다() { + //expect + assertThatThrownBy(endState::run) + .isInstanceOf(ChessGameException.class) + .hasMessage("게임이 종료되었습니다."); + } +} diff --git a/src/test/java/chess/domain/game/state/MovingStateTest.java b/src/test/java/chess/domain/game/state/MovingStateTest.java new file mode 100644 index 00000000000..6457759128e --- /dev/null +++ b/src/test/java/chess/domain/game/state/MovingStateTest.java @@ -0,0 +1,62 @@ +package chess.domain.game.state; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class MovingStateTest { + + private final MovingState movingState = MovingState.getInstance(); + + @Test + void getInstance_를_통해_가져오면_동일한_객체가_반환된다() { + //given + MovingState resultState = MovingState.getInstance(); + + //when + boolean result = movingState == resultState; + + //then + assertTrue(result); + } + + @Test + void start_를_실행하면_처음으로_돌아간다() { + //given + GameState resultState = movingState.start(); + + //when + boolean result = resultState == StartState.getInstance(); + + //then + assertTrue(result); + } + + @Test + void end_를_실행하면_종료된_상태가_반환된다() { + //given + GameState resultState = movingState.end(); + + //when + boolean result = resultState == EndState.getInstance(); + + //then + assertTrue(result); + } + + @Test + void run을_실행하면_현재_상태가_반환된다() { + //given + GameState resultState = movingState.run(); + + //when + boolean result = resultState == movingState; + + //then + assertTrue(result); + } +} diff --git a/src/test/java/chess/domain/game/state/StartStateTest.java b/src/test/java/chess/domain/game/state/StartStateTest.java new file mode 100644 index 00000000000..5f964439047 --- /dev/null +++ b/src/test/java/chess/domain/game/state/StartStateTest.java @@ -0,0 +1,60 @@ +package chess.domain.game.state; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import chess.domain.game.exception.ChessGameException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class StartStateTest { + + private final StartState startState = StartState.getInstance(); + + @Test + void getInstance를_사용하면_동일한_객체가_반환된다() { + //given + StartState resultState = StartState.getInstance(); + + //when + boolean result = startState == resultState; + + //then + assertTrue(result); + } + + @Test + void start_를_사용하면_움직이는_상태가_나온다() { + //given + GameState resultState = startState.start(); + + //when + boolean result = resultState == MovingState.getInstance(); + + //then + assertTrue(result); + } + + @Test + void end_를_사용하면_종료가_된_상태가_나온다() { + //given + GameState resultState = startState.end(); + + //when + boolean result = resultState == EndState.getInstance(); + + //then + assertTrue(result); + } + + @Test + void run_을_사용하면_아직_시작되지_않닸다는_예외가_발생한다() { + //expect + assertThatThrownBy(startState::run) + .isInstanceOf(ChessGameException.class) + .hasMessage("게임이 시작되지 않았습니다."); + } +} diff --git a/src/test/java/chess/domain/piece/ColorCompareResultTest.java b/src/test/java/chess/domain/piece/ColorCompareResultTest.java new file mode 100644 index 00000000000..7a0ae4b2316 --- /dev/null +++ b/src/test/java/chess/domain/piece/ColorCompareResultTest.java @@ -0,0 +1,25 @@ +package chess.domain.piece; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class ColorCompareResultTest { + + @ParameterizedTest + @CsvSource(value = {"BLACK, BLACK, SAME_COLOR", "BLACK, WHITE, DIFFERENT_COLOR", "WHITE, BLACK, DIFFERENT_COLOR", + "WHITE, WHITE, SAME_COLOR"}) + void 색깔_비교_테스트(Color color1, Color color2, ColorCompareResult expected) { + //given + //when + ColorCompareResult result = ColorCompareResult.of(color1, color2); + + //then + assertEquals(expected, result); + } +} diff --git a/src/test/java/chess/domain/piece/PieceTypeTest.java b/src/test/java/chess/domain/piece/PieceTypeTest.java new file mode 100644 index 00000000000..165e6fc6bbb --- /dev/null +++ b/src/test/java/chess/domain/piece/PieceTypeTest.java @@ -0,0 +1,19 @@ +package chess.domain.piece; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class PieceTypeTest { + + @ParameterizedTest + @CsvSource(value = {"PAWN, 1", "KNIGHT, 2.5", "BISHOP, 3", "ROOK, 5", "QUEEN, 9", "KING, 0"}) + void 각각의_피스_타입은_기물의_점수를_갖고_있다(PieceType pieceType, double expected) { + assertThat(pieceType.getScore()).isEqualTo(expected); + } +} diff --git a/src/test/java/chess/domain/piece/state/EmptyStateTest.java b/src/test/java/chess/domain/piece/state/EmptyStateTest.java new file mode 100644 index 00000000000..559795883d5 --- /dev/null +++ b/src/test/java/chess/domain/piece/state/EmptyStateTest.java @@ -0,0 +1,48 @@ +package chess.domain.piece.state; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import chess.domain.piece.ColorCompareResult; +import chess.domain.piece.PieceType; +import chess.domain.piece.exception.IllegalPieceMoveException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class EmptyStateTest { + + @Test + void 움직일_수_있는_여부를_물어보면_예외가_발생한다() { + //given + EmptyState emptyState = EmptyState.getInstance(); + + //when + //then + assertThatThrownBy(() -> emptyState.canMove(1, 1, ColorCompareResult.EMPTY)) + .isInstanceOf(IllegalPieceMoveException.class); + } + + @Test + void 다음_상태를_가져오려면_예외가_발생한다() { + //given + EmptyState emptyState = EmptyState.getInstance(); + + //when + //then + assertThatThrownBy(emptyState::getNextState) + .isInstanceOf(IllegalPieceMoveException.class); + } + + @Test + void 타입을_가져오면_빈_상태가_반환된다() { + //given + EmptyState emptyState = EmptyState.getInstance(); + + //when + //then + assertThat(emptyState.getType()).isEqualTo(PieceType.EMPTY); + } +} diff --git a/src/test/java/chess/domain/state/BishopStateTest.java b/src/test/java/chess/domain/state/BishopStateTest.java new file mode 100644 index 00000000000..65e7883ac06 --- /dev/null +++ b/src/test/java/chess/domain/state/BishopStateTest.java @@ -0,0 +1,54 @@ +package chess.domain.state; + +import static chess.domain.piece.ColorCompareResult.DIFFERENT_COLOR; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import chess.domain.piece.state.BishopState; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class BishopStateTest { + + private static final BishopState bishopState = BishopState.getInstance(); + + @ParameterizedTest + @CsvSource(value = {"1, 1", "3, 3", "-5, -5", " 7, -7", "5, -5"}) + void 비숍이_대각선으로_움직일_수_있다(int xChange, int yChange) { + //expect + assertTrue(() -> bishopState.canMove(xChange, yChange, DIFFERENT_COLOR)); + } + + @ParameterizedTest + @CsvSource(value = {"1, 3", "3, 2", "-5, -6", " 3, -7", "2, -5"}) + void 비숍이_대각선이_아닌_방향으로_움직일_수_없다(int xChange, int yChange) { + //expect + assertFalse(() -> bishopState.canMove(xChange, yChange, DIFFERENT_COLOR)); + } + + @Test + void 비숍의_다음_상태는_처음과_같다() { + //expect + assertSame(bishopState, bishopState.getNextState()); + } + + @Test + void 비숍의_점수는_3점이다() { + //expect + assertThat(bishopState.getScore()).isEqualTo(3, withPrecision(0.0001)); + } + + @Test + void 비숍은_킹이_아니다() { + //expect + assertFalse(bishopState.isKing()); + } +} diff --git a/src/test/java/chess/domain/state/KingStateTest.java b/src/test/java/chess/domain/state/KingStateTest.java new file mode 100644 index 00000000000..c1a25eca2d5 --- /dev/null +++ b/src/test/java/chess/domain/state/KingStateTest.java @@ -0,0 +1,60 @@ +package chess.domain.state; + +import static chess.domain.piece.ColorCompareResult.DIFFERENT_COLOR; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import chess.domain.piece.state.KingState; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class KingStateTest { + + private static final KingState kingState = KingState.getInstance(); + + @ParameterizedTest + @CsvSource(value = {"1, 1", "1, 0", "0, -1", "-1, 1", "0, 1"}) + void 킹은_대각선_혹은_직선으로_1칸_움직일_수_있다(int xChange, int yChange) { + //expect + assertTrue(() -> kingState.canMove(xChange, yChange, DIFFERENT_COLOR)); + } + + @ParameterizedTest + @CsvSource(value = {"1, 3", "0, 2", "-2, -2", "2, 0", "-1, -2"}) + void 킹은_대각선이나_직선으로_1칸만_이동_가능하다(int xChange, int yChange) { + //expect + assertFalse(() -> kingState.canMove(xChange, yChange, DIFFERENT_COLOR)); + } + + @Test + void 킹의_다음_상태는_처음과_같다() { + //expect + assertSame(kingState, kingState.getNextState()); + } + + @Test + void 킹의_점수는_0점이다() { + //expect + assertThat(kingState.getScore()).isEqualTo(0, withPrecision(0.0001)); + } + + @Test + void 킹은_킹이다() { + //expect + assertTrue(kingState.isKing()); + } + + @Test + void 제자리로_움직이면_움직일_수_없다() { + //expect + assertFalse(kingState.canMove(0, 0, DIFFERENT_COLOR)); + } +} diff --git a/src/test/java/chess/domain/state/KnightStateTest.java b/src/test/java/chess/domain/state/KnightStateTest.java new file mode 100644 index 00000000000..e8729658185 --- /dev/null +++ b/src/test/java/chess/domain/state/KnightStateTest.java @@ -0,0 +1,55 @@ +package chess.domain.state; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import chess.domain.piece.ColorCompareResult; +import chess.domain.piece.state.KnightState; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class KnightStateTest { + + private static final KnightState knightState = KnightState.getInstance(); + + @ParameterizedTest + @CsvSource(value = {"2, 1", "2, -1", "1, 2", "-1, 2"}) + void 나이트는_한_방향으로_이동한_후_해당_방향으로_대각선_이동_테스트(int xChange, int yChange) { + //expect + assertTrue(() -> knightState.canMove(xChange, yChange, ColorCompareResult.DIFFERENT_COLOR)); + } + + + @ParameterizedTest + @CsvSource(value = {"2, 2", "2, -2", "0, 2"}) + void 나이트_이동_예외_테스트(int xChange, int yChange) { + //expect + assertFalse(() -> knightState.canMove(xChange, yChange, ColorCompareResult.DIFFERENT_COLOR)); + } + + @Test + void 나이트의_다음_상태는_처음과_같다() { + //expect + assertSame(knightState, knightState.getNextState()); + } + + @Test + void 나이트의_점수는_2_5_점이다() { + //expect + assertThat(knightState.getScore()).isEqualTo(2.5, withPrecision(0.0001)); + } + + @Test + void 나이트는_킹이_아니다() { + //expect + assertFalse(knightState.isKing()); + } +} diff --git a/src/test/java/chess/domain/state/PawnStateTest.java b/src/test/java/chess/domain/state/PawnStateTest.java new file mode 100644 index 00000000000..3022f57c5b5 --- /dev/null +++ b/src/test/java/chess/domain/state/PawnStateTest.java @@ -0,0 +1,95 @@ +package chess.domain.state; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import chess.domain.piece.ColorCompareResult; +import chess.domain.piece.state.InitialPawnState; +import chess.domain.piece.state.MoveState; +import chess.domain.piece.state.MovedPawnState; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +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; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class PawnStateTest { + + private static final InitialPawnState initialPawnState = InitialPawnState.getInstance(); + private static final MovedPawnState movedPawnState = MovedPawnState.getInstance(); + + @Test + void 폰은_초기상태에_앞으로_두칸_갈_수_있다() { + //expect + assertTrue(() -> initialPawnState.canMove(0, 2, ColorCompareResult.EMPTY)); + } + + @Test + void 폰은_딱_한번_앞으로_두_칸_전진할_수_있다() { + // given + // when + boolean firstMove = initialPawnState.canMove(0, 2, ColorCompareResult.EMPTY); + MoveState finalState = initialPawnState.getNextState(); + boolean secondResult = finalState.canMove(0, 2, ColorCompareResult.EMPTY); + + // then + assertTrue(firstMove); + assertFalse(secondResult); + } + + @Test + void 폰_대각선_움직임_테스트() { + //expect + assertTrue(() -> initialPawnState.canMove(1, 1, ColorCompareResult.DIFFERENT_COLOR)); + } + + @ParameterizedTest + @ValueSource(strings = {"EMPTY", "SAME_COLOR"}) + void 폰_대각선_움직임_색_예외_테스트(ColorCompareResult colorCompareResult) { + //expect + assertFalse(() -> initialPawnState.canMove(1, 1, colorCompareResult)); + } + + @ParameterizedTest + @CsvSource(value = {"1, 0", "1, 2", "2, 1"}) + void 폰_대각선_움직임_좌표_예외_테스트(int xChange, int yChange) { + //expect + assertFalse(() -> initialPawnState.canMove(xChange, yChange, ColorCompareResult.DIFFERENT_COLOR)); + } + + @Test + void 초기_폰의_다음_상태는_움직인_폰_상태이다() { + //expect + assertSame(initialPawnState.getNextState(), movedPawnState.getNextState()); + } + + @Test + void 움직인_폰의_다음_상태는_움직인_폰_상태이다() { + //expect + assertSame(movedPawnState, movedPawnState.getNextState()); + } + + @Test + void 폰의_점수는_1점이다() { + //expect + assertThat(initialPawnState.getScore()).isEqualTo(1, withPrecision(0.0001)); + } + + @Test + void 폰은_킹이_아니다() { + //expect + assertFalse(initialPawnState.isKing()); + } + + @Test + void 움직인_폰의_타입을_가져와도_폰이다() { + //expect + assertSame(initialPawnState.getType(), movedPawnState.getType()); + } +} diff --git a/src/test/java/chess/domain/state/QueenStateTest.java b/src/test/java/chess/domain/state/QueenStateTest.java new file mode 100644 index 00000000000..5c1e2aa619d --- /dev/null +++ b/src/test/java/chess/domain/state/QueenStateTest.java @@ -0,0 +1,54 @@ +package chess.domain.state; + +import static chess.domain.piece.ColorCompareResult.DIFFERENT_COLOR; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import chess.domain.piece.state.QueenState; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class QueenStateTest { + + private static final QueenState queenState = QueenState.getInstance(); + + @ParameterizedTest + @CsvSource(value = {"1, 1", "3, 0", "-5, -5", " 0, -7", "5, -5"}) + void 퀸은_대각선_혹은_직선으로_움직일_수_있다(int xChange, int yChange) { + //expect + assertTrue(() -> queenState.canMove(xChange, yChange, DIFFERENT_COLOR)); + } + + @ParameterizedTest + @CsvSource(value = {"1, 3", "3, 2", "-5, -6", " 3, -7", "2, -5"}) + void 퀸이_대각선이_아닌_방향으로_움직일_수_없다(int xChange, int yChange) { + //expect + assertFalse(() -> queenState.canMove(xChange, yChange, DIFFERENT_COLOR)); + } + + @Test + void 퀸의_다음_상태는_처음과_같다() { + //expect + assertSame(queenState, queenState.getNextState()); + } + + @Test + void 퀸의_점수는_9점이다() { + //expect + assertThat(queenState.getScore()).isEqualTo(9.0, withPrecision(0.0001)); + } + + @Test + void 퀸은_킹이_아니다() { + //expect + assertFalse(queenState.isKing()); + } +} diff --git a/src/test/java/chess/domain/state/RookStateTest.java b/src/test/java/chess/domain/state/RookStateTest.java new file mode 100644 index 00000000000..cb1811ccfe0 --- /dev/null +++ b/src/test/java/chess/domain/state/RookStateTest.java @@ -0,0 +1,54 @@ +package chess.domain.state; + +import static chess.domain.piece.ColorCompareResult.DIFFERENT_COLOR; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.withPrecision; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import chess.domain.piece.state.RookState; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@SuppressWarnings({"NonAsciiCharacters"}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RookStateTest { + + private static final RookState rookState = RookState.getInstance(); + + @ParameterizedTest + @CsvSource(value = {"1, 0", "3, 0", "-5, 0", "0, -7", "0, -5"}) + void 룩은_직선으로_움직일_수_있다(int xChange, int yChange) { + //expect + assertTrue(() -> rookState.canMove(xChange, yChange, DIFFERENT_COLOR)); + } + + @ParameterizedTest + @CsvSource(value = {"1, 3", "3, 2", "-5, -6", " 3, -7", "2, -5"}) + void 비숍이_직선이_아닌_방향으로_움직일_수_없다(int xChange, int yChange) { + //expect + assertFalse(() -> rookState.canMove(xChange, yChange, DIFFERENT_COLOR)); + } + + @Test + void 룩의_다음_상태는_처음과_같다() { + //expect + assertSame(rookState, rookState.getNextState()); + } + + @Test + void 룩의_점수는_5점이다() { + //expect + assertThat(rookState.getScore()).isEqualTo(5, withPrecision(0.0001)); + } + + @Test + void 룩은_킹이_아니다() { + //expect + assertFalse(rookState.isKing()); + } +} diff --git a/src/test/java/chess/mysql/ConnectionGeneratorTest.java b/src/test/java/chess/mysql/ConnectionGeneratorTest.java new file mode 100644 index 00000000000..21c04b167c2 --- /dev/null +++ b/src/test/java/chess/mysql/ConnectionGeneratorTest.java @@ -0,0 +1,18 @@ +package chess.mysql; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +class ConnectionGeneratorTest { + + @Test + void connection을_생성하면_null이_아닌_결과가_반환된다() { + var connection = ConnectionGenerator.getConnection(); + assertNotNull(connection); + } +} diff --git a/src/test/java/chess/mysql/TestConnectionPool.java b/src/test/java/chess/mysql/TestConnectionPool.java new file mode 100644 index 00000000000..878c630ca87 --- /dev/null +++ b/src/test/java/chess/mysql/TestConnectionPool.java @@ -0,0 +1,25 @@ +package chess.mysql; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class TestConnectionPool implements ConnectionPool { + + private static final String SERVER = "localhost:13306"; // MySQL 서버 주소 + private static final String DATABASE = "chess"; // MySQL DATABASE 이름 + private static final String OPTION = "?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true"; + private static final String USERNAME = "user"; // MySQL 서버 아이디 + private static final String PASSWORD = "password"; // MySQL 서버 비밀번호 + + @Override + public Connection getConnection() { + try { + return DriverManager.getConnection("jdbc:mysql://" + SERVER + "/" + DATABASE + OPTION, USERNAME, PASSWORD); + } catch (SQLException e) { + System.err.println("DB 연결 오류:" + e.getMessage()); + e.printStackTrace(); + return null; + } + } +} diff --git a/src/test/java/chess/repository/ChessGameDaoTest.java b/src/test/java/chess/repository/ChessGameDaoTest.java new file mode 100644 index 00000000000..4ab8f69ff4b --- /dev/null +++ b/src/test/java/chess/repository/ChessGameDaoTest.java @@ -0,0 +1,79 @@ +package chess.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import chess.domain.game.state.MovingState; +import chess.domain.game.state.StartState; +import chess.mysql.JdbcTemplate; +import chess.mysql.TestConnectionPool; +import chess.repository.chess.ChessGameDao; +import chess.repository.user.UserDao; +import chess.repository.user.UserDto; +import java.util.Optional; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +@TestMethodOrder(OrderAnnotation.class) +class ChessGameDaoTest { + + private static final UserDao userDao = new UserDao(new JdbcTemplate(new TestConnectionPool())); + + private static int userId; + private final ChessGameDao chessGameDao = new ChessGameDao(new JdbcTemplate(new TestConnectionPool())); + + @BeforeAll + static void 사용자_생성() { + userDao.deleteByUserName("ChessGameDaoTest"); + userDao.save("ChessGameDaoTest"); + + Optional userDto = userDao.findUserIdIfExist("ChessGameDaoTest"); + if (userDto.isEmpty()) { + throw new IllegalArgumentException("사용자가 존재하지 않습니다."); + } + userId = userDto.get().getUserId(); + } + + @Test + @Order(1) + void save를_통해_새로운_보드를_생성할_수_있다() { + //expect + assertDoesNotThrow(() -> chessGameDao.save(userId, StartState.getInstance())); + } + + @Test + @Order(2) + void findBoardIdsByUserId() { + //expect + assertThat(chessGameDao.findBoardIdsByUserId(userId)).isNotEmpty(); + } + + + @Test + @Order(3) + void delete() { + //given + int boardId = chessGameDao.findBoardIdsByUserId(userId).get(0); + + //expect + assertDoesNotThrow(() -> chessGameDao.delete(boardId)); + } + + @Test + @Order(4) + void update() { + //given + int boardId = chessGameDao.save(userId, StartState.getInstance()); + chessGameDao.update(boardId, MovingState.getInstance()); + + //expect + assertThat(chessGameDao.findStatusByBoardId(boardId)).contains(MovingState.getInstance().getStateName()); + } +} diff --git a/src/test/java/chess/repository/MoveDaoTest.java b/src/test/java/chess/repository/MoveDaoTest.java new file mode 100644 index 00000000000..f3cdd4e036d --- /dev/null +++ b/src/test/java/chess/repository/MoveDaoTest.java @@ -0,0 +1,46 @@ +package chess.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import chess.mysql.JdbcTemplate; +import chess.mysql.TestConnectionPool; +import chess.repository.chess.MoveDao; +import chess.repository.chess.MoveDto; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +@DisplayNameGeneration(ReplaceUnderscores.class) +@TestMethodOrder(OrderAnnotation.class) +class MoveDaoTest { + + private final MoveDao moveDao = new MoveDao(new JdbcTemplate(new TestConnectionPool())); + + @Test + @Order(1) + void save를_통해_움직임을_저장할_수_있다() { + assertDoesNotThrow(() -> moveDao.save(3, "a2", "a3", 1)); + assertDoesNotThrow(() -> moveDao.save(3, "a7", "a5", 2)); + } + + @Test + @Order(2) + void findMovesByBoardId() { + assertThat(moveDao.findMovesByBoardId(3)) + .containsExactly( + MoveDto.of(3, "a2", "a3", 1), + MoveDto.of(3, "a7", "a5", 2) + ); + } + + @Test + @Order(3) + void deleteByBoardId() { + assertDoesNotThrow(() -> moveDao.deleteByBoardId(3)); + } +} diff --git a/src/test/java/chess/repository/UserDaoTest.java b/src/test/java/chess/repository/UserDaoTest.java new file mode 100644 index 00000000000..b6ec88e1c74 --- /dev/null +++ b/src/test/java/chess/repository/UserDaoTest.java @@ -0,0 +1,33 @@ +package chess.repository; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import chess.mysql.JdbcTemplate; +import chess.mysql.TestConnectionPool; +import chess.repository.user.UserDao; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +@DisplayNameGeneration(ReplaceUnderscores.class) +@TestMethodOrder(OrderAnnotation.class) +class UserDaoTest { + + private final UserDao userDao = new UserDao(new JdbcTemplate(new TestConnectionPool())); + + @Test + @Order(1) + void addUser() { + //expect + assertDoesNotThrow(() -> userDao.save("testUser")); + } + + @Test + @Order(2) + void deleteByUserName() { + assertDoesNotThrow(() -> userDao.deleteByUserName("testUser")); + } +}