diff --git a/README.md b/README.md index 8102f91c870..bf9a0c4ce59 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,171 @@ ## 우아한테크코스 코드리뷰 - [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) + +### 개요 + +체스는 가로와 세로가 각각 8줄씩 64칸으로 격자로 배열 된 체스보드에서 두 명의 플레이어가 기물들을 규칙에 따라 움직여 싸우는 보드 게임이다. + +### 요구사항 + +**체스** + +- [x] 흰색, 검은색으로 나뉘며 한 턴씩 번갈아 가며 기물을 움직인다. +- [x] 흰색 진영이 기물을 먼저 움직인다. +- [x] 킹이 잡힌 경우 게임을 종료한다. + +**보드** + +- [x] 보드는 가로 8칸, 세로 8칸로 이루어져있고, 총 64칸이 존재한다. + - [x] 가로(Rank)줄은 아래부터 위로 1 ~ 8이다. + - [x] 세로(File)줄은 왼쪽에서 오른쪽으로 A ~ H이다. +- [x] 각 칸에는 기물이 존재하거나, 존재하지 않을 수 있다. +- [x] 보드의 상태는 초기(InitialState), 게임 진행(PlayState), 종료(EndState)가 있다. + - [x] 초기(Initial) 상태는 초기화되지 않은 보드를 의미한다. + - [x] 초기화하는 경우 게임 진행(Play) 상태가 된다. + - [x] 게임 진행(Play) 상태는 초기화 된, 게임을 진행되고 있는 상태를 의미한다. + - [x] 어느 한 쪽의 왕이 잡힌 겨우 게임이 종료(End) 상태가 된다. + - [x] 종료(End) 상태는 게임을 진행할 수 없다. + +**기물** + +- [x] 체스 기물에는 킹(King), 퀸(Queen), 룩(Rook), 비숍(Bishop), 나이트(Knight), 폰(Pawn)이 존재한다. +- [x] 각 기물은 움직일 수 있다. + - [x] 킹: 직선, 대각선 모두 1칸 움직일 수 있다. + - [x] 퀸: 직선, 대각선 칸 수 제한없이 움직일 수 있다. + - [x] 룩: 직선 칸 수 제한없이 움직일 수 있다. + - [x] 비숍: 대각선 칸 수 제한없이 움직일 수 있다. + - [x] 나이트: 2칸 전진한 상태에서 좌우로 1칸 움직일 수 있다. 다른 기물을 뛰어넘을 수 있다. + - [x] 폰: 전진만 가능하다. 초기에는 2칸, 그 이외에는 1칸 움직일 수 있다. 기물을 잡는 경우 반드시 대각선으로 움직여야 한다. +- [x] 기물을 여러 칸 움직이는 경우 사이에 다른 기물이 존재한다면 움직일 수 없다. +- [x] 움직이려는 칸에 같은 편 기물이 존재하는 경우 움직일 수 없다. +- [x] 움직이려는 칸에 상대 편 기물이 존재하는 경우 해당 기물을 잡는다. + +**점수 계산** + +- [x] 각 진영의 점수를 구한다. 기물의 점수는 아래와 같다. + - [x] 킹: 0점 + - [x] 퀸: 9점 + - [x] 룩: 5점 + - [x] 비숍: 3점 + - [x] 나이트: 2.5점 + - [x] 폰: 1점 +- [x] 폰의 경우 세로(File)줄에 여러 개 있는 경우(더블 폰, 트리플 폰) 각 0.5점으로 계산한다. + +### 컨트롤러 + +- [x] MainController + - [x] USER, GAME, START의 명령어를 입력할 수 있다. + - [x] USER 명령어를 입력하면 계정 관리 화면으로 이동한다. (UserController) + - [x] GAME 명령어를 입력하면 게임 관리 화면으로 이동한다. (BoardController) + - [x] 해당 화면은 로그인을 해야지 이동할 수 있다. + - [x] START 명령어를 입력하면 선택한 게임을 시작한다. (GameController) + - [x] 로그인을 하지 않은 경우 게임 기록이 데이터베이스에 저장되지만 따로 관리할 수 없다. + +- [x] UserController + - [x] REGISTER, LOGIN, LOGOUT 명령어를 입력할 수 있다. + - [x] REGISTER 명령어와 함께 아이디를 입력하여 계정을 생성할 수 있다. ex) REGISTER herb + - [x] LOGIN 명령어와 함께 아이디를 입력하여 로그인을 할 수 있다. ex) LOGIN herb + - [x] LOGOUT 명령어를 입력하여 로그아웃을 할 수 있다. ex) LOGOUT + +- [x] BoardController + - [x] 게임 관리 화면으로 로그인을 해야지 이동할 수 있다. + - [x] HISTORY, CREATE, JOIN 명령어를 입력할 수 있다. + - [x] HISTORY 명령어를 입력하여 진행한 게임을 확인할 수 있다. + - [x] CREATE 명령어를 이용하여 게임을 생성할 수 있다. ex) CREATE 방이름 + - [x] JOIN 명령어와 게임 번호를 입력하여 진행할 게임을 선택할 수 있다. ex) JOIN 3 + +- [x] GameController + - [x] 게임 선택을 하지 않은 경우 새로운 게임이 데이터베이스에 저장된다. + - [x] MOVE, STATUS, END 명령어를 입력할 수 있다. + - [x] MOVE 명령어를 입력하면 기물을 이동시킨다. ex) move e2 e4 + - [x] STATUS 명령어를 입력하면 현재 게임의 상태를 볼 수 있다. + - [x] END 명령어를 입력하면 현재 게임을 종료한다. + +### 출력 + +- [x] 게임 시작시 명령어를 안내하는 내용을 출력한다. + +``` +> 체스 게임을 시작합니다. +> 게임 상태 : status +> 게임 종료 : end +> 게임 초기화 : clear +> 게임 이동 : move source위치 target위치 - 예. move b2 b3 +``` + +- [x] 시작(START) 명령어 입력시 초기화된 보드가 출력된다. + +``` +RNBQKBNR +PPPPPPPP +........ +........ +........ +........ +pppppppp +rnbqkbnr +``` + +- [x] 이동(MOVE) 명령어 입력시 이동 후의 보드가 출력된다. + +``` +RNBQKBNR +PPPPPPPP +........ +........ +....p... +........ +pppp.ppp +rnbqkbnr +``` + +- [x] 상태(STATUS) 명령어 입력시 각 진영의 점수를 확인한다. + - [x] 왕이 잡히는 경우 패배로 출력된다. + - [x] 양쪽 진영 다 왕이 잡히지 않은 경우 점수로 결과를 출력한다. + +``` +흰색 점수: 38.0 +검은색 점수: 38.0 +현재 상태: 백색 승 +``` + +### 컨트롤러 + +```mermaid +graph TD + MainController --> UserController --> UserService --> UserDao + MainController --> RoomController --> RoomService --> RoomDao + MainController --> GameController --> GameSerivce --> GameDao +``` + +### 체스 게임 + +```mermaid +graph TD + GameController --> GameInputView + GameController --> GameOutputView --> BoardConverter + + GameController --> GameService + GameService --> Board + Board --> BoardGenerator + Board --> GameResult + Position --> Rank + Position --> File + + Board --> Position + Board --> PIECE + PIECE --> Color + PIECE --> PieceType + + subgraph PIECE + direction BT + Pawn -.-> Piece + Rook -.-> Piece + Bishop -.-> Piece + Knight -.-> Piece + King -.-> Piece + Queen -.-> Piece + Empty -.-> Piece + end +``` diff --git a/build.gradle b/build.gradle index 62e0781fadb..cfa2260d887 100644 --- a/build.gradle +++ b/build.gradle @@ -9,11 +9,9 @@ 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/src/main/java/chess/ChessGameApplication.java b/src/main/java/chess/ChessGameApplication.java new file mode 100644 index 00000000000..0db3080604e --- /dev/null +++ b/src/main/java/chess/ChessGameApplication.java @@ -0,0 +1,17 @@ +package chess; + +import chess.controller.ControllerFactory; +import chess.controller.main.MainController; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ChessGameApplication { + public static void main(String[] args) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(() -> { + final MainController controller = ControllerFactory.mainController(); + controller.run(); + }); + executor.shutdown(); + } +} 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/Action.java b/src/main/java/chess/controller/Action.java new file mode 100644 index 00000000000..7524ed82df3 --- /dev/null +++ b/src/main/java/chess/controller/Action.java @@ -0,0 +1,11 @@ +package chess.controller; + +import java.util.List; + +@FunctionalInterface +public interface Action { + Action EMPTY = ignore -> { + }; + + void execute(final List commands); +} diff --git a/src/main/java/chess/controller/CommandMapper.java b/src/main/java/chess/controller/CommandMapper.java new file mode 100644 index 00000000000..c655e0b23ad --- /dev/null +++ b/src/main/java/chess/controller/CommandMapper.java @@ -0,0 +1,16 @@ +package chess.controller; + +import java.util.HashMap; +import java.util.Map; + +public class CommandMapper { + private final Map commandMapper = new HashMap<>(); + + public CommandMapper(final Map commandMapper) { + this.commandMapper.putAll(commandMapper); + } + + public V getValue(final K command) { + return commandMapper.get(command); + } +} diff --git a/src/main/java/chess/controller/ControllerFactory.java b/src/main/java/chess/controller/ControllerFactory.java new file mode 100644 index 00000000000..9b4b76a4d8c --- /dev/null +++ b/src/main/java/chess/controller/ControllerFactory.java @@ -0,0 +1,70 @@ +package chess.controller; + +import chess.controller.game.GameController; +import chess.controller.main.MainCommand; +import chess.controller.main.MainController; +import chess.controller.room.RoomController; +import chess.controller.user.UserController; +import chess.db.FixedConnectionPool; +import chess.db.JdbcTemplate; +import chess.repository.GameDao; +import chess.repository.GameJdbcDao; +import chess.repository.RoomDao; +import chess.repository.RoomJdbcDao; +import chess.repository.UserDao; +import chess.repository.UserJdbcDao; +import chess.service.GameService; +import chess.service.RoomService; +import chess.service.UserService; +import chess.view.input.InputView; +import chess.view.output.GameOutputView; +import chess.view.output.MainOutputView; +import chess.view.output.RoomOutputView; +import chess.view.output.UserOutputView; +import java.util.Map; +import java.util.Scanner; + +public class ControllerFactory { + private static final MainController INSTANCE; + private static final InputView INPUT_VIEW = new InputView(new Scanner(System.in)); + private static final JdbcTemplate JDBC_TEMPLATE = new JdbcTemplate(FixedConnectionPool.getInstance()); + + static { + final CommandMapper mainCommandMapper = new CommandMapper<>(Map.of( + MainCommand.USER, userController(), + MainCommand.ROOM, roomController(), + MainCommand.START, gameController(), + MainCommand.END, () -> { + } + )); + INSTANCE = new MainController(INPUT_VIEW, new MainOutputView(), mainCommandMapper); + } + + private static SubController userController() { + return new UserController(INPUT_VIEW, new UserOutputView(), new UserService(userDao())); + } + + private static UserDao userDao() { + return new UserJdbcDao(JDBC_TEMPLATE); + } + + private static SubController roomController() { + return new RoomController(INPUT_VIEW, new RoomOutputView(), new RoomService(roomDao())); + } + + private static RoomDao roomDao() { + return new RoomJdbcDao(JDBC_TEMPLATE); + } + + private static SubController gameController() { + return new GameController(INPUT_VIEW, new GameOutputView(), new GameService(gameDao())); + } + + private static GameDao gameDao() { + return new GameJdbcDao(JDBC_TEMPLATE); + } + + public static MainController mainController() { + return INSTANCE; + } +} diff --git a/src/main/java/chess/controller/SubController.java b/src/main/java/chess/controller/SubController.java new file mode 100644 index 00000000000..2b290fcf536 --- /dev/null +++ b/src/main/java/chess/controller/SubController.java @@ -0,0 +1,6 @@ +package chess.controller; + +@FunctionalInterface +public interface SubController { + void run(); +} diff --git a/src/main/java/chess/controller/game/GameCommand.java b/src/main/java/chess/controller/game/GameCommand.java new file mode 100644 index 00000000000..eb963a5c1d2 --- /dev/null +++ b/src/main/java/chess/controller/game/GameCommand.java @@ -0,0 +1,37 @@ +package chess.controller.game; + +import java.util.Arrays; +import java.util.List; + +public enum GameCommand { + MOVE(3), + STATUS(1), + END(1), + EMPTY(0), + ; + + private final int size; + + GameCommand(final int size) { + this.size = size; + } + + public static final int MOVE_SOURCE_INDEX = 1; + public static final int MOVE_TARGET_INDEX = 2; + private static final int COMMAND_INDEX = 0; + private static final String INVALID_COMMAND_MESSAGE = "올바른 명령어를 입력해주세요."; + + public static GameCommand from(final List commands) { + return Arrays.stream(values()) + .filter(command -> command != EMPTY) + .filter(command -> command.name().equalsIgnoreCase(commands.get(COMMAND_INDEX))) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(INVALID_COMMAND_MESSAGE)); + } + + public void validateCommandsSize(final List commands) { + if (size != commands.size()) { + throw new IllegalArgumentException(INVALID_COMMAND_MESSAGE); + } + } +} diff --git a/src/main/java/chess/controller/game/GameController.java b/src/main/java/chess/controller/game/GameController.java new file mode 100644 index 00000000000..1d059c8e792 --- /dev/null +++ b/src/main/java/chess/controller/game/GameController.java @@ -0,0 +1,93 @@ +package chess.controller.game; + +import static chess.controller.game.GameCommand.EMPTY; +import static chess.controller.game.GameCommand.END; +import static chess.controller.game.GameCommand.MOVE; +import static chess.controller.game.GameCommand.MOVE_SOURCE_INDEX; +import static chess.controller.game.GameCommand.MOVE_TARGET_INDEX; +import static chess.controller.game.GameCommand.STATUS; + +import chess.controller.Action; +import chess.controller.CommandMapper; +import chess.controller.SubController; +import chess.controller.session.RoomSession; +import chess.controller.session.UserSession; +import chess.dto.MoveDto; +import chess.service.GameService; +import chess.view.input.InputView; +import chess.view.output.GameOutputView; +import java.util.List; +import java.util.Map; + +public class GameController implements SubController { + private final InputView inputView; + private final GameOutputView outputView; + private final GameService gameService; + private final CommandMapper commandMapper; + + public GameController( + final InputView inputView, + final GameOutputView outputView, + final GameService gameService + ) { + this.inputView = inputView; + this.outputView = outputView; + this.gameService = gameService; + this.commandMapper = new CommandMapper<>(mappingCommand()); + } + + private Map mappingCommand() { + return Map.of( + MOVE, this::move, + STATUS, ignore -> status(), + END, Action.EMPTY + ); + } + + @Override + public void run() { + outputView.printGameStart(); + gameService.initialize(RoomSession.getId()); + outputView.printBoard(gameService.getResult(RoomSession.getId())); + GameCommand command = EMPTY; + while (command != END) { + command = play(); + command = checkGameOver(command); + } + outputView.printGameEnd(); + gameService.removeBoard(RoomSession.getId()); + } + + private GameCommand play() { + try { + outputView.printCommands(UserSession.getName(), RoomSession.getName()); + final List commands = inputView.readCommands(); + final GameCommand command = GameCommand.from(commands); + command.validateCommandsSize(commands); + final Action action = commandMapper.getValue(command); + action.execute(commands); + return command; + } catch (IllegalArgumentException | IllegalStateException e) { + outputView.printException(e.getMessage()); + return EMPTY; + } + } + + private void move(final List commands) { + final MoveDto moveDto = new MoveDto(commands.get(MOVE_SOURCE_INDEX), commands.get(MOVE_TARGET_INDEX)); + gameService.move(moveDto, RoomSession.getId()); + outputView.printBoard(gameService.getResult(RoomSession.getId())); + } + + private void status() { + outputView.printStatus(gameService.getResult(RoomSession.getId())); + } + + private GameCommand checkGameOver(final GameCommand command) { + if (gameService.isGameOver(RoomSession.getId())) { + outputView.printStatus(gameService.getResult(RoomSession.getId())); + return END; + } + return command; + } +} diff --git a/src/main/java/chess/controller/main/MainCommand.java b/src/main/java/chess/controller/main/MainCommand.java new file mode 100644 index 00000000000..ede08bcaf80 --- /dev/null +++ b/src/main/java/chess/controller/main/MainCommand.java @@ -0,0 +1,22 @@ +package chess.controller.main; + +import java.util.Arrays; + +public enum MainCommand { + EMPTY, + USER, + ROOM, + START, + END, + ; + + private static final String INVALID_COMMAND_MESSAGE = "올바른 명령어를 입력해주세요."; + + public static MainCommand from(final String inputCommand) { + return Arrays.stream(values()) + .filter(command -> command != EMPTY) + .filter(command -> command.name().equalsIgnoreCase(inputCommand)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(INVALID_COMMAND_MESSAGE)); + } +} 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..2c334a0937d --- /dev/null +++ b/src/main/java/chess/controller/main/MainController.java @@ -0,0 +1,50 @@ +package chess.controller.main; + +import static chess.controller.main.MainCommand.EMPTY; +import static chess.controller.main.MainCommand.END; + +import chess.controller.CommandMapper; +import chess.controller.SubController; +import chess.controller.session.RoomSession; +import chess.controller.session.UserSession; +import chess.view.input.InputView; +import chess.view.output.MainOutputView; + +public class MainController { + private final InputView inputView; + private final MainOutputView outputView; + private final CommandMapper commandMapper; + + public MainController( + final InputView inputView, + final MainOutputView outputView, + final CommandMapper commandMapper + ) { + this.inputView = inputView; + this.outputView = outputView; + this.commandMapper = commandMapper; + } + + public void run() { + MainCommand command = EMPTY; + while (command != END) { + command = readCommand(); + } + UserSession.remove(); + RoomSession.remove(); + } + + private MainCommand readCommand() { + try { + outputView.printCommands(UserSession.getName(), RoomSession.getName()); + final String command = inputView.readCommand(); + final MainCommand mainCommand = MainCommand.from(command); + final SubController controller = commandMapper.getValue(mainCommand); + controller.run(); + return mainCommand; + } catch (IllegalArgumentException | IllegalStateException e) { + outputView.printException(e.getMessage()); + return EMPTY; + } + } +} diff --git a/src/main/java/chess/controller/room/RoomCommand.java b/src/main/java/chess/controller/room/RoomCommand.java new file mode 100644 index 00000000000..14372890eb3 --- /dev/null +++ b/src/main/java/chess/controller/room/RoomCommand.java @@ -0,0 +1,38 @@ +package chess.controller.room; + +import java.util.Arrays; +import java.util.List; + +public enum RoomCommand { + HISTORY(1), + CREATE(2), + JOIN(2), + END(1), + EMPTY(0), + ; + + private final int size; + + RoomCommand(final int size) { + this.size = size; + } + + public static final int NAME_INDEX = 1; + public static final int ROOM_ID_INDEX = 1; + private static final int COMMAND_INDEX = 0; + private static final String INVALID_COMMAND_MESSAGE = "올바른 명령어를 입력해주세요."; + + public static RoomCommand from(final List commands) { + return Arrays.stream(values()) + .filter(command -> command != EMPTY) + .filter(command -> command.name().equalsIgnoreCase(commands.get(COMMAND_INDEX))) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(INVALID_COMMAND_MESSAGE)); + } + + public void validateCommandsSize(final List commands) { + if (size != commands.size()) { + throw new IllegalArgumentException(INVALID_COMMAND_MESSAGE); + } + } +} diff --git a/src/main/java/chess/controller/room/RoomController.java b/src/main/java/chess/controller/room/RoomController.java new file mode 100644 index 00000000000..7a441adb4ea --- /dev/null +++ b/src/main/java/chess/controller/room/RoomController.java @@ -0,0 +1,95 @@ +package chess.controller.room; + +import static chess.controller.room.RoomCommand.CREATE; +import static chess.controller.room.RoomCommand.EMPTY; +import static chess.controller.room.RoomCommand.END; +import static chess.controller.room.RoomCommand.HISTORY; +import static chess.controller.room.RoomCommand.JOIN; +import static chess.controller.room.RoomCommand.NAME_INDEX; +import static chess.controller.room.RoomCommand.ROOM_ID_INDEX; + +import chess.controller.Action; +import chess.controller.CommandMapper; +import chess.controller.SubController; +import chess.controller.session.RoomSession; +import chess.controller.session.UserSession; +import chess.domain.room.Room; +import chess.service.RoomService; +import chess.view.input.InputView; +import chess.view.output.RoomOutputView; +import java.util.List; +import java.util.Map; + +public class RoomController implements SubController { + private final InputView inputView; + private final RoomOutputView outputView; + private final RoomService roomService; + private final CommandMapper commandMapper; + + public RoomController( + final InputView inputView, + final RoomOutputView outputView, + final RoomService roomService + ) { + this.inputView = inputView; + this.outputView = outputView; + this.roomService = roomService; + this.commandMapper = new CommandMapper<>(mappingCommand()); + } + + private Map mappingCommand() { + return Map.of( + HISTORY, ignore -> history(), + CREATE, this::create, + JOIN, this::join, + END, Action.EMPTY + ); + } + + @Override + public void run() { + if (UserSession.get() == null) { + throw new IllegalArgumentException("로그인 후 게임 관리를 할 수 있습니다."); + } + RoomCommand command = EMPTY; + while (command != END) { + command = play(); + } + } + + private RoomCommand play() { + try { + outputView.printCommands(UserSession.getName(), RoomSession.getName()); + final List commands = inputView.readCommands(); + final RoomCommand command = RoomCommand.from(commands); + command.validateCommandsSize(commands); + final Action action = commandMapper.getValue(command); + action.execute(commands); + return command; + } catch (IllegalArgumentException | IllegalStateException e) { + outputView.printException(e.getMessage()); + return EMPTY; + } + } + + private void history() { + final List rooms = roomService.findAllByUserId(UserSession.getId()); + outputView.printRooms(rooms); + } + + private void create(final List commands) { + final String roomName = commands.get(NAME_INDEX); + roomService.save(roomName, UserSession.getId()); + outputView.printSaveSuccess(roomName); + } + + private void join(final List commands) { + try { + final int roomId = Integer.parseInt(commands.get(ROOM_ID_INDEX)); + final Room room = roomService.findById(roomId, UserSession.getId()); + RoomSession.add(room); + } catch (final NumberFormatException e) { + throw new IllegalArgumentException("올바른 값을 입력해주세요."); + } + } +} diff --git a/src/main/java/chess/controller/session/AnonymousKeyGenerator.java b/src/main/java/chess/controller/session/AnonymousKeyGenerator.java new file mode 100644 index 00000000000..2915dca4d61 --- /dev/null +++ b/src/main/java/chess/controller/session/AnonymousKeyGenerator.java @@ -0,0 +1,24 @@ +package chess.controller.session; + +import java.util.concurrent.ThreadLocalRandom; + +public class AnonymousKeyGenerator { + private static final ThreadLocalRandom random = ThreadLocalRandom.current(); + private static final ThreadLocal key = new ThreadLocal<>(); + private static final int RANDOM_MIN_VALUE = 10000; + + private AnonymousKeyGenerator() { + } + + public static int getKey() { + final Integer integer = key.get(); + if (integer == null) { + key.set(random.nextInt(RANDOM_MIN_VALUE, Integer.MAX_VALUE)); + } + return key.get(); + } + + public static void remove() { + key.remove(); + } +} diff --git a/src/main/java/chess/controller/session/RoomSession.java b/src/main/java/chess/controller/session/RoomSession.java new file mode 100644 index 00000000000..23461d4a251 --- /dev/null +++ b/src/main/java/chess/controller/session/RoomSession.java @@ -0,0 +1,35 @@ +package chess.controller.session; + +import chess.domain.room.Room; + +public class RoomSession { + private static final ThreadLocal session = new ThreadLocal<>(); + + private RoomSession() { + } + + public static void add(final Room room) { + session.set(room); + } + + public static int getId() { + final Room room = session.get(); + if (room == null) { + return AnonymousKeyGenerator.getKey(); + } + return room.getId(); + } + + public static String getName() { + final Room room = session.get(); + if (room == null) { + return "none"; + } + return room.getName(); + } + + public static void remove() { + session.remove(); + AnonymousKeyGenerator.remove(); + } +} diff --git a/src/main/java/chess/controller/session/UserSession.java b/src/main/java/chess/controller/session/UserSession.java new file mode 100644 index 00000000000..89e5479813a --- /dev/null +++ b/src/main/java/chess/controller/session/UserSession.java @@ -0,0 +1,39 @@ +package chess.controller.session; + +import chess.domain.user.User; + +public class UserSession { + private static final ThreadLocal session = new ThreadLocal<>(); + + private UserSession() { + } + + public static void add(final User user) { + session.set(user); + } + + public static User get() { + return session.get(); + } + + public static int getId() { + final User auth = session.get(); + if (auth == null) { + return AnonymousKeyGenerator.getKey(); + } + return session.get().getId(); + } + + public static String getName() { + final User auth = session.get(); + if (auth == null) { + return "anonymous"; + } + return auth.getName(); + } + + public static void remove() { + session.remove(); + AnonymousKeyGenerator.remove(); + } +} diff --git a/src/main/java/chess/controller/user/UserCommand.java b/src/main/java/chess/controller/user/UserCommand.java new file mode 100644 index 00000000000..d01bb10fd9a --- /dev/null +++ b/src/main/java/chess/controller/user/UserCommand.java @@ -0,0 +1,37 @@ +package chess.controller.user; + +import java.util.Arrays; +import java.util.List; + +public enum UserCommand { + REGISTER(2), + LOGIN(2), + LOGOUT(1), + END(1), + EMPTY(0), + ; + + private final int size; + + UserCommand(final int size) { + this.size = size; + } + + public static final int NAME_INDEX = 1; + private static final int COMMAND_INDEX = 0; + private static final String INVALID_COMMAND_MESSAGE = "올바른 명령어를 입력해주세요."; + + public static UserCommand from(final List commands) { + return Arrays.stream(values()) + .filter(command -> command != EMPTY) + .filter(command -> command.name().equalsIgnoreCase(commands.get(COMMAND_INDEX))) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(INVALID_COMMAND_MESSAGE)); + } + + public void validateCommandsSize(final List commands) { + if (size != commands.size()) { + throw new IllegalArgumentException(INVALID_COMMAND_MESSAGE); + } + } +} diff --git a/src/main/java/chess/controller/user/UserController.java b/src/main/java/chess/controller/user/UserController.java new file mode 100644 index 00000000000..7bee384d151 --- /dev/null +++ b/src/main/java/chess/controller/user/UserController.java @@ -0,0 +1,90 @@ +package chess.controller.user; + +import static chess.controller.user.UserCommand.EMPTY; +import static chess.controller.user.UserCommand.END; +import static chess.controller.user.UserCommand.LOGIN; +import static chess.controller.user.UserCommand.LOGOUT; +import static chess.controller.user.UserCommand.NAME_INDEX; +import static chess.controller.user.UserCommand.REGISTER; + +import chess.controller.Action; +import chess.controller.CommandMapper; +import chess.controller.SubController; +import chess.controller.session.RoomSession; +import chess.controller.session.UserSession; +import chess.domain.user.User; +import chess.service.UserService; +import chess.view.input.InputView; +import chess.view.output.UserOutputView; +import java.util.List; +import java.util.Map; + +public class UserController implements SubController { + private final InputView inputView; + private final UserOutputView outputView; + private final UserService userService; + private final CommandMapper commandMapper; + + public UserController( + final InputView inputView, + final UserOutputView outputView, + final UserService userService + ) { + this.inputView = inputView; + this.outputView = outputView; + this.userService = userService; + this.commandMapper = new CommandMapper<>(mappingCommand()); + } + + private Map mappingCommand() { + return Map.of( + REGISTER, this::register, + LOGIN, this::login, + LOGOUT, ignore -> logout(), + END, Action.EMPTY + ); + } + + @Override + public void run() { + UserCommand command = EMPTY; + while (command != END) { + command = play(); + } + } + + private UserCommand play() { + try { + outputView.printCommands(UserSession.getName(), RoomSession.getName()); + final List commands = inputView.readCommands(); + final UserCommand command = UserCommand.from(commands); + command.validateCommandsSize(commands); + final Action action = commandMapper.getValue(command); + action.execute(commands); + return command; + } catch (IllegalArgumentException | IllegalStateException e) { + outputView.printException(e.getMessage()); + return EMPTY; + } + } + + private void register(final List commands) { + final String name = commands.get(NAME_INDEX); + userService.save(name); + outputView.printRegisterSuccess(name); + } + + private void login(final List commands) { + if (UserSession.get() != null) { + throw new IllegalArgumentException("이미 로그인 된 상태입니다."); + } + final String name = commands.get(NAME_INDEX); + final User user = userService.findByName(name); + UserSession.add(user); + outputView.printLoginSuccess(user.getName()); + } + + private void logout() { + UserSession.remove(); + } +} diff --git a/src/main/java/chess/db/ConnectionGenerator.java b/src/main/java/chess/db/ConnectionGenerator.java new file mode 100644 index 00000000000..c876f6dab5c --- /dev/null +++ b/src/main/java/chess/db/ConnectionGenerator.java @@ -0,0 +1,26 @@ +package chess.db; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class ConnectionGenerator { + + private static final String SERVER = "localhost:13306"; + private static final String DATABASE = "chess"; + private static final String OPTION = "?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true"; + private static final String USERNAME = "root"; + private static final String PASSWORD = "root"; + private static final String URL = "jdbc:mysql://" + SERVER + "/" + DATABASE + OPTION; + + private ConnectionGenerator() { + } + + public static Connection getConnection() { + try { + return DriverManager.getConnection(URL, USERNAME, PASSWORD); + } catch (SQLException e) { + throw new DatabaseConnectionFailException(); + } + } +} diff --git a/src/main/java/chess/db/ConnectionPool.java b/src/main/java/chess/db/ConnectionPool.java new file mode 100644 index 00000000000..b9a46526b67 --- /dev/null +++ b/src/main/java/chess/db/ConnectionPool.java @@ -0,0 +1,8 @@ +package chess.db; + +import java.sql.Connection; + +public interface ConnectionPool { + + Connection getConnection(); +} diff --git a/src/main/java/chess/db/DatabaseConnectionFailException.java b/src/main/java/chess/db/DatabaseConnectionFailException.java new file mode 100644 index 00000000000..a29ea2e927f --- /dev/null +++ b/src/main/java/chess/db/DatabaseConnectionFailException.java @@ -0,0 +1,8 @@ +package chess.db; + +public class DatabaseConnectionFailException extends RuntimeException { + + public DatabaseConnectionFailException() { + super("데이터베이스 연결에 실패했습니다."); + } +} diff --git a/src/main/java/chess/db/FixedConnectionPool.java b/src/main/java/chess/db/FixedConnectionPool.java new file mode 100644 index 00000000000..561ebcbd3e9 --- /dev/null +++ b/src/main/java/chess/db/FixedConnectionPool.java @@ -0,0 +1,38 @@ +package chess.db; + +import static java.util.stream.Collectors.toList; + +import java.sql.Connection; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +public class FixedConnectionPool implements ConnectionPool { + + private static final FixedConnectionPool INSTANCE = new FixedConnectionPool(); + private static final int MAX_CONNECTION_SIZE = 5; + + private final AtomicInteger index; + private final List connections; + + private FixedConnectionPool() { + this(MAX_CONNECTION_SIZE); + } + + private FixedConnectionPool(int connectionCount) { + index = new AtomicInteger(); + connections = Stream.generate(ConnectionGenerator::getConnection) + .limit(connectionCount) + .collect(toList()); + } + + public static FixedConnectionPool getInstance() { + return INSTANCE; + } + + @Override + public Connection getConnection() { + int currentIndex = index.getAndIncrement(); + return connections.get(currentIndex % connections.size()); + } +} diff --git a/src/main/java/chess/db/JdbcTemplate.java b/src/main/java/chess/db/JdbcTemplate.java new file mode 100644 index 00000000000..3c603013138 --- /dev/null +++ b/src/main/java/chess/db/JdbcTemplate.java @@ -0,0 +1,67 @@ +package chess.db; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class JdbcTemplate { + + private final ConnectionPool connectionPool; + + public JdbcTemplate(final ConnectionPool connectionPool) { + this.connectionPool = connectionPool; + } + + public void executeUpdate(final String query, final Object... parameters) { + final Connection connection = connectionPool.getConnection(); + try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + for (int i = 1; i <= parameters.length; i++) { + preparedStatement.setObject(i, parameters[i - 1]); + } + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + public List query(final String query, final RowMapper rowMapper, final Object... parameters) { + final Connection connection = connectionPool.getConnection(); + try (final PreparedStatement preparedStatement = connection.prepareStatement(query); + final ResultSet resultSet = getResultSet(preparedStatement, parameters)) { + final List result = new ArrayList<>(); + while (resultSet.next()) { + result.add(rowMapper.mapRow(resultSet)); + } + return result; + } catch (SQLException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + private ResultSet getResultSet( + final PreparedStatement preparedStatement, + final Object[] parameters) throws SQLException { + for (int i = 1; i <= parameters.length; i++) { + preparedStatement.setObject(i, parameters[i - 1]); + } + return preparedStatement.executeQuery(); + } + + public Optional queryForSingleResult(final String query, final RowMapper rowMapper, + final Object... parameters) { + final Connection connection = connectionPool.getConnection(); + try (final PreparedStatement preparedStatement = connection.prepareStatement(query); + final ResultSet resultSet = getResultSet(preparedStatement, parameters)) { + if (resultSet.next()) { + return Optional.of(rowMapper.mapRow(resultSet)); + } + return Optional.empty(); + } catch (SQLException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/src/main/java/chess/db/RowMapper.java b/src/main/java/chess/db/RowMapper.java new file mode 100644 index 00000000000..738026ce26e --- /dev/null +++ b/src/main/java/chess/db/RowMapper.java @@ -0,0 +1,9 @@ +package chess.db; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@FunctionalInterface +public interface RowMapper { + T mapRow(final ResultSet resultSet) throws SQLException; +} diff --git a/src/main/java/chess/domain/board/Board.java b/src/main/java/chess/domain/board/Board.java new file mode 100644 index 00000000000..c3cbc1aaa1c --- /dev/null +++ b/src/main/java/chess/domain/board/Board.java @@ -0,0 +1,77 @@ +package chess.domain.board; + +import chess.domain.piece.Color; +import chess.domain.piece.Empty; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceType; +import chess.domain.position.Position; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class Board { + private static final int VALID_KING_COUNT = 2; + + private final Map board; + private Color turn; + + public Board() { + this(new HashMap<>(), Color.WHITE); + } + + private Board(final Map board, final Color turn) { + this.board = board; + this.turn = turn; + } + + public void initialize() { + board.putAll(BoardGenerator.generate()); + } + + public void move(final Position source, final Position target) { + if (isGameOver()) { + throw new IllegalStateException("게임을 진행할 수 있는 상태가 아닙니다."); + } + final Piece piece = board.get(source); + + validate(source, target, piece); + movePiece(source, target, piece); + turn = turn.nextTurn(); + } + + public boolean isGameOver() { + return VALID_KING_COUNT != board.values().stream() + .filter(piece -> piece.isSameType(PieceType.KING)) + .count(); + } + + private void validate(final Position source, final Position target, final Piece piece) { + if (piece.isNotSameColor(turn)) { + throw new IllegalArgumentException("상대방의 기물을 움직일 수 없습니다."); + } + if (piece.isNotMovable(source, target, board.get(target))) { + throw new IllegalArgumentException("올바르지 않은 이동 명령어 입니다."); + } + if (isPieceExistsBetweenPosition(source, target)) { + throw new IllegalArgumentException("이동 경로에 다른 기물이 있을 수 없습니다."); + } + } + + private boolean isPieceExistsBetweenPosition(final Position source, final Position target) { + return source.between(target).stream() + .anyMatch(this::isPieceExists); + } + + private boolean isPieceExists(final Position position) { + return !board.get(position).equals(Empty.instance()); + } + + private void movePiece(final Position source, final Position target, final Piece piece) { + board.put(target, piece); + board.put(source, Empty.instance()); + } + + public final GameResult getResult() { + return new GameResult(Collections.unmodifiableMap(board)); + } +} diff --git a/src/main/java/chess/domain/board/BoardGenerator.java b/src/main/java/chess/domain/board/BoardGenerator.java new file mode 100644 index 00000000000..2d639516ba6 --- /dev/null +++ b/src/main/java/chess/domain/board/BoardGenerator.java @@ -0,0 +1,64 @@ +package chess.domain.board; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; + +import chess.domain.piece.Bishop; +import chess.domain.piece.Color; +import chess.domain.piece.Empty; +import chess.domain.piece.King; +import chess.domain.piece.Knight; +import chess.domain.piece.Pawn; +import chess.domain.piece.Piece; +import chess.domain.piece.Queen; +import chess.domain.piece.Rook; +import chess.domain.position.File; +import chess.domain.position.Position; +import chess.domain.position.Rank; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.IntStream; + +public class BoardGenerator { + private static final Map CACHE = new HashMap<>(); + + static { + CACHE.putAll(initializePiece(Color.WHITE, Rank.ONE)); + CACHE.putAll(initializePawn(Color.WHITE, Rank.TWO)); + CACHE.putAll(initializeEmptyPiece(List.of(Rank.THREE, Rank.FOUR, Rank.FIVE, Rank.SIX))); + CACHE.putAll(initializePawn(Color.BLACK, Rank.SEVEN)); + CACHE.putAll(initializePiece(Color.BLACK, Rank.EIGHT)); + } + + private static Map initializePiece(final Color color, final Rank rank) { + final List pieces = List.of( + Rook.from(color), Knight.from(color), Bishop.from(color), Queen.from(color), + King.from(color), Bishop.from(color), Knight.from(color), Rook.from(color) + ); + final List files = Arrays.stream(File.values()).collect(toList()); + + return IntStream.range(0, pieces.size()) + .boxed() + .collect(toMap(index -> Position.of(files.get(index), rank), pieces::get)); + } + + private static Map initializePawn(final Color color, final Rank rank) { + return Arrays.stream(File.values()) + .map(file -> Position.of(file, rank)) + .collect(toMap(Function.identity(), ignore -> Pawn.from(color))); + } + + private static Map initializeEmptyPiece(final List ranks) { + return ranks.stream() + .flatMap(rank -> Arrays.stream(File.values()).map(file -> Position.of(file, rank))) + .collect(toMap(Function.identity(), ignore -> Empty.instance())); + } + + public static Map generate() { + return Collections.unmodifiableMap(CACHE); + } +} diff --git a/src/main/java/chess/domain/board/GameResult.java b/src/main/java/chess/domain/board/GameResult.java new file mode 100644 index 00000000000..b2c6a4957ff --- /dev/null +++ b/src/main/java/chess/domain/board/GameResult.java @@ -0,0 +1,85 @@ +package chess.domain.board; + +import static chess.domain.piece.Color.BLACK; +import static chess.domain.piece.Color.EMPTY; +import static chess.domain.piece.Color.WHITE; +import static chess.domain.piece.PieceType.PAWN; + +import chess.domain.piece.Color; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceType; +import chess.domain.position.File; +import chess.domain.position.Position; +import chess.domain.position.Rank; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public class GameResult { + private static final int MULTIPLE_PAWN_COUNT = 2; + private static final double MULTIPLE_PAWN_SCORE = 0.5; + + private final Map board; + + public GameResult(final Map board) { + this.board = board; + } + + public final double score(final Color color) { + return pieceScore(color) - multiplePawnScore(color); + } + + private double pieceScore(final Color color) { + return board.values().stream() + .filter(piece -> piece.isSameColor(color)) + .mapToDouble(Piece::score) + .sum(); + } + + private double multiplePawnScore(final Color color) { + final Map pawnCount = Arrays.stream(Rank.values()) + .flatMap(file -> Arrays.stream(File.values()).map(rank -> Position.of(rank, file))) + .filter(position -> board.get(position).isSameColor(color)) + .filter(position -> board.get(position).isSameType(PAWN)) + .collect(Collectors.groupingBy(Position::file, Collectors.counting())); + + return pawnCount.values().stream() + .filter(value -> value >= MULTIPLE_PAWN_COUNT) + .mapToDouble(value -> value * MULTIPLE_PAWN_SCORE) + .sum(); + } + + public final Color winner() { + if (isKingDead(WHITE) || isKingDead(BLACK)) { + return calculateByKing(); + } + return calculateByScore(); + } + + private boolean isKingDead(final Color color) { + return board.values().stream() + .filter(piece -> piece.isSameType(PieceType.KING)) + .noneMatch(piece -> piece.isSameColor(color)); + } + + private Color calculateByKing() { + if (isKingDead(WHITE)) { + return BLACK; + } + return WHITE; + } + + private Color calculateByScore() { + if (score(WHITE) == score(BLACK)) { + return EMPTY; + } + if (score(WHITE) > score(BLACK)) { + return WHITE; + } + return BLACK; + } + + public Map getBoard() { + return board; + } +} diff --git a/src/main/java/chess/domain/piece/Bishop.java b/src/main/java/chess/domain/piece/Bishop.java new file mode 100644 index 00000000000..2ee40980abb --- /dev/null +++ b/src/main/java/chess/domain/piece/Bishop.java @@ -0,0 +1,34 @@ +package chess.domain.piece; + +import static chess.domain.piece.PieceType.BISHOP; + +import chess.domain.position.Position; + +public class Bishop extends Piece { + private static final Bishop WHITE = new Bishop(Color.WHITE); + private static final Bishop BLACK = new Bishop(Color.BLACK); + + private Bishop(final Color color) { + super(color, BISHOP); + } + + public static Bishop from(final Color color) { + if (color == Color.WHITE) { + return WHITE; + } + return BLACK; + } + + @Override + protected boolean isValidMove(final Position start, final Position end) { + final int fileGap = Math.abs(start.calculateFileGap(end)); + final int rankGap = Math.abs(start.calculateRankGap(end)); + + return fileGap == rankGap; + } + + @Override + protected boolean isValidTarget(final Piece target) { + return color() != target.color(); + } +} 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..12d51732a04 --- /dev/null +++ b/src/main/java/chess/domain/piece/Color.java @@ -0,0 +1,22 @@ +package chess.domain.piece; + +public enum Color { + WHITE, + BLACK, + EMPTY, + ; + + public boolean isOpponent(final Color color) { + if (color == EMPTY) { + return false; + } + return this != color; + } + + public Color nextTurn() { + if (this == WHITE) { + return BLACK; + } + return WHITE; + } +} diff --git a/src/main/java/chess/domain/piece/Empty.java b/src/main/java/chess/domain/piece/Empty.java new file mode 100644 index 00000000000..c30e98384f3 --- /dev/null +++ b/src/main/java/chess/domain/piece/Empty.java @@ -0,0 +1,25 @@ +package chess.domain.piece; + +import chess.domain.position.Position; + +public class Empty extends Piece { + private static final Empty EMPTY = new Empty(); + + private Empty() { + super(Color.EMPTY, PieceType.EMPTY); + } + + public static Empty instance() { + return EMPTY; + } + + @Override + protected boolean isValidMove(final Position start, final Position end) { + return false; + } + + @Override + protected boolean isValidTarget(final Piece target) { + return false; + } +} diff --git a/src/main/java/chess/domain/piece/King.java b/src/main/java/chess/domain/piece/King.java new file mode 100644 index 00000000000..467f3d23821 --- /dev/null +++ b/src/main/java/chess/domain/piece/King.java @@ -0,0 +1,45 @@ +package chess.domain.piece; + +import static chess.domain.piece.PieceType.KING; + +import chess.domain.position.Position; + +public class King extends Piece { + private static final King WHITE = new King(Color.WHITE); + private static final King BLACK = new King(Color.BLACK); + private static final int GAP_LOWER_BOUND = 0; + private static final int GAP_UPPER_BOUND = 1; + + private King(final Color color) { + super(color, KING); + } + + public static King from(final Color color) { + if (color == Color.WHITE) { + return WHITE; + } + return BLACK; + } + + @Override + protected boolean isValidMove(final Position start, final Position end) { + final int fileGap = Math.abs(start.calculateFileGap(end)); + final int rankGap = Math.abs(start.calculateRankGap(end)); + + return canMoveStraight(fileGap, rankGap) || canMoveDiagonal(fileGap, rankGap); + } + + private boolean canMoveStraight(final int fileGap, final int rankGap) { + return (fileGap == GAP_LOWER_BOUND && rankGap == GAP_UPPER_BOUND) || + (fileGap == GAP_UPPER_BOUND && rankGap == GAP_LOWER_BOUND); + } + + private boolean canMoveDiagonal(final int fileGap, final int rankGap) { + return fileGap == GAP_UPPER_BOUND && rankGap == GAP_UPPER_BOUND; + } + + @Override + protected boolean isValidTarget(final Piece target) { + return color() != target.color(); + } +} diff --git a/src/main/java/chess/domain/piece/Knight.java b/src/main/java/chess/domain/piece/Knight.java new file mode 100644 index 00000000000..960aaa7a1bf --- /dev/null +++ b/src/main/java/chess/domain/piece/Knight.java @@ -0,0 +1,40 @@ +package chess.domain.piece; + +import static chess.domain.piece.PieceType.KNIGHT; + +import chess.domain.position.Position; + +public class Knight extends Piece { + private static final Knight WHITE = new Knight(Color.WHITE); + private static final Knight BLACK = new Knight(Color.BLACK); + private static final int GAP_LOWER_BOUND = 1; + private static final int GAP_UPPER_BOUND = 2; + + private Knight(final Color color) { + super(color, KNIGHT); + } + + public static Knight from(final Color color) { + if (color == Color.WHITE) { + return WHITE; + } + return BLACK; + } + + @Override + protected boolean isValidMove(final Position start, final Position end) { + final int fileGap = Math.abs(start.calculateFileGap(end)); + final int rankGap = Math.abs(start.calculateRankGap(end)); + + return canMove(fileGap, rankGap) || canMove(rankGap, fileGap); + } + + private boolean canMove(final int first, final int second) { + return first == GAP_LOWER_BOUND && second == GAP_UPPER_BOUND; + } + + @Override + protected boolean isValidTarget(final Piece target) { + return color() != target.color(); + } +} diff --git a/src/main/java/chess/domain/piece/Pawn.java b/src/main/java/chess/domain/piece/Pawn.java new file mode 100644 index 00000000000..722d4063726 --- /dev/null +++ b/src/main/java/chess/domain/piece/Pawn.java @@ -0,0 +1,62 @@ +package chess.domain.piece; + +import static chess.domain.piece.PieceType.PAWN; + +import chess.domain.position.Position; +import chess.domain.position.Rank; +import java.util.Map; + +public class Pawn extends Piece { + private static final Pawn WHITE = new Pawn(Color.WHITE); + private static final Pawn BLACK = new Pawn(Color.BLACK); + private static final Map GAP_LOWER_BOUND = Map.of(Color.WHITE, -1, Color.BLACK, 1); + private static final Map GAP_UPPER_BOUND = Map.of(Color.WHITE, -2, Color.BLACK, 2); + private static final Map INITIAL_RANK = Map.of(Color.WHITE, Rank.TWO, Color.BLACK, Rank.SEVEN); + private static final int VALID_STRAIGHT_GAP = 0; + private static final int VALID_DIAGONAL_GAP = 1; + + private Pawn(final Color color) { + super(color, PAWN); + } + + public static Pawn from(final Color color) { + if (color == Color.WHITE) { + return WHITE; + } + return BLACK; + } + + @Override + public boolean isMovable(final Position start, final Position end, final Piece target) { + return super.isMovable(start, end, target) || canMoveDiagonal(start, end, target); + } + + private boolean canMoveDiagonal(final Position start, final Position end, final Piece target) { + final int rankGap = start.calculateRankGap(end); + final int fileGap = Math.abs(start.calculateFileGap(end)); + final boolean isOpponent = color().isOpponent(target.color()); + return rankGap == GAP_LOWER_BOUND.get(color()) && fileGap == VALID_DIAGONAL_GAP && isOpponent; + } + + @Override + protected boolean isValidMove(final Position start, final Position end) { + if (start.calculateFileGap(end) != VALID_STRAIGHT_GAP) { + return false; + } + final int rankGap = start.calculateRankGap(end); + return canMoveStraight(color(), rankGap) || canMoveStraightDoubleStep(start, color(), rankGap); + } + + private boolean canMoveStraight(final Color color, final int rankGap) { + return rankGap == GAP_LOWER_BOUND.get(color); + } + + private boolean canMoveStraightDoubleStep(final Position start, final Color color, final int rankGap) { + return rankGap == GAP_UPPER_BOUND.get(color) && start.isSameRank(INITIAL_RANK.get(color)); + } + + @Override + protected boolean isValidTarget(final Piece target) { + return target.color() == Color.EMPTY; + } +} 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..90d17d55a12 --- /dev/null +++ b/src/main/java/chess/domain/piece/Piece.java @@ -0,0 +1,49 @@ +package chess.domain.piece; + +import chess.domain.position.Position; + +public abstract class Piece { + private final Color color; + private final PieceType type; + + protected Piece(final Color color, final PieceType type) { + this.color = color; + this.type = type; + } + + public boolean isNotMovable(Position sourcePosition, Position targetPosition, Piece target) { + return !isMovable(sourcePosition, targetPosition, target); + } + + public boolean isMovable(final Position start, final Position end, final Piece target) { + return isValidMove(start, end) && isValidTarget(target); + } + + protected abstract boolean isValidMove(final Position start, final Position end); + + protected abstract boolean isValidTarget(final Piece target); + + public final boolean isNotSameColor(final Color color) { + return this.color != color; + } + + public final boolean isSameColor(final Color color) { + return this.color == color; + } + + public final boolean isSameType(final PieceType type) { + return this.type == type; + } + + public PieceType type() { + return type; + } + + public Color color() { + return color; + } + + public double score() { + return type.score(); + } +} 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..3e284e5c2c6 --- /dev/null +++ b/src/main/java/chess/domain/piece/PieceType.java @@ -0,0 +1,22 @@ +package chess.domain.piece; + +public enum PieceType { + PAWN(1), + KNIGHT(2.5), + BISHOP(3), + ROOK(5), + QUEEN(9), + KING(0), + EMPTY(0), + ; + + private final double score; + + PieceType(final double score) { + this.score = score; + } + + public double score() { + return score; + } +} diff --git a/src/main/java/chess/domain/piece/Queen.java b/src/main/java/chess/domain/piece/Queen.java new file mode 100644 index 00000000000..98598719ddd --- /dev/null +++ b/src/main/java/chess/domain/piece/Queen.java @@ -0,0 +1,43 @@ +package chess.domain.piece; + +import static chess.domain.piece.PieceType.QUEEN; + +import chess.domain.position.Position; + +public class Queen extends Piece { + private static final Queen WHITE = new Queen(Color.WHITE); + private static final Queen BLACK = new Queen(Color.BLACK); + private static final int VALID_GAP = 0; + + private Queen(final Color color) { + super(color, QUEEN); + } + + public static Queen from(final Color color) { + if (color == Color.WHITE) { + return WHITE; + } + return BLACK; + } + + @Override + protected boolean isValidMove(final Position start, final Position end) { + final int fileGap = Math.abs(start.calculateFileGap(end)); + final int rankGap = Math.abs(start.calculateRankGap(end)); + + return canMoveStraight(fileGap, rankGap) || canMoveDiagonal(fileGap, rankGap); + } + + private boolean canMoveStraight(final int fileGap, final int rankGap) { + return fileGap == VALID_GAP || rankGap == VALID_GAP; + } + + private boolean canMoveDiagonal(final int fileGap, final int rankGap) { + return fileGap == rankGap; + } + + @Override + protected boolean isValidTarget(final Piece target) { + return color() != target.color(); + } +} diff --git a/src/main/java/chess/domain/piece/Rook.java b/src/main/java/chess/domain/piece/Rook.java new file mode 100644 index 00000000000..f0c602ddb39 --- /dev/null +++ b/src/main/java/chess/domain/piece/Rook.java @@ -0,0 +1,33 @@ +package chess.domain.piece; + +import chess.domain.position.Position; + +public class Rook extends Piece { + private static final Rook WHITE = new Rook(Color.WHITE); + private static final Rook BLACK = new Rook(Color.BLACK); + private static final int VALID_GAP = 0; + + private Rook(final Color color) { + super(color, PieceType.ROOK); + } + + public static Rook from(final Color color) { + if (color == Color.WHITE) { + return WHITE; + } + return BLACK; + } + + @Override + protected boolean isValidMove(final Position start, final Position end) { + final int fileGap = start.calculateFileGap(end); + final int rankGap = start.calculateRankGap(end); + + return fileGap == VALID_GAP || rankGap == VALID_GAP; + } + + @Override + protected boolean isValidTarget(final Piece target) { + return color() != target.color(); + } +} diff --git a/src/main/java/chess/domain/position/File.java b/src/main/java/chess/domain/position/File.java new file mode 100644 index 00000000000..a9ca03c7746 --- /dev/null +++ b/src/main/java/chess/domain/position/File.java @@ -0,0 +1,58 @@ +package chess.domain.position; + +import static java.lang.Integer.max; +import static java.lang.Integer.min; +import static java.util.stream.Collectors.toList; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +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 START_EXCLUSIVE = 1; + + private final String symbol; + private final int position; + + File(final String symbol, final int position) { + this.symbol = symbol; + this.position = position; + } + + private static File from(final int position) { + return Arrays.stream(values()) + .filter(value -> value.position == position) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("위치 값은 1 ~ 8 사이의 값이어야 합니다.")); + } + + public List between(final File file) { + final List result = IntStream.range(min(position, file.position), max(position, file.position)) + .skip(START_EXCLUSIVE) + .mapToObj(File::from) + .collect(toList()); + if (position > file.position) { + Collections.reverse(result); + } + return result; + } + + public int calculateGap(final File target) { + return position - target.position; + } + + public String symbol() { + return symbol; + } +} diff --git a/src/main/java/chess/domain/position/Position.java b/src/main/java/chess/domain/position/Position.java new file mode 100644 index 00000000000..3e22e4eb309 --- /dev/null +++ b/src/main/java/chess/domain/position/Position.java @@ -0,0 +1,103 @@ +package chess.domain.position; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +public class Position { + private static final int STRAIGHT_GAP = 0; + private static final Map CACHE; + + static { + CACHE = Arrays.stream(File.values()) + .flatMap(file -> Arrays.stream(Rank.values()) + .map(rank -> Map.entry(file.symbol() + rank.symbol(), new Position(file, rank)))) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private final File file; + private final Rank rank; + + private Position(final File file, final Rank rank) { + this.file = file; + this.rank = rank; + } + + public static Position of(final File file, final Rank rank) { + return CACHE.get(file.symbol() + rank.symbol()); + } + + public static Position from(final String position) { + if (!CACHE.containsKey(position.toUpperCase())) { + throw new IllegalArgumentException("잘못된 위치값입니다."); + } + return CACHE.get(position.toUpperCase()); + } + + public int calculateFileGap(final Position target) { + return file.calculateGap(target.file); + } + + public int calculateRankGap(final Position target) { + return rank.calculateGap(target.rank); + } + + public List between(final Position other) { + final List ranks = rank.between(other.rank); + final List files = file.between(other.file); + + if (calculateFileGap(other) == STRAIGHT_GAP || calculateRankGap(other) == STRAIGHT_GAP) { + return betweenStraight(ranks, files); + } + if (ranks.size() != files.size()) { + return Collections.emptyList(); + } + return betweenDiagonal(ranks, files); + } + + private List betweenStraight(final List ranks, final List files) { + if (files.isEmpty()) { + return betweenRankStraight(ranks); + } + return betweenFileStraight(files); + } + + private List betweenRankStraight(final List ranks) { + return ranks.stream() + .map(rank -> Position.of(file, rank)) + .collect(toList()); + } + + private List betweenFileStraight(final List files) { + return files.stream() + .map(file -> Position.of(file, rank)) + .collect(toList()); + } + + private List betweenDiagonal(final List ranks, final List files) { + return IntStream.range(0, ranks.size()) + .mapToObj(index -> Position.of(files.get(index), ranks.get(index))) + .collect(toList()); + } + + public boolean isSameRank(final Rank rank) { + return this.rank == rank; + } + + @Override + public String toString() { + return "Position{" + + "file=" + file + + ", rank=" + rank + + '}'; + } + + public File file() { + return file; + } +} diff --git a/src/main/java/chess/domain/position/Rank.java b/src/main/java/chess/domain/position/Rank.java new file mode 100644 index 00000000000..3989b45f569 --- /dev/null +++ b/src/main/java/chess/domain/position/Rank.java @@ -0,0 +1,58 @@ +package chess.domain.position; + +import static java.lang.Integer.max; +import static java.lang.Integer.min; +import static java.util.stream.Collectors.toList; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +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 START_EXCLUSIVE = 1; + + private final String symbol; + private final int position; + + Rank(final String symbol, final int position) { + this.symbol = symbol; + this.position = position; + } + + private static Rank from(final int position) { + return Arrays.stream(values()) + .filter(value -> value.position == position) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("위치 값은 1 ~ 8 사이의 값이어야 합니다.")); + } + + public List between(final Rank rank) { + final List result = IntStream.range(min(position, rank.position), max(position, rank.position)) + .skip(START_EXCLUSIVE) + .mapToObj(Rank::from) + .collect(toList()); + if (position > rank.position) { + Collections.reverse(result); + } + return result; + } + + public int calculateGap(final Rank target) { + return position - target.position; + } + + public String symbol() { + return symbol; + } +} diff --git a/src/main/java/chess/domain/room/Room.java b/src/main/java/chess/domain/room/Room.java new file mode 100644 index 00000000000..3b5cd506c64 --- /dev/null +++ b/src/main/java/chess/domain/room/Room.java @@ -0,0 +1,29 @@ +package chess.domain.room; + +public class Room { + private final int id; + private final String name; + private final int userId; + + public Room(final int id, final String name, final int userId) { + this.id = id; + this.name = name; + this.userId = userId; + } + + public boolean isNotCreatedBy(final int id) { + return userId != id; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public int getUserId() { + return userId; + } +} diff --git a/src/main/java/chess/domain/user/User.java b/src/main/java/chess/domain/user/User.java new file mode 100644 index 00000000000..8586de3c578 --- /dev/null +++ b/src/main/java/chess/domain/user/User.java @@ -0,0 +1,19 @@ +package chess.domain.user; + +public class User { + private final int id; + private final String name; + + public User(final int id, final String name) { + this.id = id; + this.name = name; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/chess/dto/MoveDto.java b/src/main/java/chess/dto/MoveDto.java new file mode 100644 index 00000000000..1ff37074c2c --- /dev/null +++ b/src/main/java/chess/dto/MoveDto.java @@ -0,0 +1,19 @@ +package chess.dto; + +public class MoveDto { + private final String source; + private final String target; + + public MoveDto(final String source, final String target) { + this.source = source; + this.target = target; + } + + public String getSource() { + return source; + } + + public String getTarget() { + return target; + } +} diff --git a/src/main/java/chess/repository/GameDao.java b/src/main/java/chess/repository/GameDao.java new file mode 100644 index 00000000000..7e96083e09d --- /dev/null +++ b/src/main/java/chess/repository/GameDao.java @@ -0,0 +1,10 @@ +package chess.repository; + +import chess.dto.MoveDto; +import java.util.List; + +public interface GameDao { + void save(final MoveDto moveDto, final int roomId); + + List findAllByRoomId(final int roomId); +} diff --git a/src/main/java/chess/repository/GameJdbcDao.java b/src/main/java/chess/repository/GameJdbcDao.java new file mode 100644 index 00000000000..5d47778bdc5 --- /dev/null +++ b/src/main/java/chess/repository/GameJdbcDao.java @@ -0,0 +1,34 @@ +package chess.repository; + +import chess.db.JdbcTemplate; +import chess.db.RowMapper; +import chess.dto.MoveDto; +import java.util.List; + +public class GameJdbcDao implements GameDao { + private final RowMapper rowMapper = resultSet -> { + final String source = resultSet.getString("source"); + final String target = resultSet.getString("target"); + return new MoveDto(source, target); + }; + private final JdbcTemplate jdbcTemplate; + + public GameJdbcDao(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public void save(final MoveDto moveDto, final int roomId) { + jdbcTemplate.executeUpdate( + "INSERT INTO move (source, target, room_id) VALUES (?, ?, ?)", + moveDto.getSource(), + moveDto.getTarget(), + roomId + ); + } + + @Override + public List findAllByRoomId(final int roomId) { + return jdbcTemplate.query("SELECT * FROM move where room_id = ?", rowMapper, roomId); + } +} diff --git a/src/main/java/chess/repository/RoomDao.java b/src/main/java/chess/repository/RoomDao.java new file mode 100644 index 00000000000..d44f5584ce5 --- /dev/null +++ b/src/main/java/chess/repository/RoomDao.java @@ -0,0 +1,13 @@ +package chess.repository; + +import chess.domain.room.Room; +import java.util.List; +import java.util.Optional; + +public interface RoomDao { + void save(final String roomName, final int userId); + + List findAllByUserId(final int userId); + + Optional findById(final int roomId); +} diff --git a/src/main/java/chess/repository/RoomJdbcDao.java b/src/main/java/chess/repository/RoomJdbcDao.java new file mode 100644 index 00000000000..5778a0495ba --- /dev/null +++ b/src/main/java/chess/repository/RoomJdbcDao.java @@ -0,0 +1,40 @@ +package chess.repository; + +import chess.db.JdbcTemplate; +import chess.db.RowMapper; +import chess.domain.room.Room; +import java.util.List; +import java.util.Optional; + +public class RoomJdbcDao implements RoomDao { + private final RowMapper rowMapper = resultSet -> { + final int id = resultSet.getInt("id"); + final String name = resultSet.getString("name"); + final int findUserId = resultSet.getInt("user_id"); + return new Room(id, name, findUserId); + }; + private final JdbcTemplate jdbcTemplate; + + public RoomJdbcDao(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public void save(final String roomName, final int userId) { + jdbcTemplate.executeUpdate( + "INSERT INTO room (name, user_id) values (?, ?)", + roomName, + userId + ); + } + + @Override + public List findAllByUserId(final int userId) { + return jdbcTemplate.query("select * from room where user_id = ?", rowMapper, userId); + } + + @Override + public Optional findById(final int roomId) { + return jdbcTemplate.queryForSingleResult("select * from room where id = ?", rowMapper, roomId); + } +} diff --git a/src/main/java/chess/repository/UserDao.java b/src/main/java/chess/repository/UserDao.java new file mode 100644 index 00000000000..a5782c4ff2b --- /dev/null +++ b/src/main/java/chess/repository/UserDao.java @@ -0,0 +1,11 @@ +package chess.repository; + +import chess.domain.user.User; +import java.util.Optional; + +public interface UserDao { + + void save(final String name); + + Optional findByName(final String name); +} diff --git a/src/main/java/chess/repository/UserJdbcDao.java b/src/main/java/chess/repository/UserJdbcDao.java new file mode 100644 index 00000000000..30cffe4698c --- /dev/null +++ b/src/main/java/chess/repository/UserJdbcDao.java @@ -0,0 +1,29 @@ +package chess.repository; + +import chess.db.JdbcTemplate; +import chess.db.RowMapper; +import chess.domain.user.User; +import java.util.Optional; + +public class UserJdbcDao implements UserDao { + private final RowMapper rowMapper = resultSet -> { + final int id = resultSet.getInt("id"); + final String findName = resultSet.getString("name"); + return new User(id, findName); + }; + private final JdbcTemplate jdbcTemplate; + + public UserJdbcDao(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public void save(final String name) { + jdbcTemplate.executeUpdate("INSERT INTO user (name) VALUES (?)", name); + } + + @Override + public Optional findByName(final String name) { + return jdbcTemplate.queryForSingleResult("SELECT * FROM user WHERE name = ?", rowMapper, name); + } +} diff --git a/src/main/java/chess/service/GameService.java b/src/main/java/chess/service/GameService.java new file mode 100644 index 00000000000..919c18222de --- /dev/null +++ b/src/main/java/chess/service/GameService.java @@ -0,0 +1,62 @@ +package chess.service; + +import chess.domain.board.Board; +import chess.domain.board.GameResult; +import chess.domain.position.Position; +import chess.dto.MoveDto; +import chess.repository.GameDao; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class GameService { + private final Map boards = new ConcurrentHashMap<>(); + private final GameDao gameDao; + + public GameService(final GameDao gameDao) { + this.gameDao = gameDao; + } + + public void initialize(final int roomId) { + final Board board = new Board(); + board.initialize(); + final List moves = gameDao.findAllByRoomId(roomId); + for (MoveDto move : moves) { + final Position source = Position.from(move.getSource()); + final Position target = Position.from(move.getTarget()); + board.move(source, target); + } + boards.put(roomId, board); + } + + public void move(final MoveDto moveDto, final int roomId) { + final Board board = boards.get(roomId); + if (board == null) { + throw new IllegalArgumentException("게임을 찾을 수 없습니다."); + } + final Position sourcePosition = Position.from(moveDto.getSource()); + final Position targetPosition = Position.from(moveDto.getTarget()); + board.move(sourcePosition, targetPosition); + gameDao.save(moveDto, roomId); + } + + public boolean isGameOver(final int roomId) { + final Board board = boards.get(roomId); + if (board == null) { + throw new IllegalArgumentException("게임을 찾을 수 없습니다."); + } + return board.isGameOver(); + } + + public GameResult getResult(final int roomId) { + final Board board = boards.get(roomId); + if (board == null) { + throw new IllegalArgumentException("게임을 찾을 수 없습니다."); + } + return board.getResult(); + } + + public void removeBoard(final int roomId) { + boards.remove(roomId); + } +} diff --git a/src/main/java/chess/service/RoomService.java b/src/main/java/chess/service/RoomService.java new file mode 100644 index 00000000000..4c0ae6a1b8a --- /dev/null +++ b/src/main/java/chess/service/RoomService.java @@ -0,0 +1,30 @@ +package chess.service; + +import chess.domain.room.Room; +import chess.repository.RoomDao; +import java.util.List; + +public class RoomService { + private final RoomDao roomDao; + + public RoomService(final RoomDao roomDao) { + this.roomDao = roomDao; + } + + public void save(final String roomName, final int userId) { + roomDao.save(roomName, userId); + } + + public List findAllByUserId(final int userId) { + return roomDao.findAllByUserId(userId); + } + + public Room findById(final int id, final int userId) { + final Room room = roomDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("아이디에 해당하는 방이 없습니다.")); + if (room.isNotCreatedBy(userId)) { + throw new IllegalArgumentException("방의 주인이 아닙니다."); + } + return room; + } +} diff --git a/src/main/java/chess/service/UserService.java b/src/main/java/chess/service/UserService.java new file mode 100644 index 00000000000..d3308c5747b --- /dev/null +++ b/src/main/java/chess/service/UserService.java @@ -0,0 +1,27 @@ +package chess.service; + +import chess.domain.user.User; +import chess.repository.UserDao; +import java.util.Optional; + +public class UserService { + private final UserDao userDao; + + public UserService(final UserDao userDao) { + this.userDao = userDao; + } + + public void save(final String name) { + final Optional user = userDao.findByName(name); + if (user.isPresent()) { + throw new IllegalArgumentException("이미 등록된 이름입니다."); + } + userDao.save(name); + } + + public User findByName(final String name) { + final User user = userDao.findByName(name) + .orElseThrow(() -> new IllegalArgumentException("해당 이름을 가진 유저가 없습니다.")); + return user; + } +} diff --git a/src/main/java/chess/view/input/InputView.java b/src/main/java/chess/view/input/InputView.java new file mode 100644 index 00000000000..3b0f207f9e9 --- /dev/null +++ b/src/main/java/chess/view/input/InputView.java @@ -0,0 +1,30 @@ +package chess.view.input; + +import static java.util.stream.Collectors.toList; + +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +public class InputView { + private static final String DELIMITER = " "; + private static final int LIMIT = -1; + + private final Scanner scanner; + + public InputView(final Scanner scanner) { + this.scanner = scanner; + } + + public String readCommand() { + return scanner.nextLine(); + } + + public List readCommands() { + final String input = scanner.nextLine(); + + return Arrays.stream(input.split(DELIMITER, LIMIT)) + .map(String::trim) + .collect(toList()); + } +} diff --git a/src/main/java/chess/view/output/BoardConverter.java b/src/main/java/chess/view/output/BoardConverter.java new file mode 100644 index 00000000000..7623d76d557 --- /dev/null +++ b/src/main/java/chess/view/output/BoardConverter.java @@ -0,0 +1,55 @@ +package chess.view.output; + +import chess.domain.piece.Color; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceType; +import chess.domain.position.File; +import chess.domain.position.Position; +import chess.domain.position.Rank; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Map; +import java.util.stream.Collectors; + +public class BoardConverter { + private static final String NEXT_LINE = System.lineSeparator(); + private static final Map SYMBOLS; + + static { + SYMBOLS = Map.of( + PieceType.PAWN, "P", + PieceType.KNIGHT, "N", + PieceType.BISHOP, "B", + PieceType.ROOK, "R", + PieceType.QUEEN, "Q", + PieceType.KING, "K", + PieceType.EMPTY, "." + ); + } + + private BoardConverter() { + } + + public static String convert(final Map board) { + final String result = Arrays.stream(Rank.values()) + .sorted(Comparator.reverseOrder()) + .map(rank -> generatePieceSymbols(board, rank)) + .collect(Collectors.joining(NEXT_LINE)); + return result + NEXT_LINE; + } + + private static String generatePieceSymbols(final Map board, final Rank rank) { + return Arrays.stream(File.values()) + .map(file -> Position.of(file, rank)) + .map(position -> generatePieceSymbol(board.get(position))) + .collect(Collectors.joining()); + } + + private static String generatePieceSymbol(final Piece piece) { + final String result = SYMBOLS.get(piece.type()); + if (piece.isSameColor(Color.WHITE)) { + return result.toLowerCase(); + } + return result; + } +} diff --git a/src/main/java/chess/view/output/GameOutputView.java b/src/main/java/chess/view/output/GameOutputView.java new file mode 100644 index 00000000000..84b544ba5bc --- /dev/null +++ b/src/main/java/chess/view/output/GameOutputView.java @@ -0,0 +1,46 @@ +package chess.view.output; + +import chess.domain.board.GameResult; +import chess.domain.piece.Color; + +public class GameOutputView { + public void printGameStart() { + System.out.println("> 체스 게임을 시작합니다."); + } + + public void printCommands(final String user, final String room) { + System.out.println("[로그인 한 유저: " + user + ", 선택한 방 이름: " + room + "]"); + System.out.println("> 게임 이동 : move source위치 target위치 - 예) move b2 b3"); + System.out.println("> 게임 상태 : status"); + System.out.println("> 게임 종료 : end"); + } + + public void printBoard(final GameResult result) { + System.out.println(BoardConverter.convert(result.getBoard())); + } + + public void printStatus(final GameResult result) { + System.out.println("흰색 점수: " + result.score(Color.WHITE)); + System.out.println("검은색 점수: " + result.score(Color.BLACK)); + System.out.println("현재 상태: " + generateWinnerMessage(result)); + } + + private String generateWinnerMessage(final GameResult result) { + final Color winner = result.winner(); + if (winner == Color.WHITE) { + return "백색 승"; + } + if (winner == Color.BLACK) { + return "검은색 승"; + } + return "무승부"; + } + + public void printGameEnd() { + System.out.println("체스 게임을 종료합니다."); + } + + public void printException(final String message) { + System.out.println("[ERROR] " + message); + } +} diff --git a/src/main/java/chess/view/output/MainOutputView.java b/src/main/java/chess/view/output/MainOutputView.java new file mode 100644 index 00000000000..9dc78234ae0 --- /dev/null +++ b/src/main/java/chess/view/output/MainOutputView.java @@ -0,0 +1,16 @@ +package chess.view.output; + +public class MainOutputView { + + public void printCommands(final String user, final String room) { + System.out.println("[로그인 한 유저: " + user + ", 선택한 방 이름: " + room + "]"); + System.out.println("> 계정 관리 : user"); + System.out.println("> 방 관리 : room"); + System.out.println("> 게임 시작 : start"); + System.out.println("> 종료 : end"); + } + + public void printException(final String message) { + System.out.println("[ERROR] " + message); + } +} diff --git a/src/main/java/chess/view/output/RoomOutputView.java b/src/main/java/chess/view/output/RoomOutputView.java new file mode 100644 index 00000000000..f3e5833587f --- /dev/null +++ b/src/main/java/chess/view/output/RoomOutputView.java @@ -0,0 +1,28 @@ +package chess.view.output; + +import chess.domain.room.Room; +import java.util.List; + +public class RoomOutputView { + public void printCommands(final String user, final String room) { + System.out.println("[로그인 한 유저: " + user + ", 선택한 방 이름: " + room + "]"); + System.out.println("> 방 조회 : history"); + System.out.println("> 방 생성 : create 방이름 - 예) create room1"); + System.out.println("> 방 참가 : join 방번호 - 예) join 1"); + System.out.println("> 메인 화면 : end"); + } + + public void printRooms(final List rooms) { + for (Room room : rooms) { + System.out.println("> " + room.getId() + ". " + room.getName()); + } + } + + public void printSaveSuccess(final String roomName) { + System.out.println(roomName + " 방 생성 완료"); + } + + public void printException(final String message) { + System.out.println("[ERROR] " + message); + } +} diff --git a/src/main/java/chess/view/output/UserOutputView.java b/src/main/java/chess/view/output/UserOutputView.java new file mode 100644 index 00000000000..e90fd37574c --- /dev/null +++ b/src/main/java/chess/view/output/UserOutputView.java @@ -0,0 +1,23 @@ +package chess.view.output; + +public class UserOutputView { + public void printCommands(final String user, final String room) { + System.out.println("[로그인 한 유저: " + user + ", 선택한 방 이름: " + room + "]"); + System.out.println("> 회원가입 : register 이름 - 예) register charlie"); + System.out.println("> 로그인 : login 이름 - 예) login charlie"); + System.out.println("> 로그아웃 : logout"); + System.out.println("> 메인 화면 : end"); + } + + public void printRegisterSuccess(final String name) { + System.out.println(name + " 회원 가입 완료"); + } + + public void printLoginSuccess(final String name) { + System.out.println(name + " 로그인 완료!"); + } + + public void printException(final String message) { + System.out.println("[ERROR] " + message); + } +} diff --git a/src/main/resources/chess.sql b/src/main/resources/chess.sql new file mode 100644 index 00000000000..673f14244fd --- /dev/null +++ b/src/main/resources/chess.sql @@ -0,0 +1,29 @@ +create +database testchess; + +CREATE TABLE User +( + id int PRIMARY KEY AUTO_INCREMENT, + name varchar(255) +); + +CREATE TABLE Room +( + id int PRIMARY KEY AUTO_INCREMENT, + name varchar(255), + user_id int +); + +CREATE TABLE Move +( + id int PRIMARY KEY AUTO_INCREMENT, + source varchar(2), + target varchar(2), + room_id int +); + +ALTER TABLE Room + ADD FOREIGN KEY (user_id) REFERENCES User (`id`); + +ALTER TABLE Move + ADD FOREIGN KEY (room_id) REFERENCES Room (`id`); 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/game/GameCommandTest.java b/src/test/java/chess/controller/game/GameCommandTest.java new file mode 100644 index 00000000000..eaba0038ae3 --- /dev/null +++ b/src/test/java/chess/controller/game/GameCommandTest.java @@ -0,0 +1,49 @@ +package chess.controller.game; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class GameCommandTest { + + @Test + void 올바른_입력값이_아니라면_예외를_던진다() { + // given + final List invalidCommands = List.of("invalid"); + + // expect + assertThatThrownBy(() -> GameCommand.from(invalidCommands)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 명령어를 입력해주세요."); + } + + @Test + void Command가_정상_반환된다() { + // given + final List validCommand = List.of("move"); + + // when + final GameCommand command = GameCommand.from(validCommand); + + // expect + assertThat(command).isEqualTo(GameCommand.MOVE); + } + + @Test + void 명령어가_올바른_길이가_아니라면_예외를_던진다() { + // given + final List invalidCommands = List.of("move", "e2", "e4", "e6"); + final GameCommand command = GameCommand.from(invalidCommands); + + // expect + assertThatThrownBy(() -> command.validateCommandsSize(invalidCommands)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 명령어를 입력해주세요."); + } +} diff --git a/src/test/java/chess/controller/main/MainCommandTest.java b/src/test/java/chess/controller/main/MainCommandTest.java new file mode 100644 index 00000000000..b9dd65fd734 --- /dev/null +++ b/src/test/java/chess/controller/main/MainCommandTest.java @@ -0,0 +1,36 @@ +package chess.controller.main; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class MainCommandTest { + + @Test + void 올바른_입력값이_아니라면_예외를_던진다() { + // given + final String invalidCommand = "invalid"; + + // expect + assertThatThrownBy(() -> MainCommand.from(invalidCommand)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 명령어를 입력해주세요."); + } + + @Test + void Command가_정상_반환된다() { + // given + final String validCommand = "START"; + + // when + final MainCommand command = MainCommand.from(validCommand); + + // expect + assertThat(command).isEqualTo(MainCommand.START); + } +} diff --git a/src/test/java/chess/controller/room/RoomCommandTest.java b/src/test/java/chess/controller/room/RoomCommandTest.java new file mode 100644 index 00000000000..31bad325fb9 --- /dev/null +++ b/src/test/java/chess/controller/room/RoomCommandTest.java @@ -0,0 +1,49 @@ +package chess.controller.room; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class RoomCommandTest { + + @Test + void 올바른_입력값이_아니라면_예외를_던진다() { + // given + final List invalidCommands = List.of("invalid"); + + // expect + assertThatThrownBy(() -> RoomCommand.from(invalidCommands)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 명령어를 입력해주세요."); + } + + @Test + void Command가_정상_반환된다() { + // given + final List validCommand = List.of("history"); + + // when + final RoomCommand command = RoomCommand.from(validCommand); + + // expect + assertThat(command).isEqualTo(RoomCommand.HISTORY); + } + + @Test + void 명령어가_올바른_길이가_아니라면_예외를_던진다() { + // given + final List invalidCommands = List.of("join", "1", "3"); + final RoomCommand command = RoomCommand.from(invalidCommands); + + // expect + assertThatThrownBy(() -> command.validateCommandsSize(invalidCommands)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 명령어를 입력해주세요."); + } +} diff --git a/src/test/java/chess/controller/user/UserCommandTest.java b/src/test/java/chess/controller/user/UserCommandTest.java new file mode 100644 index 00000000000..7e45a2d2feb --- /dev/null +++ b/src/test/java/chess/controller/user/UserCommandTest.java @@ -0,0 +1,49 @@ +package chess.controller.user; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class UserCommandTest { + + @Test + void 올바른_입력값이_아니라면_예외를_던진다() { + // given + final List invalidCommands = List.of("invalid"); + + // expect + assertThatThrownBy(() -> UserCommand.from(invalidCommands)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 명령어를 입력해주세요."); + } + + @Test + void Command가_정상_반환된다() { + // given + final List validCommand = List.of("register"); + + // when + final UserCommand command = UserCommand.from(validCommand); + + // expect + assertThat(command).isEqualTo(UserCommand.REGISTER); + } + + @Test + void 명령어가_올바른_길이가_아니라면_예외를_던진다() { + // given + final List invalidCommands = List.of("register", "my", "user"); + final UserCommand command = UserCommand.from(invalidCommands); + + // expect + assertThatThrownBy(() -> command.validateCommandsSize(invalidCommands)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 명령어를 입력해주세요."); + } +} diff --git a/src/test/java/chess/db/TestConnectionPool.java b/src/test/java/chess/db/TestConnectionPool.java new file mode 100644 index 00000000000..10e44fca6ce --- /dev/null +++ b/src/test/java/chess/db/TestConnectionPool.java @@ -0,0 +1,37 @@ +package chess.db; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class TestConnectionPool implements ConnectionPool { + private static final String SERVER = "localhost:13306"; + private static final String DATABASE = "testchess"; + private static final String OPTION = "?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true"; + private static final String USERNAME = "root"; + private static final String PASSWORD = "root"; + private static final String URL = "jdbc:mysql://" + SERVER + "/" + DATABASE + OPTION; + + private final Connection CONNECTION; + + public TestConnectionPool() { + try { + CONNECTION = DriverManager.getConnection(URL, USERNAME, PASSWORD); + } catch (SQLException e) { + throw new DatabaseConnectionFailException(); + } + } + + @Override + public Connection getConnection() { + return CONNECTION; + } + + public void closeConnection() { + try { + CONNECTION.close(); + } catch (SQLException e) { + throw new DatabaseConnectionFailException(); + } + } +} diff --git a/src/test/java/chess/domain/board/BoardGeneratorTest.java b/src/test/java/chess/domain/board/BoardGeneratorTest.java new file mode 100644 index 00000000000..84dcc280dbf --- /dev/null +++ b/src/test/java/chess/domain/board/BoardGeneratorTest.java @@ -0,0 +1,52 @@ +package chess.domain.board; + +import static chess.domain.piece.PieceType.BISHOP; +import static chess.domain.piece.PieceType.EMPTY; +import static chess.domain.piece.PieceType.KING; +import static chess.domain.piece.PieceType.KNIGHT; +import static chess.domain.piece.PieceType.PAWN; +import static chess.domain.piece.PieceType.QUEEN; +import static chess.domain.piece.PieceType.ROOK; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; + +import chess.domain.piece.Piece; +import chess.domain.piece.PieceType; +import chess.domain.position.File; +import chess.domain.position.Position; +import chess.domain.position.Rank; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class BoardGeneratorTest { + + @Test + void 초기화된_체스판을_반환한다() { + // expect + final Map result = BoardGenerator.generate(); + final List pieceTypes = Arrays.stream(Rank.values()) + .sorted(Comparator.reverseOrder()) + .flatMap(file -> Arrays.stream(File.values()).map(rank -> Position.of(rank, file))) + .map(result::get) + .map(Piece::type) + .collect(toList()); + + assertThat(pieceTypes).containsExactly( + ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK, + PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, + ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK + ); + } +} diff --git a/src/test/java/chess/domain/board/BoardTest.java b/src/test/java/chess/domain/board/BoardTest.java new file mode 100644 index 00000000000..4444f7d6ee1 --- /dev/null +++ b/src/test/java/chess/domain/board/BoardTest.java @@ -0,0 +1,169 @@ +package chess.domain.board; + +import static chess.domain.piece.PieceType.BISHOP; +import static chess.domain.piece.PieceType.EMPTY; +import static chess.domain.piece.PieceType.KING; +import static chess.domain.piece.PieceType.KNIGHT; +import static chess.domain.piece.PieceType.PAWN; +import static chess.domain.piece.PieceType.QUEEN; +import static chess.domain.piece.PieceType.ROOK; +import static chess.fixture.PositionFixture.D1; +import static chess.fixture.PositionFixture.D2; +import static chess.fixture.PositionFixture.D4; +import static chess.fixture.PositionFixture.D5; +import static chess.fixture.PositionFixture.E2; +import static chess.fixture.PositionFixture.E4; +import static chess.fixture.PositionFixture.E5; +import static chess.fixture.PositionFixture.E7; +import static chess.fixture.PositionFixture.E8; +import static chess.fixture.PositionFixture.F5; +import static chess.fixture.PositionFixture.F7; +import static chess.fixture.PositionFixture.G6; +import static chess.fixture.PositionFixture.G7; +import static chess.fixture.PositionFixture.H5; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import chess.domain.piece.Color; +import chess.domain.piece.Pawn; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceType; +import chess.domain.position.File; +import chess.domain.position.Position; +import chess.domain.position.Rank; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class BoardTest { + + @Test + void 체스판을_초기화한다() { + // given + Board board = new Board(); + + // when + board.initialize(); + + // then + final Map result = board.getResult().getBoard(); + final List pieceTypes = Arrays.stream(Rank.values()) + .sorted(Comparator.reverseOrder()) + .flatMap(file -> Arrays.stream(File.values()).map(rank -> Position.of(rank, file))) + .map(result::get) + .map(Piece::type) + .collect(toList()); + assertThat(pieceTypes).containsExactly( + ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK, + PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, + ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK + ); + } + + @Test + void 보드가_초기화되지_않은_상태에서_이동_커맨드를_입력받는_경우_예외를_던진다() { + // given + Board board = new Board(); + + // expect + assertThatThrownBy(() -> board.move(D2, D5)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("게임을 진행할 수 있는 상태가 아닙니다."); + } + + @Test + void 왕이_잡힌_상태에서_이동_커맨드를_입력받는_경우_예외를_던진다() { + // given + final Board board = new Board(); + board.initialize(); + board.move(E2, E4); + board.move(E7, E5); + board.move(D1, H5); + board.move(F7, F5); + board.move(H5, E8); + + // expect + assertThatThrownBy(() -> board.move(D2, D5)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("게임을 진행할 수 있는 상태가 아닙니다."); + } + + @Test + void 불가능한_이동_커맨드를_입력받는_경우_예외를_던진다() { + // given + Board board = new Board(); + board.initialize(); + + // expect + assertThatThrownBy(() -> board.move(D2, D5)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바르지 않은 이동 명령어 입니다."); + } + + @Test + void 이동_가능한_커맨드를_입력받는_경우_기물을_이동한다() { + // given + Board board = new Board(); + board.initialize(); + + // when + board.move(E2, E4); + + // then + final Map result = board.getResult().getBoard(); + assertThat(result.get(E4)).isEqualTo(Pawn.from(Color.WHITE)); + } + + @Test + void 상대편의_기물을_이동하려는_경우_예외를_던진다() { + // given + Board board = new Board(); + board.initialize(); + + // expect + assertThatThrownBy(() -> board.move(G7, G6)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("상대방의 기물을 움직일 수 없습니다."); + } + + @Test + void 이동_경로에_기물이_있는_경우_예외를_던진다() { + // given + Board board = new Board(); + board.initialize(); + + // expect + assertThatThrownBy(() -> board.move(D1, D4)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동 경로에 다른 기물이 있을 수 없습니다."); + } + + @Test + void 왕이_잡히는_경우_게임이_종료된다() { + // given + Board board = new Board(); + board.initialize(); + board.move(E2, E4); + board.move(E7, E5); + board.move(D1, H5); + board.move(F7, F5); + + // when + board.move(H5, E8); + + // then + assertThat(board.isGameOver()).isTrue(); + } +} diff --git a/src/test/java/chess/domain/board/GameResultTest.java b/src/test/java/chess/domain/board/GameResultTest.java new file mode 100644 index 00000000000..6a636e09096 --- /dev/null +++ b/src/test/java/chess/domain/board/GameResultTest.java @@ -0,0 +1,112 @@ +package chess.domain.board; + +import static chess.fixture.PositionFixture.B8; +import static chess.fixture.PositionFixture.C1; +import static chess.fixture.PositionFixture.C6; +import static chess.fixture.PositionFixture.D1; +import static chess.fixture.PositionFixture.D2; +import static chess.fixture.PositionFixture.D4; +import static chess.fixture.PositionFixture.D5; +import static chess.fixture.PositionFixture.D7; +import static chess.fixture.PositionFixture.E2; +import static chess.fixture.PositionFixture.E4; +import static chess.fixture.PositionFixture.E5; +import static chess.fixture.PositionFixture.E7; +import static chess.fixture.PositionFixture.E8; +import static chess.fixture.PositionFixture.F4; +import static chess.fixture.PositionFixture.F5; +import static chess.fixture.PositionFixture.F7; +import static chess.fixture.PositionFixture.H5; +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.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class GameResultTest { + + @Test + void 기물의_총_점수를_반환한다() { + // given + Board board = new Board(); + board.initialize(); + final GameResult result = board.getResult(); + + // when + final double score = result.score(Color.WHITE); + + // then + assertThat(score).isEqualTo(38.0); + } + + @Test + void 하나의_File에_여러_개의_폰이_존재하는_경우_각폰의_점수가_반이_된다() { + // given + Board board = new Board(); + board.initialize(); + board.move(E2, E4); + board.move(D7, D5); + board.move(E4, D5); + final GameResult result = board.getResult(); + + // when + final double score = result.score(Color.WHITE); + + // then + assertThat(score).isEqualTo(37.0); + } + + @Test + void 왕이_잡힌_경우_왕을_잡은쪽이_승리한다() { + // given + Board board = new Board(); + board.initialize(); + board.move(E2, E4); + board.move(E7, E5); + board.move(D1, H5); + board.move(F7, F5); + board.move(H5, E8); + final GameResult result = board.getResult(); + + // when + final Color winner = result.winner(); + + // then + assertThat(winner).isEqualTo(Color.WHITE); + } + + @Test + void 왕이_잡히지_않은_경우_점수를_비교하여_결과를_계산한다() { + // given + Board board = new Board(); + board.initialize(); + board.move(D2, D4); + board.move(B8, C6); + board.move(C1, F4); + board.move(C6, D4); + final GameResult result = board.getResult(); + + // when + final Color winner = result.winner(); + + // then + assertThat(winner).isEqualTo(Color.BLACK); + } + + @Test + void 양쪽다_왕이_살아있고_점수가_동일할_경우_EMPTY를_반환한다() { + // given + Board board = new Board(); + board.initialize(); + final GameResult result = board.getResult(); + + // when + final Color winner = result.winner(); + + // then + assertThat(winner).isEqualTo(Color.EMPTY); + } +} diff --git a/src/test/java/chess/domain/piece/BishopTest.java b/src/test/java/chess/domain/piece/BishopTest.java new file mode 100644 index 00000000000..237327feca6 --- /dev/null +++ b/src/test/java/chess/domain/piece/BishopTest.java @@ -0,0 +1,70 @@ +package chess.domain.piece; + +import static chess.domain.piece.Color.BLACK; +import static chess.domain.piece.Color.WHITE; +import static chess.domain.piece.Pawn.from; +import static org.assertj.core.api.Assertions.assertThat; + +import chess.domain.position.File; +import chess.domain.position.Position; +import chess.domain.position.Rank; +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; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class BishopTest { + + @Test + void 비숍이_정상적으로_생성된다() { + // given + final Bishop bishop = Bishop.from(Color.WHITE); + + // expect + assertThat(bishop.type()).isEqualTo(PieceType.BISHOP); + } + + @ParameterizedTest(name = "비숍이 WHITE일 때 가려는 위치에 있는 기물이 {0}인 경우 움직임 가능 여부: {1}") + @CsvSource({"WHITE, false", "BLACK, true", "EMPTY, true"}) + void 아군의_위치로_이동_가능_여부를_확인한다(final Color color, final boolean result) { + // given + final Bishop bishop = Bishop.from(WHITE); + + // when + final boolean movable = bishop.isValidTarget(from(color)); + + // then + assertThat(movable).isEqualTo(result); + } + + @ParameterizedTest(name = "비숍이 해당 위치로 움직일 수 있다. 현재 위치: D, FOUR, 이동 위치: {0}, {1}") + @CsvSource({"A, SEVEN", "C, FIVE", "H, EIGHT", "E, FIVE", "G, ONE", "E, THREE", "A, ONE", "C, THREE"}) + void 비숍이_해당_위치로_움직일_수_있다(final File file, final Rank rank) { + // given + final Bishop bishop = Bishop.from(WHITE); + final Position start = Position.of(File.D, Rank.FOUR); + + // when + final boolean result = bishop.isValidMove(start, Position.of(file, rank)); + + // then + assertThat(result).isTrue(); + } + + @ParameterizedTest(name = "비숍이 해당 위치로 움직일 수 없다. 현재 위치: D, FIVE, 이동 위치: {0}, {1}") + @CsvSource({"A, SEVEN", "C, FIVE", "H, EIGHT", "E, FIVE", "G, ONE", "E, THREE", "A, ONE", "C, THREE"}) + void 비숍이_해당_위치로_움직일_수_없다(final File file, final Rank rank) { + // given + final Bishop bishop = Bishop.from(BLACK); + final Position start = Position.of(File.D, Rank.FIVE); + + // when + final boolean result = bishop.isValidMove(start, Position.of(file, rank)); + + // then + assertThat(result).isFalse(); + } +} diff --git a/src/test/java/chess/domain/piece/ColorTest.java b/src/test/java/chess/domain/piece/ColorTest.java new file mode 100644 index 00000000000..b384cd33864 --- /dev/null +++ b/src/test/java/chess/domain/piece/ColorTest.java @@ -0,0 +1,28 @@ +package chess.domain.piece; + +import static chess.domain.piece.Color.BLACK; +import static org.assertj.core.api.Assertions.assertThat; + +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; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class ColorTest { + + @ParameterizedTest(name = "입력한 Color와 다른 값인지 확인한다. 대상: BLACK 입력: {0}, 결과: {1}") + @CsvSource({"WHITE, true", "BLACK, false", "EMPTY, false"}) + void 입력한_Color가_아닌지_확인한다(final Color color, final boolean result) { + // expect + assertThat(BLACK.isOpponent(color)).isEqualTo(result); + } + + @ParameterizedTest(name = "다음 턴을 반한한다 현재: {0}, 결과: {1}") + @CsvSource({"WHITE, BLACK", "BLACK, WHITE"}) + void 다음_턴을_반환한다(final Color current, final Color next) { + // expect + assertThat(current.nextTurn()).isEqualTo(next); + } +} diff --git a/src/test/java/chess/domain/piece/EmptyTest.java b/src/test/java/chess/domain/piece/EmptyTest.java new file mode 100644 index 00000000000..b920107fcb7 --- /dev/null +++ b/src/test/java/chess/domain/piece/EmptyTest.java @@ -0,0 +1,21 @@ +package chess.domain.piece; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class EmptyTest { + + @Test + void 빈_기물을_정상적으로_반환한다() { + // given + final Empty empty = Empty.instance(); + + // expect + assertThat(empty.type()).isEqualTo(PieceType.EMPTY); + } +} diff --git a/src/test/java/chess/domain/piece/KingTest.java b/src/test/java/chess/domain/piece/KingTest.java new file mode 100644 index 00000000000..bf0755a0c13 --- /dev/null +++ b/src/test/java/chess/domain/piece/KingTest.java @@ -0,0 +1,69 @@ +package chess.domain.piece; + +import static chess.domain.piece.Color.WHITE; +import static chess.domain.piece.Pawn.from; +import static org.assertj.core.api.Assertions.assertThat; + +import chess.domain.position.File; +import chess.domain.position.Position; +import chess.domain.position.Rank; +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; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class KingTest { + + @Test + void 킹이_정상적으로_생성된다() { + // given + final King king = King.from(Color.WHITE); + + // expect + assertThat(king.type()).isEqualTo(PieceType.KING); + } + + @ParameterizedTest(name = "킹이 WHITE일 때 가려는 위치에 있는 기물이 {0}인 경우 움직임 가능 여부: {1}") + @CsvSource({"WHITE, false", "BLACK, true", "EMPTY, true"}) + void 아군의_위치로_이동_가능_여부를_확인한다(final Color color, final boolean result) { + // given + final King king = King.from(WHITE); + + // when + final boolean movable = king.isValidTarget(from(color)); + + // then + assertThat(movable).isEqualTo(result); + } + + @ParameterizedTest(name = "킹이 해당 위치로 움직일 수 있다. 현재 위치: D, FOUR, 이동 위치: {0}, {1}") + @CsvSource({"C, FIVE", "D, FIVE", "E, FIVE", "C, FOUR", "E, FOUR", "C, THREE", "D, THREE", "E, THREE"}) + void 킹이_해당_위치로_움직일_수_있다(final File file, final Rank rank) { + // given + final King king = King.from(WHITE); + final Position start = Position.of(File.D, Rank.FOUR); + + // when + final boolean result = king.isValidMove(start, Position.of(file, rank)); + + // then + assertThat(result).isTrue(); + } + + @ParameterizedTest(name = "킹이 해당 위치로 움직일 수 없다. 현재 위치: G, SEVEN, 이동 위치: {0}, {1}") + @CsvSource({"C, FIVE", "D, FIVE", "E, FIVE", "C, FOUR", "E, FOUR", "C, THREE", "D, THREE", "E, THREE"}) + void 킹이_해당_위치로_움직일_수_없다(final File file, final Rank rank) { + // given + final King king = King.from(WHITE); + final Position start = Position.of(File.G, Rank.SEVEN); + + // when + final boolean result = king.isValidMove(start, Position.of(file, rank)); + + // then + assertThat(result).isFalse(); + } +} diff --git a/src/test/java/chess/domain/piece/KnightTest.java b/src/test/java/chess/domain/piece/KnightTest.java new file mode 100644 index 00000000000..9f3d8037529 --- /dev/null +++ b/src/test/java/chess/domain/piece/KnightTest.java @@ -0,0 +1,69 @@ +package chess.domain.piece; + +import static chess.domain.piece.Color.WHITE; +import static chess.domain.piece.Pawn.from; +import static org.assertj.core.api.Assertions.assertThat; + +import chess.domain.position.File; +import chess.domain.position.Position; +import chess.domain.position.Rank; +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; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class KnightTest { + + @Test + void 나이트가_정상적으로_생성된다() { + // given + final Knight knight = Knight.from(WHITE); + + // expect + assertThat(knight.type()).isEqualTo(PieceType.KNIGHT); + } + + @ParameterizedTest(name = "나이트가 WHITE일 때 가려는 위치에 있는 기물이 {0}인 경우 움직임 가능 여부: {1}") + @CsvSource({"WHITE, false", "BLACK, true", "EMPTY, true"}) + void 아군의_위치로_이동_가능_여부를_확인한다(final Color color, final boolean result) { + // given + final Knight knight = Knight.from(WHITE); + + // when + final boolean movable = knight.isValidTarget(from(color)); + + // then + assertThat(movable).isEqualTo(result); + } + + @ParameterizedTest(name = "나이트가 해당 위치로 움직일 수 있다. 현재 위치: C, THREE, 이동 위치: {0}, {1}") + @CsvSource({"B, ONE", "A, TWO", "A, FOUR", "B, FIVE", "D, FIVE", "E, FOUR", "E, TWO", "D, ONE"}) + void 나이트가_해당_위치로_움직일_수_있다(final File file, final Rank rank) { + // given + final Knight knight = Knight.from(WHITE); + final Position start = Position.of(File.C, Rank.THREE); + + // when + final boolean result = knight.isValidMove(start, Position.of(file, rank)); + + // then + assertThat(result).isTrue(); + } + + @ParameterizedTest(name = "나이트가 해당 위치로 움직일 수 없다. 현재 위치: G, SEVEN, 이동 위치: {0}, {1}") + @CsvSource({"B, ONE", "A, TWO", "A, FOUR", "B, FIVE", "D, FIVE", "E, FOUR", "E, TWO", "D, ONE"}) + void 나이트가_해당_위치로_움직일_수_없다(final File file, final Rank rank) { + // given + final Knight knight = Knight.from(WHITE); + final Position start = Position.of(File.G, Rank.SEVEN); + + // when + final boolean result = knight.isValidMove(start, Position.of(file, rank)); + + // then + assertThat(result).isFalse(); + } +} diff --git a/src/test/java/chess/domain/piece/PawnTest.java b/src/test/java/chess/domain/piece/PawnTest.java new file mode 100644 index 00000000000..81a430da857 --- /dev/null +++ b/src/test/java/chess/domain/piece/PawnTest.java @@ -0,0 +1,168 @@ +package chess.domain.piece; + +import static chess.domain.piece.Color.BLACK; +import static chess.domain.piece.Color.WHITE; +import static chess.domain.position.Position.of; +import static org.assertj.core.api.Assertions.assertThat; + +import chess.domain.position.File; +import chess.domain.position.Position; +import chess.domain.position.Rank; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class PawnTest { + + @Test + void 폰이_정상적으로_생성된다() { + // given + final Pawn pawn = Pawn.from(Color.WHITE); + + // expect + assertThat(pawn.type()).isEqualTo(PieceType.PAWN); + } + + @Nested + class 흰색_폰이_ { + // given + final Pawn pawn = Pawn.from(WHITE); + final Position start = of(File.E, Rank.TWO); + + @Nested + class 뒤로는_ { + + @ParameterizedTest(name = "이동할 수 없다. 현재 위치: E, TWO, 이동 위치: {0}, {1}") + @CsvSource({"D, ONE", "E, ONE", "F, ONE"}) + void 이동할_수_없다(final File file, final Rank rank) { + // when + final Piece target = Empty.instance(); + + // then + assertThat(pawn.isMovable(start, of(file, rank), target)).isFalse(); + } + } + + @Nested + class 전진_시_ { + + @ParameterizedTest(name = "기물이 존재하지 않으면 움직일 수 있다. 현재 위치: E, TWO, 이동 위치: {0}, {1}") + @CsvSource({"E, THREE", "E, FOUR"}) + void 기물이_존재하지_않으면_움직일_수_있다(final File file, final Rank rank) { + // when + final Piece target = Empty.instance(); + + // then + assertThat(pawn.isMovable(start, of(file, rank), target)).isTrue(); + } + + @ParameterizedTest(name = "상대방 기물이 존재하면 움직일 수 없다. 현재 위치: E, TWO, 이동 위치: {0}, {1}") + @CsvSource({"E, THREE", "E, FOUR"}) + void 상대방_기물이_존재하면_움직일_수_없다(final File file, final Rank rank) { + // when + final Piece target = Pawn.from(BLACK); + + // then + assertThat(pawn.isMovable(start, of(file, rank), target)).isFalse(); + } + } + + @Nested + class 대각선_이동_시_ { + + @ParameterizedTest(name = "상대방의 기물이 존재하면 움직일 수 있다. 현재 위치: E, TWO, 이동 위치: {0}, {1}") + @CsvSource({"F, THREE", "D, THREE"}) + void 상대방의_기물이_존재하면_움직일_수_있다(final File file, final Rank rank) { + // when + final Piece target = Pawn.from(BLACK); + + // then + assertThat(pawn.isMovable(start, of(file, rank), target)).isTrue(); + } + + @ParameterizedTest(name = "상대방의 기물이 존재하지 않으면 움직일 수 없다. 현재 위치: E, TWO, 이동 위치: {0}, {1}") + @CsvSource({"F, THREE", "D, THREE"}) + void 상대방의_기물이_존재하지_않으면_움직일_수_없다(final File file, final Rank rank) { + // when + final Piece target = Empty.instance(); + + // then + assertThat(pawn.isMovable(start, of(file, rank), target)).isFalse(); + } + } + } + + @Nested + class 검은색_폰이_ { + // given + final Pawn pawn = Pawn.from(BLACK); + final Position start = of(File.E, Rank.SEVEN); + + @Nested + class 뒤로는_ { + + @ParameterizedTest(name = "이동할 수 없다. 현재 위치: E, SEVEN, 이동 위치: {0}, {1}") + @CsvSource({"D, EIGHT", "E, EIGHT", "F, EIGHT"}) + void 이동할_수_없다(final File file, final Rank rank) { + // when + final Piece target = Empty.instance(); + + // then + assertThat(pawn.isMovable(start, of(file, rank), target)).isFalse(); + } + } + + @Nested + class 전진_시_ { + + @ParameterizedTest(name = "기물이 존재하지 않으면 움직일 수 있다. 현재 위치: E, SEVEN, 이동 위치: {0}, {1}") + @CsvSource({"E, SIX", "E, FIVE"}) + void 기물이_존재하지_않으면_움직일_수_있다(final File file, final Rank rank) { + // when + final Piece target = Empty.instance(); + + // then + assertThat(pawn.isMovable(start, of(file, rank), target)).isTrue(); + } + + @ParameterizedTest(name = "상대방 기물이 존재하면 움직일 수 없다. 현재 위치: E, SEVEN, 이동 위치: {0}, {1}") + @CsvSource({"E, SIX", "E, FIVE"}) + void 상대방_기물이_존재하면_움직일_수_없다(final File file, final Rank rank) { + // when + final Piece target = Pawn.from(WHITE); + + // then + assertThat(pawn.isMovable(start, of(file, rank), target)).isFalse(); + } + } + + @Nested + class 대각선_이동_시_ { + + @ParameterizedTest(name = "상대방의 기물이 존재하면 움직일 수 있다. 현재 위치: E, SEVEN, 이동 위치: {0}, {1}") + @CsvSource({"D, SIX", "F, SIX"}) + void 상대방의_기물이_존재하면_움직일_수_있다(final File file, final Rank rank) { + // when + final Piece target = Pawn.from(WHITE); + + // then + assertThat(pawn.isMovable(start, of(file, rank), target)).isTrue(); + } + + @ParameterizedTest(name = "상대방의 기물이 존재하지 않으면 움직일 수 없다. 현재 위치: E, SEVEN, 이동 위치: {0}, {1}") + @CsvSource({"D, SIX", "F, SIX"}) + void 상대방의_기물이_존재하지_않으면_움직일_수_없다(final File file, final Rank rank) { + // when + final Piece target = Empty.instance(); + + // then + assertThat(pawn.isMovable(start, of(file, rank), target)).isFalse(); + } + } + } +} diff --git a/src/test/java/chess/domain/piece/PieceTest.java b/src/test/java/chess/domain/piece/PieceTest.java new file mode 100644 index 00000000000..1e8e6b3c6c4 --- /dev/null +++ b/src/test/java/chess/domain/piece/PieceTest.java @@ -0,0 +1,85 @@ +package chess.domain.piece; + +import static org.assertj.core.api.Assertions.assertThat; + +import chess.domain.position.Position; +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; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class PieceTest { + + @ParameterizedTest(name = "색을 입력받아 다른 색인지 확인한다. 현재 색: WHITE, 입력: {0}, 결과: {1}") + @CsvSource({"WHITE, false", "BLACK, true"}) + void 색을_입력받아_다른_색인지_확인한다(final Color color, final boolean result) { + // given + final Piece piece = generatePiece(Color.WHITE, PieceType.PAWN); + + // expect + assertThat(piece.isNotSameColor(color)).isEqualTo(result); + } + + + @ParameterizedTest(name = "색을 입력받아 같은 색인지 확인한다. 현재 색: WHITE, 입력: {0}, 결과: {1}") + @CsvSource({"WHITE, true", "BLACK, false"}) + void 색을_입력받아_같은_색인지_확인한다(final Color color, final boolean result) { + // given + final Piece piece = generatePiece(Color.WHITE, PieceType.PAWN); + + // expect + assertThat(piece.isSameColor(color)).isEqualTo(result); + } + + @ParameterizedTest(name = "색을 입력받아 같은 색인지 확인한다. 현재 타입: PAWN, 입력: {0}, 결과: {1}") + @CsvSource({"PAWN, true", "KING, false"}) + void 타입을_입력받아_같은_타입인지_확인한다(final PieceType type, final boolean result) { + // given + final Piece piece = generatePiece(Color.WHITE, PieceType.PAWN); + + // expect + assertThat(piece.isSameType(type)).isEqualTo(result); + } + + @Test + void 해당_기물의_색상을_반환한다() { + // given + final Piece piece = generatePiece(Color.WHITE, PieceType.PAWN); + + // when + final Color color = piece.color(); + + // then + assertThat(color).isEqualTo(Color.WHITE); + } + + private static Piece generatePiece(final Color color, final PieceType type) { + final Piece piece = new Piece(color, type) { + @Override + protected boolean isValidMove(final Position start, final Position end) { + return false; + } + + @Override + protected boolean isValidTarget(final Piece target) { + return false; + } + }; + return piece; + } + + @Test + void 해당_기물의_타입을_반환한다() { + // given + final Piece piece = generatePiece(Color.WHITE, PieceType.PAWN); + + // when + final PieceType type = piece.type(); + + // then + assertThat(type).isEqualTo(PieceType.PAWN); + } +} 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..31b325b7f30 --- /dev/null +++ b/src/test/java/chess/domain/piece/PieceTypeTest.java @@ -0,0 +1,20 @@ +package chess.domain.piece; + +import static org.assertj.core.api.Assertions.assertThat; + +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; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class PieceTypeTest { + + @ParameterizedTest(name = "각 기물의 점수를 반환한다. 기물: {0}, 점수: {1}") + @CsvSource({"PAWN, 1", "KNIGHT, 2.5", "BISHOP, 3", "ROOK, 5", "QUEEN, 9", "KING, 0"}) + void 각_기물의_점수를_반환한다(final PieceType type, final double score) { + // expect + assertThat(type.score()).isEqualTo(score); + } +} diff --git a/src/test/java/chess/domain/piece/QueenTest.java b/src/test/java/chess/domain/piece/QueenTest.java new file mode 100644 index 00000000000..144aea9b5be --- /dev/null +++ b/src/test/java/chess/domain/piece/QueenTest.java @@ -0,0 +1,72 @@ +package chess.domain.piece; + +import static chess.domain.piece.Color.WHITE; +import static chess.domain.piece.Pawn.from; +import static org.assertj.core.api.Assertions.assertThat; + +import chess.domain.position.File; +import chess.domain.position.Position; +import chess.domain.position.Rank; +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; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class QueenTest { + + @Test + void 퀸이_정상적으로_생성된다() { + // given + final Queen queen = Queen.from(Color.WHITE); + + // expect + assertThat(queen.type()).isEqualTo(PieceType.QUEEN); + } + + @ParameterizedTest(name = "퀸이 WHITE일 때 가려는 위치에 있는 기물이 {0}인 경우 움직임 가능 여부: {1}") + @CsvSource({"WHITE, false", "BLACK, true", "EMPTY, true"}) + void 아군의_위치로_이동_가능_여부를_확인한다(final Color color, final boolean result) { + // given + final Queen queen = Queen.from(WHITE); + + // when + final boolean movable = queen.isValidTarget(from(color)); + + // then + assertThat(movable).isEqualTo(result); + } + + @ParameterizedTest(name = "퀸이 해당 위치로 움직일 수 있다. 현재 위치: D, FOUR, 이동 위치: {0}, {1}") + @CsvSource({ + "C, FIVE", "D, FIVE", "E, FIVE", "C, FOUR", "E, FOUR", "C, THREE", "D, THREE", "E, THREE", + "A, SEVEN", "A, FOUR", "A, ONE", "D, ONE", "G, ONE", "H, FOUR", "H, EIGHT", "D, EIGHT" + }) + void 퀸이_해당_위치로_움직일_수_있다(final File file, final Rank rank) { + // given + final Queen queen = Queen.from(WHITE); + final Position start = Position.of(File.D, Rank.FOUR); + + // when + final boolean result = queen.isValidMove(start, Position.of(file, rank)); + + // then + assertThat(result).isTrue(); + } + + @ParameterizedTest(name = "퀸이 해당 위치로 움직일 수 없다. 현재 위치: G, SEVEN, 이동 위치: {0}, {1}") + @CsvSource({"H, FIVE", "F, FIVE", "E, SIX", "E, EIGHT", "F, THREE", "H, THREE", "C, SIX", "C, EIGHT"}) + void 퀸이_해당_위치로_움직일_수_없다(final File file, final Rank rank) { + // given + final Queen queen = Queen.from(WHITE); + final Position start = Position.of(File.G, Rank.SEVEN); + + // when + final boolean result = queen.isValidMove(start, Position.of(file, rank)); + + // then + assertThat(result).isFalse(); + } +} diff --git a/src/test/java/chess/domain/piece/RookTest.java b/src/test/java/chess/domain/piece/RookTest.java new file mode 100644 index 00000000000..d4f2b3a3c3d --- /dev/null +++ b/src/test/java/chess/domain/piece/RookTest.java @@ -0,0 +1,69 @@ +package chess.domain.piece; + +import static chess.domain.piece.Color.WHITE; +import static chess.domain.piece.Pawn.from; +import static org.assertj.core.api.Assertions.assertThat; + +import chess.domain.position.File; +import chess.domain.position.Position; +import chess.domain.position.Rank; +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; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class RookTest { + + @Test + void 룩이_정상적으로_생성된다() { + // given + final Rook rook = Rook.from(Color.WHITE); + + // expect + assertThat(rook.type()).isEqualTo(PieceType.ROOK); + } + + @ParameterizedTest(name = "룩이 WHITE일 때 가려는 위치에 있는 기물이 {0}인 경우 움직임 가능 여부: {1}") + @CsvSource({"WHITE, false", "BLACK, true", "EMPTY, true"}) + void 아군의_위치로_이동_가능_여부를_확인한다(final Color color, final boolean result) { + // given + final Rook rook = Rook.from(WHITE); + + // when + final boolean movable = rook.isValidTarget(from(color)); + + // then + assertThat(movable).isEqualTo(result); + } + + @ParameterizedTest(name = "룩이 해당 위치로 움직일 수 있다. 현재 위치: D, FOUR, 이동 위치: {0}, {1}") + @CsvSource({"D, ONE", "D, THREE", "A, FOUR", "C, FOUR", "F, FOUR", "H, FOUR", "D, FIVE", "D, EIGHT"}) + void 룩이_해당_위치로_움직일_수_있다(final File file, final Rank rank) { + // given + final Rook rook = Rook.from(WHITE); + final Position start = Position.of(File.D, Rank.FOUR); + + // when + final boolean result = rook.isValidMove(start, Position.of(file, rank)); + + // then + assertThat(result).isTrue(); + } + + @ParameterizedTest(name = "룩이 해당 위치로 움직일 수 없다. 현재 위치: F, SIX, 이동 위치: {0}, {1}") + @CsvSource({"D, ONE", "D, THREE", "A, FOUR", "C, FOUR", "H, FOUR", "D, FIVE", "D, EIGHT"}) + void 룩이_해당_위치로_움직일_수_없다(final File file, final Rank rank) { + // given + final Rook rook = Rook.from(WHITE); + final Position start = Position.of(File.F, Rank.SIX); + + // when + final boolean result = rook.isValidMove(start, Position.of(file, rank)); + + // then + assertThat(result).isFalse(); + } +} diff --git a/src/test/java/chess/domain/position/FileTest.java b/src/test/java/chess/domain/position/FileTest.java new file mode 100644 index 00000000000..98d4ddddd94 --- /dev/null +++ b/src/test/java/chess/domain/position/FileTest.java @@ -0,0 +1,50 @@ +package chess.domain.position; + +import static chess.domain.position.File.A; +import static chess.domain.position.File.B; +import static chess.domain.position.File.C; +import static chess.domain.position.File.D; +import static chess.domain.position.File.E; +import static chess.domain.position.File.F; +import static chess.domain.position.File.G; +import static chess.domain.position.File.H; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class FileTest { + + @ParameterizedTest(name = "두 파일 사이의 파일들을 반환한다. 시작: {0}, 도착: {1}, 결과: {2}") + @MethodSource("betweenSource") + void 두_파일_사이의_파일을_반환한다(final File source, final File target, final List result) { + // expect + assertThat(source.between(target)).containsExactlyElementsOf(result); + } + + static Stream betweenSource() { + return Stream.of( + Arguments.of(A, D, List.of(B, C)), + Arguments.of(A, B, Collections.emptyList()), + Arguments.of(A, A, Collections.emptyList()), + Arguments.of(B, A, Collections.emptyList()), + Arguments.of(H, E, List.of(G, F)) + ); + } + + @ParameterizedTest(name = "입력받은 파일과의 차이를 반환한다. 시작: {0}, 도착: {1}, 결과: {2}") + @CsvSource({"A, D, -3", "B, B, 0", "H, G, 1"}) + void 입력받은_파일과의_차이를_반환한다(final File source, final File target, final int result) { + // expect + assertThat(source.calculateGap(target)).isEqualTo(result); + } +} diff --git a/src/test/java/chess/domain/position/PositionTest.java b/src/test/java/chess/domain/position/PositionTest.java new file mode 100644 index 00000000000..3536e1f9613 --- /dev/null +++ b/src/test/java/chess/domain/position/PositionTest.java @@ -0,0 +1,193 @@ +package chess.domain.position; + +import static chess.domain.position.Position.from; +import static chess.fixture.PositionFixture.A1; +import static chess.fixture.PositionFixture.A2; +import static chess.fixture.PositionFixture.A3; +import static chess.fixture.PositionFixture.A4; +import static chess.fixture.PositionFixture.A5; +import static chess.fixture.PositionFixture.A6; +import static chess.fixture.PositionFixture.A7; +import static chess.fixture.PositionFixture.A8; +import static chess.fixture.PositionFixture.B1; +import static chess.fixture.PositionFixture.B2; +import static chess.fixture.PositionFixture.B3; +import static chess.fixture.PositionFixture.B5; +import static chess.fixture.PositionFixture.B6; +import static chess.fixture.PositionFixture.B7; +import static chess.fixture.PositionFixture.B8; +import static chess.fixture.PositionFixture.C1; +import static chess.fixture.PositionFixture.C2; +import static chess.fixture.PositionFixture.C3; +import static chess.fixture.PositionFixture.C6; +import static chess.fixture.PositionFixture.D1; +import static chess.fixture.PositionFixture.D2; +import static chess.fixture.PositionFixture.D4; +import static chess.fixture.PositionFixture.D5; +import static chess.fixture.PositionFixture.E1; +import static chess.fixture.PositionFixture.E2; +import static chess.fixture.PositionFixture.E4; +import static chess.fixture.PositionFixture.E5; +import static chess.fixture.PositionFixture.E6; +import static chess.fixture.PositionFixture.F1; +import static chess.fixture.PositionFixture.F2; +import static chess.fixture.PositionFixture.F3; +import static chess.fixture.PositionFixture.F4; +import static chess.fixture.PositionFixture.F5; +import static chess.fixture.PositionFixture.F6; +import static chess.fixture.PositionFixture.G1; +import static chess.fixture.PositionFixture.G2; +import static chess.fixture.PositionFixture.G3; +import static chess.fixture.PositionFixture.G7; +import static chess.fixture.PositionFixture.H1; +import static chess.fixture.PositionFixture.H2; +import static chess.fixture.PositionFixture.H8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Collections; +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; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class PositionTest { + + @Test + void Rank_와_File_을_받아_정상적으로_생성된다() { + // expect + assertThatNoException().isThrownBy(() -> Position.of(File.A, Rank.ONE)); + } + + @Test + void command_를_입력받아_Position_을_반환한다() { + // given + final Position position = from("a2"); + + // expect + assertThat(position).isEqualTo(Position.of(File.A, Rank.TWO)); + } + + @Test + void 잘못된_커맨드를_입력받으면_예외를_던진다() { + // expect + assertThatThrownBy(() -> from("a0")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("잘못된 위치값입니다."); + } + + @Test + void 입력받는_포지션과의_파일_차이를_반환한다() { + // given + final Position source = Position.of(File.A, Rank.FOUR); + final Position target = Position.of(File.G, Rank.FIVE); + + // when + final int result = source.calculateFileGap(target); + + // then + assertThat(result).isEqualTo(-6); + } + + @Test + void 입력받는_포지션과의_랭크_차이를_반환한다() { + // given + final Position source = Position.of(File.A, Rank.FOUR); + final Position target = Position.of(File.G, Rank.FIVE); + + // when + final int result = source.calculateRankGap(target); + + // then + assertThat(result).isEqualTo(-1); + } + + @ParameterizedTest(name = "시작과 끝이 직선인 경우 포지션 사이의 값들을 반환한다. 시작: {0}, 도착: {1}") + @MethodSource("betweenStraightSource") + void 시작과_끝이_직선인_경우_포지션_사이의_값들을_반환한다( + final Position source, + final Position target, + final List result + ) { + // expect + final List positions = source.between(target); + assertThat(positions).containsAll(result); + } + + static Stream betweenStraightSource() { + return Stream.of( + Arguments.of(A1, A8, List.of(A2, A3, A4, A5, A6, A7)), + Arguments.of(A1, H1, List.of(B1, C1, D1, E1, F1, G1)), + Arguments.of(A1, A2, Collections.emptyList()), + Arguments.of(A1, B1, Collections.emptyList()), + Arguments.of(H2, A2, List.of(G2, F2, E2, D2, C2, B2)), + Arguments.of(B8, B6, List.of(B7)) + ); + } + + @ParameterizedTest(name = "시작과 끝이 대각선인 경우 포지션 사이의 값들을 반환한다. 시작: {0}, 도착: {1}") + @MethodSource("betweenDiagonalSource") + void 시작과_끝이_대각선인_경우_포지션_사이의_값들을_반환한다( + final Position source, + final Position target, + final List result + ) { + // expect + final List positions = source.between(target); + assertThat(positions).containsAll(result); + } + + static Stream betweenDiagonalSource() { + return Stream.of( + Arguments.of(A1, H8, List.of(B2, C3, D4, E5, F6, G7)), + Arguments.of(A8, H1, List.of(B7, C6, D5, E4, F3, G2)), + Arguments.of(A1, B2, Collections.emptyList()), + Arguments.of(B2, C1, Collections.emptyList()), + Arguments.of(H2, F4, List.of(G3)), + Arguments.of(D4, B2, List.of(C3)) + ); + } + + @ParameterizedTest(name = "나이트 이동의 경우 빈 컬렉션을 반환한다. 시작: {0}, 도착: {1}") + @MethodSource("betweenKnightSource") + void 나이트_이동의_경우_빈_컬렉션을_반환한다( + final Position source, + final Position target, + final List result + ) { + // expect + final List positions = source.between(target); + assertThat(positions).containsAll(result); + } + + static Stream betweenKnightSource() { + return Stream.of( + Arguments.of(D4, C2, Collections.emptyList()), + Arguments.of(D4, B3, Collections.emptyList()), + Arguments.of(D4, B5, Collections.emptyList()), + Arguments.of(D4, C6, Collections.emptyList()), + Arguments.of(D4, E6, Collections.emptyList()), + Arguments.of(D4, F5, Collections.emptyList()), + Arguments.of(D4, F3, Collections.emptyList()), + Arguments.of(D4, E2, Collections.emptyList()) + ); + } + + @ParameterizedTest(name = "입력받은 파일과 같은 파일인지 확인한다. 대상: ONE, 입력: {0}, 결과: {1}") + @CsvSource({"ONE, true", "TWO, false"}) + void 입력받은_파일과_같은_랭크인지_확인한다(final Rank rank, final boolean result) { + // given + final Position source = Position.of(File.A, Rank.ONE); + + // expect + assertThat(source.isSameRank(rank)).isEqualTo(result); + } +} diff --git a/src/test/java/chess/domain/position/RankTest.java b/src/test/java/chess/domain/position/RankTest.java new file mode 100644 index 00000000000..83dd42ce6ed --- /dev/null +++ b/src/test/java/chess/domain/position/RankTest.java @@ -0,0 +1,50 @@ +package chess.domain.position; + +import static chess.domain.position.Rank.EIGHT; +import static chess.domain.position.Rank.FIVE; +import static chess.domain.position.Rank.FOUR; +import static chess.domain.position.Rank.ONE; +import static chess.domain.position.Rank.SEVEN; +import static chess.domain.position.Rank.SIX; +import static chess.domain.position.Rank.THREE; +import static chess.domain.position.Rank.TWO; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class RankTest { + + @ParameterizedTest(name = "두 랭크 사이의 랭크들을 반환한다. 시작: {0}, 도착: {1}, 결과: {2}") + @MethodSource("betweenSource") + void 두_랭크_사이의_랭크를_반환한다(final Rank source, final Rank target, final List result) { + // expect + assertThat(source.between(target)).containsExactlyElementsOf(result); + } + + static Stream betweenSource() { + return Stream.of( + Arguments.of(ONE, FOUR, List.of(TWO, THREE)), + Arguments.of(ONE, TWO, Collections.emptyList()), + Arguments.of(ONE, ONE, Collections.emptyList()), + Arguments.of(TWO, ONE, Collections.emptyList()), + Arguments.of(EIGHT, FIVE, List.of(SEVEN, SIX)) + ); + } + + @ParameterizedTest(name = "입력받은 랭크와의 차이를 반환한다. 시작: {0}, 도착: {1}, 결과: {2}") + @CsvSource({"ONE, SEVEN, -6", "TWO, TWO, 0", "SEVEN, FOUR, 3"}) + void 입력받은_랭크와의_차이를_반환한다(final Rank source, final Rank target, final int result) { + // expect + assertThat(source.calculateGap(target)).isEqualTo(result); + } +} diff --git a/src/test/java/chess/domain/room/RoomTest.java b/src/test/java/chess/domain/room/RoomTest.java new file mode 100644 index 00000000000..7bd47def667 --- /dev/null +++ b/src/test/java/chess/domain/room/RoomTest.java @@ -0,0 +1,34 @@ +package chess.domain.room; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class RoomTest { + + @Test + void 방이_정상_생성된다() { + // given + final Room room = new Room(1, "방1", 1); + + // expect + assertThat(room.getName()).isEqualTo("방1"); + } + + @Test + void 방이_해당_사용자가_생성했는지_확인한다() { + // given + final Room room = new Room(1, "방1", 1); + final int userId = 2; + + // when + final boolean result = room.isNotCreatedBy(userId); + + // expect + assertThat(result).isTrue(); + } +} diff --git a/src/test/java/chess/domain/user/UserTest.java b/src/test/java/chess/domain/user/UserTest.java new file mode 100644 index 00000000000..85072d25592 --- /dev/null +++ b/src/test/java/chess/domain/user/UserTest.java @@ -0,0 +1,21 @@ +package chess.domain.user; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class UserTest { + + @Test + void 사용자가_정상_생성된다() { + // given + final User user = new User(1, "가비"); + + // expect + assertThat(user.getName()).isEqualTo("가비"); + } +} diff --git a/src/test/java/chess/fixture/PositionFixture.java b/src/test/java/chess/fixture/PositionFixture.java new file mode 100644 index 00000000000..d38ef5c87a6 --- /dev/null +++ b/src/test/java/chess/fixture/PositionFixture.java @@ -0,0 +1,94 @@ +package chess.fixture; + +import static chess.domain.position.File.A; +import static chess.domain.position.File.B; +import static chess.domain.position.File.C; +import static chess.domain.position.File.D; +import static chess.domain.position.File.E; +import static chess.domain.position.File.F; +import static chess.domain.position.File.G; +import static chess.domain.position.File.H; +import static chess.domain.position.Rank.EIGHT; +import static chess.domain.position.Rank.FIVE; +import static chess.domain.position.Rank.FOUR; +import static chess.domain.position.Rank.ONE; +import static chess.domain.position.Rank.SEVEN; +import static chess.domain.position.Rank.SIX; +import static chess.domain.position.Rank.THREE; +import static chess.domain.position.Rank.TWO; + +import chess.domain.position.Position; + +public class PositionFixture { + public static final Position A1 = Position.of(A, ONE); + public static final Position A2 = Position.of(A, TWO); + public static final Position A3 = Position.of(A, THREE); + public static final Position A4 = Position.of(A, FOUR); + public static final Position A5 = Position.of(A, FIVE); + public static final Position A6 = Position.of(A, SIX); + public static final Position A7 = Position.of(A, SEVEN); + public static final Position A8 = Position.of(A, EIGHT); + + public static final Position B1 = Position.of(B, ONE); + public static final Position B2 = Position.of(B, TWO); + public static final Position B3 = Position.of(B, THREE); + public static final Position B4 = Position.of(B, FOUR); + public static final Position B5 = Position.of(B, FIVE); + public static final Position B6 = Position.of(B, SIX); + public static final Position B7 = Position.of(B, SEVEN); + public static final Position B8 = Position.of(B, EIGHT); + + public static final Position C1 = Position.of(C, ONE); + public static final Position C2 = Position.of(C, TWO); + public static final Position C3 = Position.of(C, THREE); + public static final Position C4 = Position.of(C, FOUR); + public static final Position C5 = Position.of(C, FIVE); + public static final Position C6 = Position.of(C, SIX); + public static final Position C7 = Position.of(C, SEVEN); + public static final Position C8 = Position.of(C, EIGHT); + + public static final Position D1 = Position.of(D, ONE); + public static final Position D2 = Position.of(D, TWO); + public static final Position D3 = Position.of(D, THREE); + public static final Position D4 = Position.of(D, FOUR); + public static final Position D5 = Position.of(D, FIVE); + public static final Position D6 = Position.of(D, SIX); + public static final Position D7 = Position.of(D, SEVEN); + public static final Position D8 = Position.of(D, EIGHT); + + public static final Position E1 = Position.of(E, ONE); + public static final Position E2 = Position.of(E, TWO); + public static final Position E3 = Position.of(E, THREE); + public static final Position E4 = Position.of(E, FOUR); + public static final Position E5 = Position.of(E, FIVE); + public static final Position E6 = Position.of(E, SIX); + public static final Position E7 = Position.of(E, SEVEN); + public static final Position E8 = Position.of(E, EIGHT); + + public static final Position F1 = Position.of(F, ONE); + public static final Position F2 = Position.of(F, TWO); + public static final Position F3 = Position.of(F, THREE); + public static final Position F4 = Position.of(F, FOUR); + public static final Position F5 = Position.of(F, FIVE); + public static final Position F6 = Position.of(F, SIX); + public static final Position F7 = Position.of(F, SEVEN); + public static final Position F8 = Position.of(F, EIGHT); + + public static final Position G1 = Position.of(G, ONE); + public static final Position G2 = Position.of(G, TWO); + public static final Position G3 = Position.of(G, THREE); + public static final Position G4 = Position.of(G, FOUR); + public static final Position G5 = Position.of(G, FIVE); + public static final Position G6 = Position.of(G, SIX); + public static final Position G7 = Position.of(G, SEVEN); + public static final Position G8 = Position.of(G, EIGHT); + + public static final Position H1 = Position.of(H, ONE); + public static final Position H2 = Position.of(H, TWO); + public static final Position H3 = Position.of(H, THREE); + public static final Position H4 = Position.of(H, FOUR); + public static final Position H5 = Position.of(H, FIVE); + public static final Position H6 = Position.of(H, SIX); + public static final Position H7 = Position.of(H, SEVEN); + public static final Position H8 = Position.of(H, EIGHT); +} diff --git a/src/test/java/chess/repository/ChessJdbcDaoTest.java b/src/test/java/chess/repository/ChessJdbcDaoTest.java new file mode 100644 index 00000000000..7a9b6262510 --- /dev/null +++ b/src/test/java/chess/repository/ChessJdbcDaoTest.java @@ -0,0 +1,74 @@ +package chess.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import chess.db.JdbcTemplate; +import chess.db.TestConnectionPool; +import chess.dto.MoveDto; +import java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ChessJdbcDaoTest { + private static TestConnectionPool connectionPool; + private static JdbcTemplate jdbcTemplate; + private static GameDao gameDao; + + @BeforeAll + static void beforeAll() { + connectionPool = new TestConnectionPool(); + jdbcTemplate = new JdbcTemplate(connectionPool); + gameDao = new GameJdbcDao(jdbcTemplate); + final String query = "CREATE TABLE IF NOT EXISTS move (" + + " id INT NOT NULL AUTO_INCREMENT PRIMARY KEY," + + " source VARCHAR(2) NOT NULL," + + " target VARCHAR(2) NOT NULL," + + " room_id INT NOT NULL" + + ");"; + jdbcTemplate.executeUpdate(query); + } + + @BeforeEach + void setUp() { + jdbcTemplate.executeUpdate("DELETE FROM move"); + } + + @AfterAll + static void afterAll() { + connectionPool.closeConnection(); + } + + @Test + void 기물의_이동과_방_아이디를_받아_기물의_움직임을_저장한다() { + // given + final MoveDto moveDto = new MoveDto("d2", "d4"); + final int roomId = 1; + + // when + gameDao.save(moveDto, roomId); + + // then + final List result = gameDao.findAllByRoomId(roomId); + assertThat(result.size()).isOne(); + } + + @Test + void 방_아이디를_입력받아_모든_기보를_조회한다() { + // given + final int roomId = 1; + gameDao.save(new MoveDto("d2", "d4"), roomId); + gameDao.save(new MoveDto("d7", "d5"), roomId); + + // when + final List result = gameDao.findAllByRoomId(roomId); + + // then + assertThat(result.size()).isEqualTo(2); + } +} diff --git a/src/test/java/chess/repository/RoomJdbcDaoTest.java b/src/test/java/chess/repository/RoomJdbcDaoTest.java new file mode 100644 index 00000000000..fba48fa68f3 --- /dev/null +++ b/src/test/java/chess/repository/RoomJdbcDaoTest.java @@ -0,0 +1,90 @@ +package chess.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import chess.db.JdbcTemplate; +import chess.db.TestConnectionPool; +import chess.domain.room.Room; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class RoomJdbcDaoTest { + private static TestConnectionPool connectionPool; + private static JdbcTemplate jdbcTemplate; + private static RoomDao roomDao; + + @BeforeAll + static void beforeAll() { + connectionPool = new TestConnectionPool(); + jdbcTemplate = new JdbcTemplate(connectionPool); + roomDao = new RoomJdbcDao(jdbcTemplate); + final String query = "CREATE TABLE IF NOT EXISTS room (" + + " id int PRIMARY KEY AUTO_INCREMENT," + + " name varchar(255)," + + " user_id int" + + ");"; + jdbcTemplate.executeUpdate(query); + } + + @BeforeEach + void setUp() { + jdbcTemplate.executeUpdate("DELETE FROM room"); + } + + @AfterAll + static void afterAll() { + connectionPool.closeConnection(); + } + + @Test + void 사용자의_아이디와_방_이름을_입력받아_저장한다() { + // given + final String roomName = "방1"; + final int userId = 1; + + // when + roomDao.save(roomName, userId); + + // then + final List result = roomDao.findAllByUserId(userId); + assertThat(result).hasSize(1); + } + + @Test + void 사용자의_아이디를_받아_참가한_방_목록을_조회한다() { + // given + final int userId = 1; + roomDao.save("방1", userId); + roomDao.save("방2", userId); + + // when + final List result = roomDao.findAllByUserId(userId); + + // then + assertThat(result).hasSize(2); + } + + @Test + void 방_아이디를_받아_방을_조회한다() { + // given + final int userId = 1; + roomDao.save("방1", userId); + final List rooms = roomDao.findAllByUserId(userId); + final int roomId = rooms.get(0).getId(); + + // when + final Optional room = roomDao.findById(roomId); + + // then + final Room result = room.get(); + assertThat(result.getName()).isEqualTo("방1"); + } +} diff --git a/src/test/java/chess/repository/UserJdbcDaoTest.java b/src/test/java/chess/repository/UserJdbcDaoTest.java new file mode 100644 index 00000000000..bbf42faf223 --- /dev/null +++ b/src/test/java/chess/repository/UserJdbcDaoTest.java @@ -0,0 +1,76 @@ +package chess.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import chess.db.JdbcTemplate; +import chess.db.TestConnectionPool; +import chess.domain.user.User; +import java.util.Optional; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class UserJdbcDaoTest { + private static TestConnectionPool connectionPool; + private static JdbcTemplate jdbcTemplate; + private static UserDao userDao; + + @BeforeAll + static void beforeAll() { + connectionPool = new TestConnectionPool(); + jdbcTemplate = new JdbcTemplate(connectionPool); + userDao = new UserJdbcDao(jdbcTemplate); + final String query = "CREATE TABLE IF NOT EXISTS user (" + + " id int PRIMARY KEY AUTO_INCREMENT," + + " name varchar(255)" + + ");"; + jdbcTemplate.executeUpdate(query); + } + + @BeforeEach + void setUp() { + jdbcTemplate.executeUpdate("DELETE FROM user"); + } + + @AfterAll + static void afterAll() { + connectionPool.closeConnection(); + } + + @Test + void 사용자의_이름을_받아_사용자를_저장한다() { + // given + final String name = "herb"; + + // when + userDao.save(name); + + // then + final Optional user = userDao.findByName(name); + final User result = user.get(); + assertThat(result.getName()).isEqualTo("herb"); + } + + @Test + void 사용자의_이름을_받아_사용자를_조회한다() { + // given + final String name = "herb"; + userDao.save(name); + + // when + final Optional user = userDao.findByName(name); + + // then + final User result = user.get(); + assertAll( + () -> assertThat(result.getId()).isPositive(), + () -> assertThat(result.getName()).isEqualTo("herb") + ); + } +} diff --git a/src/test/java/chess/service/GameServiceTest.java b/src/test/java/chess/service/GameServiceTest.java new file mode 100644 index 00000000000..6b8764904e4 --- /dev/null +++ b/src/test/java/chess/service/GameServiceTest.java @@ -0,0 +1,171 @@ +package chess.service; + +import static chess.domain.piece.PieceType.BISHOP; +import static chess.domain.piece.PieceType.EMPTY; +import static chess.domain.piece.PieceType.KING; +import static chess.domain.piece.PieceType.KNIGHT; +import static chess.domain.piece.PieceType.PAWN; +import static chess.domain.piece.PieceType.QUEEN; +import static chess.domain.piece.PieceType.ROOK; +import static chess.fixture.PositionFixture.E4; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import chess.domain.piece.Color; +import chess.domain.piece.Pawn; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceType; +import chess.domain.position.File; +import chess.domain.position.Position; +import chess.domain.position.Rank; +import chess.dto.MoveDto; +import chess.repository.GameDao; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class GameServiceTest { + + private GameDao mockGameDao; + + @BeforeEach + void setUp() { + mockGameDao = new GameDaoStub(); + } + + private static List toPieceTypes(final Map board) { + return Arrays.stream(Rank.values()) + .sorted(Comparator.reverseOrder()) + .flatMap(file -> Arrays.stream(File.values()).map(rank -> Position.of(rank, file))) + .map(board::get) + .map(Piece::type) + .collect(toList()); + } + + @Test + void 체스_게임을_생성한다() { + // given + final GameService gameService = new GameService(mockGameDao); + final int roomId = 1; + + // when + gameService.initialize(roomId); + + // then + final List result = toPieceTypes(gameService.getResult(roomId).getBoard()); + assertThat(result).containsExactly( + ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK, + PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, + ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK + ); + } + + @Test + void 기물을_움직인다() { + // given + final GameService gameService = new GameService(mockGameDao); + final int roomId = 1; + gameService.initialize(roomId); + + // when + gameService.move(new MoveDto("e2", "e4"), roomId); + + // then + final Map board = gameService.getResult(roomId).getBoard(); + assertThat(board.get(E4)).isEqualTo(Pawn.from(Color.WHITE)); + } + + @Test + void 루이로페즈_모던_슈타이니츠_바리에이션_으로_게임을_진행한다() { + // given + final GameService gameService = new GameService(mockGameDao); + final int roomId = 1; + gameService.initialize(roomId); + + // when + gameService.move(new MoveDto("e2", "e4"), roomId); + gameService.move(new MoveDto("e7", "e5"), roomId); + gameService.move(new MoveDto("g1", "f3"), roomId); + gameService.move(new MoveDto("b8", "c6"), roomId); + gameService.move(new MoveDto("f1", "b5"), roomId); + gameService.move(new MoveDto("a7", "a6"), roomId); + gameService.move(new MoveDto("b5", "a4"), roomId); + gameService.move(new MoveDto("d7", "d6"), roomId); + + // then + final List result = toPieceTypes(gameService.getResult(roomId).getBoard()); + assertThat(result).containsExactly( + ROOK, EMPTY, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK, + EMPTY, PAWN, PAWN, EMPTY, EMPTY, PAWN, PAWN, PAWN, + PAWN, EMPTY, KNIGHT, PAWN, EMPTY, EMPTY, EMPTY, EMPTY, + EMPTY, EMPTY, EMPTY, EMPTY, PAWN, EMPTY, EMPTY, EMPTY, + BISHOP, EMPTY, EMPTY, EMPTY, PAWN, EMPTY, EMPTY, EMPTY, + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, KNIGHT, EMPTY, EMPTY, + PAWN, PAWN, PAWN, PAWN, EMPTY, PAWN, PAWN, PAWN, + ROOK, KNIGHT, BISHOP, QUEEN, KING, EMPTY, EMPTY, ROOK + ); + } + + @Test + void 왕이_잡히는_경우_게임이_종료된다() { + // given + final GameService gameService = new GameService(mockGameDao); + final int roomId = 1; + gameService.initialize(roomId); + gameService.move(new MoveDto("e2", "e4"), roomId); + gameService.move(new MoveDto("e7", "e5"), roomId); + gameService.move(new MoveDto("d1", "h5"), roomId); + gameService.move(new MoveDto("f7", "f5"), roomId); + + // when + gameService.move(new MoveDto("h5", "e8"), roomId); + + // then + assertThat(gameService.isGameOver(roomId)).isTrue(); + } + + @Test + void 보드를_삭제한다() { + // given + final GameService gameService = new GameService(mockGameDao); + final int roomId = 1; + gameService.initialize(roomId); + gameService.move(new MoveDto("e2", "e4"), roomId); + gameService.move(new MoveDto("e7", "e5"), roomId); + gameService.move(new MoveDto("d1", "h5"), roomId); + gameService.move(new MoveDto("f7", "f5"), roomId); + gameService.move(new MoveDto("h5", "e8"), roomId); + + // when + gameService.removeBoard(roomId); + + // then + assertThatThrownBy(() -> gameService.move(new MoveDto("h5", "e8"), roomId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("게임을 찾을 수 없습니다."); + } + + private class GameDaoStub implements GameDao { + @Override + public void save(final MoveDto moveDto, final int roomId) { + } + + @Override + public List findAllByRoomId(final int roomId) { + return List.of(); + } + } +} diff --git a/src/test/java/chess/service/RoomServiceTest.java b/src/test/java/chess/service/RoomServiceTest.java new file mode 100644 index 00000000000..61a96cffafe --- /dev/null +++ b/src/test/java/chess/service/RoomServiceTest.java @@ -0,0 +1,124 @@ +package chess.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import chess.domain.room.Room; +import chess.repository.RoomDao; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class RoomServiceTest { + private RoomDao mockRoomDao; + private RoomService roomService; + + @BeforeEach + void setUp() { + mockRoomDao = new RoomDaoStub(); + roomService = new RoomService(mockRoomDao); + } + + @Test + void 게임을_생성한다() { + // given + final String roomName = "방1"; + final int userId = 1; + + // when + roomService.save(roomName, userId); + + // then + final Room room = roomService.findById(1, userId); + assertAll( + () -> assertThat(room.getId()).isEqualTo(1), + () -> assertThat(room.getName()).isEqualTo("방1") + ); + } + + @Test + void 입력한_사용자_아이디에_대한_모든_게임을_조회한다() { + // given + final int userId = 1; + roomService.save("방1", userId); + roomService.save("방2", userId); + + // when + final List result = roomService.findAllByUserId(userId); + + // then + assertThat(result).hasSize(2); + } + + @Test + void 단일_조회시_방이_존재하지_않는_경우_예외를_던진다() { + // given + final int userId = 1; + + // expect + assertThatThrownBy(() -> roomService.findById(1, userId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("아이디에 해당하는 방이 없습니다."); + } + + @Test + void 단일_조회시_방을_생성한_사람이_아닌_다른사람의_아이디를_입력하는_경우_예외를_던진다() { + // given + final int userId = 1; + roomService.save("방1", userId); + + // expect + assertThatThrownBy(() -> roomService.findById(1, 9999)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("방의 주인이 아닙니다."); + } + + @Test + void 방_번호와_사용자_아이디를_입력받아_게임을_반환한다() { + // given + final int userId = 1; + roomService.save("방1", userId); + + // when + final Room room = roomService.findById(1, userId); + + // then + assertAll( + () -> assertThat(room.getId()).isEqualTo(1), + () -> assertThat(room.getName()).isEqualTo("방1") + ); + } + + private class RoomDaoStub implements RoomDao { + private final List rooms = new ArrayList<>(); + private int index = 0; + + @Override + public void save(final String roomName, final int userId) { + final Room room = new Room(++index, roomName, userId); + rooms.add(room); + } + + @Override + public List findAllByUserId(final int userId) { + return rooms.stream() + .filter(room -> room.getUserId() == userId) + .collect(Collectors.toList()); + } + + @Override + public Optional findById(final int roomId) { + return rooms.stream() + .filter(room -> room.getId() == roomId) + .findFirst(); + } + } +} diff --git a/src/test/java/chess/service/UserServiceTest.java b/src/test/java/chess/service/UserServiceTest.java new file mode 100644 index 00000000000..2eaf77d92a1 --- /dev/null +++ b/src/test/java/chess/service/UserServiceTest.java @@ -0,0 +1,98 @@ +package chess.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import chess.domain.user.User; +import chess.repository.UserDao; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class UserServiceTest { + + private UserDao mockUserDao; + private UserService userService; + + @BeforeEach + void setUp() { + mockUserDao = new UserDaoStub(); + userService = new UserService(mockUserDao); + } + + @Test + void 이미_등록된_이름을_입력받아_회원가입을_진행하는_경우_예외를_던진다() { + // given + final String name = "herb"; + userService.save(name); + + // expect + assertThatThrownBy(() -> userService.save(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미 등록된 이름입니다."); + } + + @Test + void 사용자의_이름을_받아_사용자를_저장한다() { + // given + final String name = "herb"; + + // when + userService.save(name); + + // then + final User result = userService.findByName(name); + assertThat(result.getName()).isEqualTo("herb"); + } + + @Test + void 존재하지_않는_사용자를_조회하는_경우_예외를_던진다() { + // given + final String name = "herb"; + + // expect + assertThatThrownBy(() -> userService.findByName(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("해당 이름을 가진 유저가 없습니다."); + } + + @Test + void 사용자의_이름을_받아_사용자를_조회한다() { + // given + final String name = "herb"; + userService.save(name); + + // when + final User result = userService.findByName(name); + + // then + assertAll( + () -> assertThat(result.getId()).isPositive(), + () -> assertThat(result.getName()).isEqualTo("herb") + ); + } + + private class UserDaoStub implements UserDao { + private final List users = new ArrayList<>(); + private int index = 0; + + @Override + public void save(final String name) { + users.add(new User(++index, name)); + } + + @Override + public Optional findByName(final String name) { + return users.stream() + .filter(user -> user.getName().equals(name)) + .findFirst(); + } + } +} diff --git a/src/test/java/chess/view/BoardConverterTest.java b/src/test/java/chess/view/BoardConverterTest.java new file mode 100644 index 00000000000..5b7666444d4 --- /dev/null +++ b/src/test/java/chess/view/BoardConverterTest.java @@ -0,0 +1,39 @@ +package chess.view; + +import static org.assertj.core.api.Assertions.assertThat; + +import chess.domain.board.BoardGenerator; +import chess.domain.piece.Piece; +import chess.domain.position.Position; +import chess.view.output.BoardConverter; +import java.util.Map; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class BoardConverterTest { + + @Test + void 입력받은_보드를_문자열로_변환한다() { + // given + final Map board = BoardGenerator.generate(); + final String nextLine = System.lineSeparator(); + + // when + final String result = BoardConverter.convert(board); + + // then + assertThat(result).isEqualTo( + "RNBQKBNR" + nextLine + + "PPPPPPPP" + nextLine + + "........" + nextLine + + "........" + nextLine + + "........" + nextLine + + "........" + nextLine + + "pppppppp" + nextLine + + "rnbqkbnr" + nextLine + ); + } +}