diff --git a/README.md b/README.md index 8102f91c870..c8d1d264f7b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,165 @@ # java-chess -체스 미션 저장소 +## Step1, 2 기능 요구사항 -## 우아한테크코스 코드리뷰 +### 기물 공통 -- [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) +- [x] 움직이지 않을 경우 예외가 발생한다. + +### Pawn + +- [x] 앞에 다른 기물이 없을 때 한 칸 이동한다. + - [x] **[예외]** 앞에 다른 기물이 있을 때 + - [x] **[예외]** 두 칸 이상 이동할 때 +- [x] 처음 위치일 때 앞으로 두 칸 이동할 수 있다. + - [x] **[예외]** 앞에 다른 기물이 있을 때 (아군, 적군) + - [x] **[예외]** 처음 위치가 아닐 때 + - 블랙 : 7 rank + - 화이트 : 2 rank +- [x] 대각선에 상대 기물이 있으면 대각선으로 이동한다. + - [x] **[예외]** 대각선에 상대 기물이 없을 때 + - [x] **[예외]** 두 칸 이상 이동할 때 +- [x] 뒤로 이동할 수 없다. + +### Knight + +- [x] L자로 이동한다. + - [x] **[예외]** L자로 이동하지 않을 때 +- [x] 이동 경로에 다른 기물이 있어도 뛰어 넘을 수 있다. +- [x] 도착지에 상대 기물이 있으면 잡을 수 있다. + - [x] **[예외]** 도착지에 아군 기물이 있을 때 + +### Bishop + +- [x] 대각선으로 이동할 수 있다. + - [x] **[예외]** 대각선으로 이동하지 않을 때 +- [x] 이동 경로에 다른 기물이 있으면 이동할 수 없다. +- [x] 도착지에 상대 기물이 있으면 잡을 수 있다. + - [x] **[예외]** 도착지에 아군 기물이 있을 때 + +### Rook + +- [x] 수직, 수평으로 이동할 수 있다. + - [x] **[예외]** 수직, 수평으로 이동하지 않을 때 +- [x] 이동 경로에 다른 기물이 있으면 이동할 수 없다. +- [x] 도착지에 상대 기물이 있으면 잡을 수 있다. + - [x] **[예외]** 도착지에 아군 기물이 있을 때 + +### Queen + +- [x] 수평, 수직, 대각선으로 이동할 수 있다. + - [x] **[예외]** 수평, 수직, 대각선으로 이동하지 않을 때 +- [x] 이동 경로에 다른 기물이 있으면 이동할 수 없다. +- [x] 도착지에 상대 기물이 있으면 잡을 수 있다. + - [x] **[예외]** 도착지에 아군 기물이 있을 때 + +### King + +- [x] 수평, 수직, 대각선으로 한 칸 이동할 수 있다. + - [x] **[예외]** 수평, 수직, 대각선으로 이동하지 않을 때 + - [x] **[예외]** 두 칸 이상 이동할 때 +- [x] 도착지에 상대 기물이 있으면 잡을 수 있다. + - [x] **[예외]** 도착지에 아군 기물이 있을 때 + +### 체스판 + +- [x] 포지션이 체스판 범위 내인지 확인한다. + - [x] **[예외]** 범위를 벗어났을 때 +- [x] 기물의 이동 경로에 다른 기물이 있는지 확인한다. + - [x] **[예외]** 경로에 다른 기물이 있을 때 +- [x] 자신의 턴인지 확인한다. + - [x] **[예외]** 상대의 턴일 때 +- [x] 기물을 이동한다. + +### 체스판 팩토리 + +- [x] 기물을 생성한다. +- [x] 체스판을 생성한다. + +### 커맨드 + +- [x] start는 게임을 시작한다. +- [x] end는 게임을 종료한다. +- [x] move는 기물을 이동한다. + +### 입력 + +- [x] 커맨드를 입력받는다. + +### 출력 + +- [x] 체스판을 출력한다. + - [x] 아래(백, 소문자), 위(흑, 대문자) + +### 추가 룰 + +- [ ] 프로모션 : 폰이 상대 마지막 진영까지 도달하면 다른 기물로 변경할 수 있다. +- [ ] 앙파상 +- [ ] 캐슬링 + +## Step3, 4 기능 요구사항 + +### 체스 게임 + +- 게임이 시작될 때 체스판을 만든다. + - DB에 저장된 데이터가 존재하면 불러온다. + - DB에 저장된 데이터가 없으면 체스판을 새로 만든다. +- 체스 게임을 진행한다. +- 게임 점수를 계산한다. +- 게임을 종료하면 데이터를 갱신한다. + - 킹이 죽지 않았으면 현재 체스판 데이터를 모두 저장한다. + - 킹이 죽으면 모든 테이터를 삭제한다. + + +### 점수 계산 + +- [x] 현재까지 남아 있는 말에 따라 점수를 계산한다. + - [x] 각 팀의 점수를 따로 계산한다. +- [x] queen은 9점, rook은 5점, bishop은 3점, knight는 2.5점이다. +- [x] pawn의 기본 점수는 1점이다. + - [x] 같은 세로줄에 같은 색의 폰이 있는 경우 0.5점을 준다. +- [x] 킹이 없다면 0점이다. + +### 커맨드 + +- start + - [x] 체스판을 출력한다. +- move + - [x] 말을 움직인다. + - [x] 체스판을 출력한다. +- status + - [x] 각 팀의 점수를 출력한다. +- end + - [x] 게임을 종료한다. + - [x] 각 팀의 점수를 출력한다. +- (추가) 킹이 죽으면 게임은 자동으로 종료된다. + +### DB CRUD 기능 + +- [x] 기존 게임 데이터가 존재하는지 여부 조회 +- [x] 기존 게임 데이터를 불러오기 +- [x] 게임 데이터 갱신 (피스들, 턴) + - [x] 이전 게임 데이터 삭제 + - [x] 현재 게임 데이터 저장 + +### 추가 기능 1: 체스 게임방을 만들고 체스 게임방에 입장할 수 있는 기능을 추가한다. + +- [x] 존재하는 모든 게임방 번호를 조회한다. + - [x] 종료된 게임은 제외한다. +- [x] 입장할 게임방 번호를 입력 받는다. + - [x] new를 입력하면 새로운 방을 생성한다. + - **[예외]** 종료된 게임방 번호 + - **[예외]** 존재하지 않는 게임방 번호 +- [x] 게임을 생성한다. (턴, 피스들) + - [x] 기존 데이터를 불러오기 + - [x] 새로 생성하기 +- [x] end를 입력하면 게임 데이터를 저장한다. + +### 추가 기능 2: 사용자별로 체스 게임 기록을 관리할 수 있다. + +- [x] 사용자 이름을 입력 받는다. + - **[예외]** 사용자 이름은 4~10자 사이여야 한다. +- [x] DB에서 사용자 정보를 불러온다. + - [x] 존재하지 않는 사용자라면 새로 추가한다. +- [x] 사용자의 게임 기록을 불러온다. +- 이후는 기존 어플리케이션 흐름대로 진행 diff --git a/build.gradle b/build.gradle index 3697236c6fb..8c605072e94 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ dependencies { testImplementation platform('org.assertj:assertj-bom:3.25.1') testImplementation('org.junit.jupiter:junit-jupiter') testImplementation('org.assertj:assertj-core') + runtimeOnly("com.mysql:mysql-connector-j:8.3.0") } java { diff --git a/docker/db/mysql/init/schema.sql b/docker/db/mysql/init/schema.sql new file mode 100644 index 00000000000..a356fffc983 --- /dev/null +++ b/docker/db/mysql/init/schema.sql @@ -0,0 +1,32 @@ +create table users +( + username varchar(16), + primary key (username) +); + +create table rooms +( + user varchar(16), + room_id int, + primary key (room_id), + foreign key (user) references users (username) +); + +create table game_states +( + game_id int, + state varchar(8) not null, + primary key (game_id), + foreign key (game_id) references rooms (room_id) +); + +create table pieces +( + game_id int, + board_file varchar(1) not null, + board_rank varchar(1) not null, + color varchar(8) not null, + type varchar(8) not null, + primary key (game_id, board_file, board_rank), + foreign key (game_id) references rooms (room_id) +); diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000000..558a1d5a53f --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.9" +services: + db: + image: mysql:8.0.28 + platform: linux/x86_64 + restart: always + ports: + - "13306:3306" + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: chess + MYSQL_USER: user + MYSQL_PASSWORD: password + TZ: Asia/Seoul + volumes: + - ./db/mysql/data:/var/lib/mysql + - ./db/mysql/config:/etc/mysql/conf.d + - ./db/mysql/init:/docker-entrypoint-initdb.d diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000000..1499d1f1f82 --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,8 @@ +import controller.MainController; + +public class Application { + public static void main(String[] args) { + MainController mainController = new MainController(); + mainController.run(); + } +} diff --git a/src/main/java/controller/MainController.java b/src/main/java/controller/MainController.java new file mode 100644 index 00000000000..f37658d9995 --- /dev/null +++ b/src/main/java/controller/MainController.java @@ -0,0 +1,32 @@ +package controller; + +import controller.game.ChessGameController; +import controller.room.GameRoomController; +import controller.user.UserController; +import database.dao.GameStateDaoImpl; +import database.dao.PieceDaoImpl; +import database.dao.RoomDaoImpl; +import database.dao.UserDaoImpl; +import dto.RoomDto; +import dto.UserDto; +import service.ChessGameService; +import service.GameRoomService; +import service.UserService; + +public class MainController { + private final GameRoomController gameRoomController = new GameRoomController(new GameRoomService(new RoomDaoImpl())); + private final ChessGameController chessGameController + = new ChessGameController( + new ChessGameService( + new PieceDaoImpl(), + new GameStateDaoImpl() + ) + ); + private final UserController userController = new UserController(new UserService(new UserDaoImpl())); + + public void run() { + UserDto user = userController.loadUser(); + RoomDto room = gameRoomController.loadRoom(user); + chessGameController.run(room); + } +} diff --git a/src/main/java/controller/game/ChessGameController.java b/src/main/java/controller/game/ChessGameController.java new file mode 100644 index 00000000000..d5bedd1c854 --- /dev/null +++ b/src/main/java/controller/game/ChessGameController.java @@ -0,0 +1,35 @@ +package controller.game; + +import controller.game.command.GameCommand; +import domain.ChessGame; +import dto.RoomDto; +import service.ChessGameService; +import view.InputView; +import view.OutputView; + +public class ChessGameController { + private final ChessGameService chessGameService; + + public ChessGameController(ChessGameService chessGameService) { + this.chessGameService = chessGameService; + } + + public void run(RoomDto roomDto) { + ChessGame chessGame = chessGameService.initializeChessGame(roomDto); + OutputView.printGameGuideMessage(); + while (chessGame.isPlaying()) { + readCommandUntilValid(chessGame); + } + chessGameService.saveChessGame(chessGame, roomDto); + } + + private void readCommandUntilValid(ChessGame chessGame) { + try { + GameCommand command = InputView.readGameCommand(); + command.execute(chessGame); + } catch (Exception e) { + OutputView.printErrorMessage(e); + readCommandUntilValid(chessGame); + } + } +} diff --git a/src/main/java/controller/game/command/EndOnGameCommand.java b/src/main/java/controller/game/command/EndOnGameCommand.java new file mode 100644 index 00000000000..6c966f54f37 --- /dev/null +++ b/src/main/java/controller/game/command/EndOnGameCommand.java @@ -0,0 +1,22 @@ +package controller.game.command; + +import domain.ChessGame; + +import java.util.List; + +public class EndOnGameCommand implements GameCommand { + public EndOnGameCommand(final List arguments) { + validateArgumentSize(arguments); + } + + private void validateArgumentSize(final List arguments) { + if (!arguments.isEmpty()) { + throw new IllegalArgumentException(); + } + } + + @Override + public void execute(final ChessGame game) { + game.end(); + } +} diff --git a/src/main/java/controller/game/command/GameCommand.java b/src/main/java/controller/game/command/GameCommand.java new file mode 100644 index 00000000000..3139a3cc479 --- /dev/null +++ b/src/main/java/controller/game/command/GameCommand.java @@ -0,0 +1,7 @@ +package controller.game.command; + +import domain.ChessGame; + +public interface GameCommand { + void execute(ChessGame game); +} diff --git a/src/main/java/controller/game/command/MoveOnGameCommand.java b/src/main/java/controller/game/command/MoveOnGameCommand.java new file mode 100644 index 00000000000..ec7630be1e0 --- /dev/null +++ b/src/main/java/controller/game/command/MoveOnGameCommand.java @@ -0,0 +1,52 @@ +package controller.game.command; + +import domain.ChessGame; +import domain.board.Score; +import domain.position.Position; +import view.OutputView; + +import java.util.List; +import java.util.regex.Pattern; + +public class MoveOnGameCommand implements GameCommand { + private static final Pattern POSITION_INPUT_PATTERN = Pattern.compile("^[A-H][1-8]$"); + private static final int ARGUMENT_SIZE = 2; + + private final Position source; + private final Position target; + + public MoveOnGameCommand(final List arguments) { + validateArgumentSize(arguments); + this.source = new Position(arguments.get(0)); + this.target = new Position(arguments.get(1)); + } + + private void validateArgumentSize(final List arguments) { + if (arguments.size() != ARGUMENT_SIZE) { + throw new IllegalArgumentException(); + } + validatePositions(arguments); + } + + private void validatePositions(final List inputs) { + inputs.forEach(this::validatePositionFormat); + } + + private void validatePositionFormat(final String positionInput) { + if (!POSITION_INPUT_PATTERN.matcher(positionInput).matches()) { + throw new IllegalArgumentException(); + } + } + + @Override + public void execute(final ChessGame game) { + game.move(source, target); + OutputView.printBoard(game.getBoard()); + + if (game.isGameOver()) { + final Score score = game.getScore(); + OutputView.printScore(score); + OutputView.printWinner(score); + } + } +} diff --git a/src/main/java/controller/game/command/StartOnGameCommand.java b/src/main/java/controller/game/command/StartOnGameCommand.java new file mode 100644 index 00000000000..c7df39f16c0 --- /dev/null +++ b/src/main/java/controller/game/command/StartOnGameCommand.java @@ -0,0 +1,24 @@ +package controller.game.command; + +import domain.ChessGame; +import view.OutputView; + +import java.util.List; + +public class StartOnGameCommand implements GameCommand { + public StartOnGameCommand(final List arguments) { + validateArgumentSize(arguments); + } + + private void validateArgumentSize(final List arguments) { + if (!arguments.isEmpty()) { + throw new IllegalArgumentException(); + } + } + + @Override + public void execute(final ChessGame game) { + game.start(); + OutputView.printBoard(game.getBoard()); + } +} diff --git a/src/main/java/controller/game/command/StatusOnGameCommand.java b/src/main/java/controller/game/command/StatusOnGameCommand.java new file mode 100644 index 00000000000..46cba28f6e6 --- /dev/null +++ b/src/main/java/controller/game/command/StatusOnGameCommand.java @@ -0,0 +1,25 @@ +package controller.game.command; + +import domain.ChessGame; +import domain.board.Score; +import view.OutputView; + +import java.util.List; + +public class StatusOnGameCommand implements GameCommand { + public StatusOnGameCommand(final List arguments) { + validateArgumentSize(arguments); + } + + private void validateArgumentSize(final List arguments) { + if (!arguments.isEmpty()) { + throw new IllegalArgumentException(); + } + } + + @Override + public void execute(final ChessGame game) { + final Score score = game.getScore(); + OutputView.printScore(score); + } +} diff --git a/src/main/java/controller/room/GameRoomController.java b/src/main/java/controller/room/GameRoomController.java new file mode 100644 index 00000000000..8fb1be50fc8 --- /dev/null +++ b/src/main/java/controller/room/GameRoomController.java @@ -0,0 +1,37 @@ +package controller.room; + +import controller.room.command.RoomCommand; +import dto.RoomDto; +import dto.UserDto; +import service.GameRoomService; +import view.InputView; +import view.OutputView; + +import java.util.List; + +public class GameRoomController { + private final GameRoomService roomService; + + public GameRoomController(GameRoomService roomService) { + this.roomService = roomService; + } + + public RoomDto loadRoom(UserDto user) { + List rooms = roomService.loadActiveRoomAll(user); + OutputView.printGameRoomGuideMessage(rooms); + + RoomDto roomDto = readCommandUntilValid(user, rooms); + OutputView.printEnteringRoomMessage(roomDto); + return roomDto; + } + + private RoomDto readCommandUntilValid(UserDto user, List rooms) { + try { + RoomCommand command = InputView.readRoomCommand(rooms); + return command.execute(roomService, user); + } catch (IllegalArgumentException e) { + OutputView.printErrorMessage(e); + return readCommandUntilValid(user, rooms); + } + } +} diff --git a/src/main/java/controller/room/command/NewRoomOnRoomCommand.java b/src/main/java/controller/room/command/NewRoomOnRoomCommand.java new file mode 100644 index 00000000000..4c4344a545e --- /dev/null +++ b/src/main/java/controller/room/command/NewRoomOnRoomCommand.java @@ -0,0 +1,24 @@ +package controller.room.command; + +import dto.RoomDto; +import dto.UserDto; +import service.GameRoomService; + +import java.util.List; + +public class NewRoomOnRoomCommand implements RoomCommand { + public NewRoomOnRoomCommand(final List arguments) { + validateArgumentSize(arguments); + } + + private void validateArgumentSize(final List arguments) { + if (!arguments.isEmpty()) { + throw new IllegalArgumentException(); + } + } + + @Override + public RoomDto execute(final GameRoomService gameRoomService, final UserDto user) { + return gameRoomService.createNewRoom(user); + } +} diff --git a/src/main/java/controller/room/command/RoomCommand.java b/src/main/java/controller/room/command/RoomCommand.java new file mode 100644 index 00000000000..d6aed0280ad --- /dev/null +++ b/src/main/java/controller/room/command/RoomCommand.java @@ -0,0 +1,9 @@ +package controller.room.command; + +import dto.RoomDto; +import dto.UserDto; +import service.GameRoomService; + +public interface RoomCommand { + RoomDto execute(GameRoomService gameRoomService, UserDto user); +} diff --git a/src/main/java/controller/room/command/SelectRoomOnRoomCommand.java b/src/main/java/controller/room/command/SelectRoomOnRoomCommand.java new file mode 100644 index 00000000000..3db83c2ddb8 --- /dev/null +++ b/src/main/java/controller/room/command/SelectRoomOnRoomCommand.java @@ -0,0 +1,47 @@ +package controller.room.command; + +import dto.RoomDto; +import dto.UserDto; +import service.GameRoomService; + +import java.util.List; + +public class SelectRoomOnRoomCommand implements RoomCommand { + private static final int ARGUMENT_SIZE = 1; + + private final String roomId; + + public SelectRoomOnRoomCommand(final List arguments, final List validRooms) { + validateArgumentSize(arguments); + validateRoomIdFormat(arguments.get(0)); + validateRoomIdRunning(validRooms, arguments.get(0)); + this.roomId = arguments.get(0); + } + + private void validateArgumentSize(final List arguments) { + if (arguments.size() != ARGUMENT_SIZE) { + throw new IllegalArgumentException(); + } + } + + private void validateRoomIdFormat(final String input) { + try { + Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(); + } + } + + private void validateRoomIdRunning(final List validRooms, final String input) { + boolean isRunningRoomNotFound = validRooms.stream() + .noneMatch(room -> room.roomId() == Integer.parseInt(input)); + if (isRunningRoomNotFound) { + throw new IllegalArgumentException(); + } + } + + @Override + public RoomDto execute(final GameRoomService gameRoomService, final UserDto user) { + return gameRoomService.findRoomById(roomId); + } +} diff --git a/src/main/java/controller/user/UserController.java b/src/main/java/controller/user/UserController.java new file mode 100644 index 00000000000..7a2bbc24818 --- /dev/null +++ b/src/main/java/controller/user/UserController.java @@ -0,0 +1,32 @@ +package controller.user; + +import controller.user.command.UserCommand; +import dto.UserDto; +import service.UserService; +import view.InputView; +import view.OutputView; + +public class UserController { + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + public UserDto loadUser() { + OutputView.printUserNameInputMessage(); + UserDto user = readCommandUntilValid(); + OutputView.printUserNameMessage(user); + return user; + } + + private UserDto readCommandUntilValid() { + try { + UserCommand command = InputView.readUserCommand(); + return command.execute(userService); + } catch (IllegalArgumentException e) { + OutputView.printErrorMessage(e); + return readCommandUntilValid(); + } + } +} diff --git a/src/main/java/controller/user/command/FindUserOnDemand.java b/src/main/java/controller/user/command/FindUserOnDemand.java new file mode 100644 index 00000000000..1a82a0e8765 --- /dev/null +++ b/src/main/java/controller/user/command/FindUserOnDemand.java @@ -0,0 +1,37 @@ +package controller.user.command; + +import dto.UserDto; +import service.UserService; + +import java.util.List; + +public class FindUserOnDemand implements UserCommand { + private static final int ARGUMENT_SIZE = 1; + private static final int MINIMUM_NAME_LENGTH = 4; + private static final int MAXIMUM_NAME_LENGTH = 10; + + private final String username; + + public FindUserOnDemand(final List arguments) { + validateArgumentSize(arguments); + validateUsernameFormat(arguments.get(0)); + this.username = arguments.get(0); + } + + private void validateArgumentSize(final List arguments) { + if (arguments.size() != ARGUMENT_SIZE) { + throw new IllegalArgumentException(); + } + } + + private void validateUsernameFormat(final String input) { + if (input.length() < MINIMUM_NAME_LENGTH || input.length() > MAXIMUM_NAME_LENGTH) { + throw new IllegalArgumentException(); + } + } + + @Override + public UserDto execute(UserService userService) { + return userService.findByUsername(username); + } +} diff --git a/src/main/java/controller/user/command/UserCommand.java b/src/main/java/controller/user/command/UserCommand.java new file mode 100644 index 00000000000..c5418dfff7e --- /dev/null +++ b/src/main/java/controller/user/command/UserCommand.java @@ -0,0 +1,8 @@ +package controller.user.command; + +import dto.UserDto; +import service.UserService; + +public interface UserCommand { + UserDto execute(UserService userService); +} diff --git a/src/main/java/database/ConnectionManager.java b/src/main/java/database/ConnectionManager.java new file mode 100644 index 00000000000..a360675d0b3 --- /dev/null +++ b/src/main/java/database/ConnectionManager.java @@ -0,0 +1,23 @@ +package database; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class ConnectionManager { + private static final String SERVER = "localhost:13306"; + private static final String DATABASE = "chess"; + private static final String OPTION = "?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"; + private static final String USERNAME = "root"; + private static final String PASSWORD = "root"; + + public Connection getConnection() { + try { + return DriverManager.getConnection("jdbc:mysql://" + SERVER + "/" + DATABASE + OPTION, USERNAME, PASSWORD); + } catch (final SQLException e) { + System.err.println("DB 연결 오류:" + e.getMessage()); + e.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/database/JdbcTemplate.java b/src/main/java/database/JdbcTemplate.java new file mode 100644 index 00000000000..1b3b106e308 --- /dev/null +++ b/src/main/java/database/JdbcTemplate.java @@ -0,0 +1,54 @@ +package database; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class JdbcTemplate { + private final ConnectionManager connectionManager; + + public JdbcTemplate() { + this.connectionManager = new ConnectionManager(); + } + + public void execute(final String query, final String... parameters) { + try (final Connection connection = connectionManager.getConnection(); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + setParameters(preparedStatement, parameters); + preparedStatement.executeUpdate(); + } catch (final SQLException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + public List executeAndGet(final String query, final RowMapper mapper, final String... parameters) { + try (final Connection connection = connectionManager.getConnection(); + final PreparedStatement preparedStatement = connection.prepareStatement(query)) { + setParameters(preparedStatement, parameters); + return resolveResult(mapper, preparedStatement); + } catch (final SQLException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + private void setParameters(final PreparedStatement preparedStatement, final String... parameters) throws SQLException { + for (int i = 0; i < parameters.length; i++) { + preparedStatement.setString(i + 1, parameters[i]); + } + } + + private List resolveResult(final RowMapper mapper, final PreparedStatement preparedStatement) { + try (final ResultSet resultSet = preparedStatement.executeQuery()) { + final List results = new ArrayList<>(); + while (resultSet.next()) { + results.add(mapper.mapRow(resultSet)); + } + return results; + } catch (final SQLException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/src/main/java/database/RowMapper.java b/src/main/java/database/RowMapper.java new file mode 100644 index 00000000000..bfaf494439a --- /dev/null +++ b/src/main/java/database/RowMapper.java @@ -0,0 +1,9 @@ +package database; + +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/database/dao/GameStateDao.java b/src/main/java/database/dao/GameStateDao.java new file mode 100644 index 00000000000..af9ccf69ee7 --- /dev/null +++ b/src/main/java/database/dao/GameStateDao.java @@ -0,0 +1,13 @@ +package database.dao; + +import dto.StateDto; + +import java.util.Optional; + +public interface GameStateDao { + void add(StateDto stateDto); + + Optional findByGameId(int gameId); + + void deleteByGameId(int gameId); +} diff --git a/src/main/java/database/dao/GameStateDaoImpl.java b/src/main/java/database/dao/GameStateDaoImpl.java new file mode 100644 index 00000000000..0efe6fe2ac0 --- /dev/null +++ b/src/main/java/database/dao/GameStateDaoImpl.java @@ -0,0 +1,37 @@ +package database.dao; + +import database.JdbcTemplate; +import database.RowMapper; +import dto.StateDto; + +import java.util.List; +import java.util.Optional; + +public class GameStateDaoImpl implements GameStateDao { + private static final String TABLE_NAME = "game_states"; + + private final JdbcTemplate jdbcTemplate = new JdbcTemplate(); + private final RowMapper rowMapper = resultSet -> new StateDto( + resultSet.getString("state"), + resultSet.getInt("game_id") + ); + + public void add(final StateDto stateDto) { + final String query = "INSERT INTO " + TABLE_NAME + " VALUES (?, ?)"; + jdbcTemplate.execute(query, String.valueOf(stateDto.gameId()), stateDto.state()); + } + + public Optional findByGameId(final int gameId) { + final String query = "SELECT * FROM " + TABLE_NAME + " WHERE game_id = ? LIMIT 1"; + final List turns = jdbcTemplate.executeAndGet(query, rowMapper, String.valueOf(gameId)); + if (turns.isEmpty()) { + return Optional.empty(); + } + return Optional.ofNullable(turns.get(0)); + } + + public void deleteByGameId(final int gameId) { + final String query = "DELETE FROM " + TABLE_NAME + " WHERE game_id = ?"; + jdbcTemplate.execute(query, String.valueOf(gameId)); + } +} diff --git a/src/main/java/database/dao/PieceDao.java b/src/main/java/database/dao/PieceDao.java new file mode 100644 index 00000000000..5b6444ac4d0 --- /dev/null +++ b/src/main/java/database/dao/PieceDao.java @@ -0,0 +1,14 @@ +package database.dao; + +import dto.PieceDto; +import dto.RoomDto; + +import java.util.List; + +public interface PieceDao { + void add(RoomDto room, PieceDto piece); + + List findPieceByGameId(int gameId); + + void deleteAllByGameId(int gameId); +} diff --git a/src/main/java/database/dao/PieceDaoImpl.java b/src/main/java/database/dao/PieceDaoImpl.java new file mode 100644 index 00000000000..145883b43df --- /dev/null +++ b/src/main/java/database/dao/PieceDaoImpl.java @@ -0,0 +1,37 @@ +package database.dao; + +import database.JdbcTemplate; +import database.RowMapper; +import dto.PieceDto; +import dto.RoomDto; + +import java.util.List; + +public class PieceDaoImpl implements PieceDao { + private static final String TABLE_NAME = "pieces"; + + private final JdbcTemplate jdbcTemplate = new JdbcTemplate(); + private final RowMapper rowMapper = resultSet -> new PieceDto( + resultSet.getString("board_file"), + resultSet.getString("board_rank"), + resultSet.getString("color"), + resultSet.getString("type") + ); + + public void add(final RoomDto room, final PieceDto piece) { + final String query = "INSERT INTO " + TABLE_NAME + " VALUES(?, ?, ?, ?, ?)"; + jdbcTemplate.execute(query, + String.valueOf(room.roomId()), piece.boardFile(), piece.boardRank(), + piece.color(), piece.type()); + } + + public List findPieceByGameId(final int gameId) { + final String query = "SELECT * FROM " + TABLE_NAME + " WHERE game_id = ?"; + return jdbcTemplate.executeAndGet(query, rowMapper, String.valueOf(gameId)); + } + + public void deleteAllByGameId(final int gameId) { + final String query = "DELETE FROM " + TABLE_NAME + " WHERE game_id = ?"; + jdbcTemplate.execute(query, String.valueOf(gameId)); + } +} diff --git a/src/main/java/database/dao/RoomDao.java b/src/main/java/database/dao/RoomDao.java new file mode 100644 index 00000000000..86bcb505ba0 --- /dev/null +++ b/src/main/java/database/dao/RoomDao.java @@ -0,0 +1,17 @@ +package database.dao; + +import dto.RoomDto; +import dto.UserDto; + +import java.util.List; +import java.util.Optional; + +public interface RoomDao { + void add(UserDto userDto, RoomDto roomDto); + + Optional addNewRoom(UserDto userDto); + + Optional find(String roomId); + + List findActiveRoomAll(UserDto user); +} diff --git a/src/main/java/database/dao/RoomDaoImpl.java b/src/main/java/database/dao/RoomDaoImpl.java new file mode 100644 index 00000000000..c305d15b080 --- /dev/null +++ b/src/main/java/database/dao/RoomDaoImpl.java @@ -0,0 +1,56 @@ +package database.dao; + +import database.JdbcTemplate; +import database.RowMapper; +import dto.RoomDto; +import dto.UserDto; + +import java.util.List; +import java.util.Optional; + +public class RoomDaoImpl implements RoomDao { + private static final String TABLE_NAME = "rooms"; + private static final String GAME_STATUES_TABLE_NAME = "game_states"; + + private final JdbcTemplate jdbcTemplate = new JdbcTemplate(); + private final RowMapper rowMapper = resultSet -> new RoomDto( + resultSet.getInt("room_id") + ); + + public void add(final UserDto userDto, final RoomDto roomDto) { + final String insertQuery = "INSERT INTO " + TABLE_NAME + " VALUES (?, ?)"; + jdbcTemplate.execute(insertQuery, userDto.username(), "" + roomDto.roomId()); + } + + public Optional addNewRoom(final UserDto userDto) { + final int newRoomId = getRoomIdMax() + 1; + return Optional.of(insertNewRoom(userDto, newRoomId)); + } + + private int getRoomIdMax() { + final String selectQuery = "SELECT MAX(room_id) AS room_id FROM " + TABLE_NAME; + final List rooms = jdbcTemplate.executeAndGet(selectQuery, rowMapper); + return rooms.get(0).roomId(); + } + + private RoomDto insertNewRoom(final UserDto userDto, final int newRoomId) { + final String insertQuery = "INSERT INTO " + TABLE_NAME + " VALUES (?, ?)"; + jdbcTemplate.execute(insertQuery, userDto.username(), "" + newRoomId); + return new RoomDto(newRoomId); + } + + public Optional find(final String roomId) { + final String query = "SELECT * FROM " + TABLE_NAME + " WHERE room_id = ?"; + final List rooms = jdbcTemplate.executeAndGet(query, rowMapper, roomId); + if (rooms.isEmpty()) { + return Optional.empty(); + } + return Optional.of(rooms.get(0)); + } + + public List findActiveRoomAll(final UserDto user) { + final String query = "SELECT * FROM " + TABLE_NAME + " AS r JOIN " + + GAME_STATUES_TABLE_NAME + " AS s ON r.room_id = s.game_id WHERE r.user = ? and s.state != 'GAMEOVER'"; + return jdbcTemplate.executeAndGet(query, rowMapper, user.username()); + } +} diff --git a/src/main/java/database/dao/UserDao.java b/src/main/java/database/dao/UserDao.java new file mode 100644 index 00000000000..2dababf0aed --- /dev/null +++ b/src/main/java/database/dao/UserDao.java @@ -0,0 +1,11 @@ +package database.dao; + +import dto.UserDto; + +import java.util.Optional; + +public interface UserDao { + Optional find(String username); + + void add(String username); +} diff --git a/src/main/java/database/dao/UserDaoImpl.java b/src/main/java/database/dao/UserDaoImpl.java new file mode 100644 index 00000000000..2ac14c6cd1d --- /dev/null +++ b/src/main/java/database/dao/UserDaoImpl.java @@ -0,0 +1,33 @@ +package database.dao; + +import database.JdbcTemplate; +import database.RowMapper; +import dto.UserDto; + +import java.util.List; +import java.util.Optional; + +public class UserDaoImpl implements UserDao { + private static final String TABLE_NAME = "users"; + + private final JdbcTemplate jdbcTemplate = new JdbcTemplate(); + private final RowMapper rowMapper = resultSet -> new UserDto( + resultSet.getString("username") + ); + + @Override + public Optional find(final String username) { + final String query = "SELECT * FROM " + TABLE_NAME + " WHERE username = ? LIMIT 1"; + List users = jdbcTemplate.executeAndGet(query, rowMapper, username); + if (users.isEmpty()) { + return Optional.empty(); + } + return Optional.of(users.get(0)); + } + + @Override + public void add(final String username) { + final String query = "INSERT INTO " + TABLE_NAME + " VALUES (?)"; + jdbcTemplate.execute(query, username); + } +} diff --git a/src/main/java/domain/ChessGame.java b/src/main/java/domain/ChessGame.java new file mode 100644 index 00000000000..9a580eb47dd --- /dev/null +++ b/src/main/java/domain/ChessGame.java @@ -0,0 +1,58 @@ +package domain; + +import domain.board.ChessBoard; +import domain.board.Score; +import domain.piece.Color; +import domain.position.Position; +import domain.state.ReadyState; +import domain.state.State; + +public class ChessGame { + private final ChessBoard board; + private State state; + + public ChessGame(final ChessBoard board) { + this(board, new ReadyState()); + } + + private ChessGame(final ChessBoard board, final State state) { + this.board = board; + this.state = state; + } + + public void start() { + this.state = state.start(); + } + + public void move(final Position source, final Position target) { + this.state = state.move(); + board.move(source, target); + if (board.isKingNotExist()) { + this.state = state.end(); + } + } + + public void end() { + state = state.end(); + } + + public boolean isPlaying() { + return state.isPlaying(); + } + + public boolean isGameOver() { + return board.isKingNotExist(); + } + + public ChessBoard getBoard() { + return board; + } + + public Color getTurn() { + return board.getTurn(); + } + + public Score getScore() { + return board.calculateScore(); + } +} diff --git a/src/main/java/domain/board/ChessBoard.java b/src/main/java/domain/board/ChessBoard.java new file mode 100644 index 00000000000..3e36782b2d6 --- /dev/null +++ b/src/main/java/domain/board/ChessBoard.java @@ -0,0 +1,105 @@ +package domain.board; + +import domain.piece.Color; +import domain.piece.Empty; +import domain.piece.Piece; +import domain.piece.Type; +import domain.position.Position; +import domain.position.Route; +import dto.PieceDto; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ChessBoard { + private final Map board; + private Color turn; + + public ChessBoard(Map board) { + this(new HashMap<>(board), Color.WHITE); + } + + ChessBoard(Map board, Color turn) { + this.board = new HashMap<>(board); + this.turn = turn; + } + + public void move(final Position source, final Position target) { + validateEmptyPiece(source); + validateTurn(source); + validateEmptyRoute(source, target); + validateLegalMove(source, target); + movePiece(source, target); + changeTurn(); + } + + private void validateEmptyPiece(final Position source) { + final Piece piece = findPieceByPosition(source); + if (piece.color().isNeutrality()) { + throw new IllegalArgumentException("피스가 없습니다."); + } + } + + private void validateTurn(final Position source) { + final Piece piece = findPieceByPosition(source); + if (this.turn != piece.color()) { + throw new IllegalArgumentException("상대 턴입니다."); + } + } + + private void validateEmptyRoute(final Position source, final Position target) { + final Route route = Route.create(source, target); + if (route.isBlocked(board)) { + throw new IllegalArgumentException("중간에 말이 있어서 이동할 수 없습니다."); + } + } + + private void validateLegalMove(final Position source, final Position target) { + final Piece resourcePiece = findPieceByPosition(source); + resourcePiece.validateMovement(source, target, findPieceByPosition(target)); + } + + private void movePiece(final Position source, final Position target) { + final Piece piece = findPieceByPosition(source); + board.remove(source); + board.put(target, piece); + } + + private Piece findPieceByPosition(final Position position) { + return board.getOrDefault(position, Empty.getInstance()); + } + + private void changeTurn() { + if (this.turn == Color.BLACK) { + this.turn = Color.WHITE; + return; + } + this.turn = Color.BLACK; + } + + public boolean isKingNotExist() { + return board.values().stream() + .filter(piece -> piece.type() == Type.KING) + .count() != 2; + } + + public Score calculateScore() { + return Score.calculate(board); + } + + public Map getBoard() { + return Collections.unmodifiableMap(board); + } + + public List getPieces() { + return board.entrySet().stream() + .map(entry -> PieceDto.of(entry.getKey(), entry.getValue())) + .toList(); + } + + public Color getTurn() { + return this.turn; + } +} diff --git a/src/main/java/domain/board/ChessBoardFactory.java b/src/main/java/domain/board/ChessBoardFactory.java new file mode 100644 index 00000000000..94d349cae60 --- /dev/null +++ b/src/main/java/domain/board/ChessBoardFactory.java @@ -0,0 +1,53 @@ +package domain.board; + +import domain.piece.Color; +import domain.piece.Piece; +import domain.piece.nonpawn.Bishop; +import domain.piece.nonpawn.King; +import domain.piece.nonpawn.Knight; +import domain.piece.nonpawn.Queen; +import domain.piece.nonpawn.Rook; +import domain.piece.pawn.BlackPawn; +import domain.piece.pawn.WhitePawn; +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; +import dto.PieceDto; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ChessBoardFactory { + private static final int SPECIAL_PIECE_SIZE = 8; + private static final List whiteSpecialPieces = List.of( + new Rook(Color.WHITE), new Knight(Color.WHITE), new Bishop(Color.WHITE), new Queen(Color.WHITE), + new King(Color.WHITE), new Bishop(Color.WHITE), new Knight(Color.WHITE), new Rook(Color.WHITE) + ); + private static final List blackSpecialPieces = List.of( + new Rook(Color.BLACK), new Knight(Color.BLACK), new Bishop(Color.BLACK), new Queen(Color.BLACK), + new King(Color.BLACK), new Bishop(Color.BLACK), new Knight(Color.BLACK), new Rook(Color.BLACK) + ); + + public static ChessBoard createInitialChessBoard() { + final Map pieceMap = new HashMap<>(); + for (int order = 0; order < SPECIAL_PIECE_SIZE; order++) { + pieceMap.put(new Position(File.fromOrder(order), Rank.EIGHT), blackSpecialPieces.get(order)); + pieceMap.put(new Position(File.fromOrder(order), Rank.SEVEN), new BlackPawn()); + pieceMap.put(new Position(File.fromOrder(order), Rank.TWO), new WhitePawn()); + pieceMap.put(new Position(File.fromOrder(order), Rank.ONE), whiteSpecialPieces.get(order)); + } + return new ChessBoard(pieceMap); + } + + public static ChessBoard loadPreviousChessBoard(final List pieces, final Color turn) { + return pieces.stream() + .collect( + Collectors.collectingAndThen( + Collectors.toMap(PieceDto::getPosition, PieceDto::getPiece), + board -> new ChessBoard(board, turn) + ) + ); + } +} diff --git a/src/main/java/domain/board/Score.java b/src/main/java/domain/board/Score.java new file mode 100644 index 00000000000..37fbdca3dd2 --- /dev/null +++ b/src/main/java/domain/board/Score.java @@ -0,0 +1,77 @@ +package domain.board; + +import domain.piece.Color; +import domain.piece.Piece; +import domain.piece.Type; +import domain.position.File; +import domain.position.Position; + +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Score { + private final double whiteScore; + private final double blackScore; + + private Score(final double whiteScore, final double blackScore) { + this.whiteScore = whiteScore; + this.blackScore = blackScore; + } + + public static Score calculate(final Map board) { + final double whiteScore = calculateBoardScore(board, Color.WHITE); + final double blackScore = calculateBoardScore(board, Color.BLACK); + return new Score(whiteScore, blackScore); + } + + public static double calculateBoardScore(final Map board, final Color color) { + if (hasNoKingPiece(board, color)) { + return 0; + } + return calculateTotalScore(board, color) - calculateDistinctFilesPenalty(board, color); + } + + private static boolean hasNoKingPiece(final Map board, final Color color) { + return getPiecesOfColor(board, color) + .noneMatch(positionPieceEntry -> positionPieceEntry.getValue().type() == Type.KING); + } + + private static double calculateTotalScore(final Map board, final Color color) { + return getPiecesOfColor(board, color) + .mapToDouble(positionPieceEntry -> positionPieceEntry.getValue().type().score()) + .sum(); + } + + private static double calculateDistinctFilesPenalty(final Map board, final Color color) { + final Map fileCounts = countPawnPiecesByFile(board, color); + + final long totalDuplicateFiles = fileCounts.values().stream() + .filter(count -> count > 1) + .reduce(0L, Long::sum); + + return totalDuplicateFiles * 0.5; + } + + private static Map countPawnPiecesByFile(final Map board, final Color color) { + return getPiecesOfColor(board, color) + .filter(entry -> entry.getValue().type() == Type.PAWN) + .collect(Collectors.groupingBy( + entry -> entry.getKey().file(), + Collectors.counting() + )); + } + + private static Stream> getPiecesOfColor(final Map board, final Color color) { + return board.entrySet().stream() + .filter(positionPieceEntry -> positionPieceEntry.getValue().color() == color); + } + + public double getWhiteScore() { + return whiteScore; + } + + public double getBlackScore() { + return blackScore; + } +} diff --git a/src/main/java/domain/piece/Color.java b/src/main/java/domain/piece/Color.java new file mode 100644 index 00000000000..6232daaba63 --- /dev/null +++ b/src/main/java/domain/piece/Color.java @@ -0,0 +1,15 @@ +package domain.piece; + +public enum Color { + WHITE, + BLACK, + NEUTRALITY; + + public boolean isBlack() { + return this == BLACK; + } + + public boolean isNeutrality() { + return this == NEUTRALITY; + } +} diff --git a/src/main/java/domain/piece/Empty.java b/src/main/java/domain/piece/Empty.java new file mode 100644 index 00000000000..25c621cba28 --- /dev/null +++ b/src/main/java/domain/piece/Empty.java @@ -0,0 +1,29 @@ +package domain.piece; + +import domain.position.Position; + +public class Empty implements Piece { + private static final Empty INSTANCE = new Empty(); + + private Empty() { + } + + public static Empty getInstance() { + return INSTANCE; + } + + @Override + public void validateMovement(final Position resource, final Position target, final Piece other) { + throw new UnsupportedOperationException("Empty는 움직일 수 없습니다."); + } + + @Override + public Color color() { + return Color.NEUTRALITY; + } + + @Override + public Type type() { + throw new UnsupportedOperationException("Empty는 타입이 없습니다."); + } +} diff --git a/src/main/java/domain/piece/Piece.java b/src/main/java/domain/piece/Piece.java new file mode 100644 index 00000000000..4cda19b161f --- /dev/null +++ b/src/main/java/domain/piece/Piece.java @@ -0,0 +1,11 @@ +package domain.piece; + +import domain.position.Position; + +public interface Piece { + void validateMovement(Position source, Position target, Piece other); + + Color color(); + + Type type(); +} diff --git a/src/main/java/domain/piece/Type.java b/src/main/java/domain/piece/Type.java new file mode 100644 index 00000000000..39657584bc5 --- /dev/null +++ b/src/main/java/domain/piece/Type.java @@ -0,0 +1,41 @@ +package domain.piece; + +import domain.piece.nonpawn.Bishop; +import domain.piece.nonpawn.King; +import domain.piece.nonpawn.Knight; +import domain.piece.nonpawn.Queen; +import domain.piece.nonpawn.Rook; +import domain.piece.pawn.BlackPawn; +import domain.piece.pawn.WhitePawn; + +import java.util.function.Function; + +public enum Type { + KING(0.0, King::new), + QUEEN(9.0, Queen::new), + ROOK(5.0, Rook::new), + BISHOP(3.0, Bishop::new), + KNIGHT(2.5, Knight::new), + PAWN(1.0, (color) -> { + if (color.isBlack()) { + return new BlackPawn(); + } + return new WhitePawn(); + }); + + final double score; + final Function getInstance; + + Type(final double score, final Function getInstance) { + this.score = score; + this.getInstance = getInstance; + } + + public Piece getPiece(final Color color) { + return getInstance.apply(color); + } + + public double score() { + return score; + } +} diff --git a/src/main/java/domain/piece/nonpawn/Bishop.java b/src/main/java/domain/piece/nonpawn/Bishop.java new file mode 100644 index 00000000000..27aa984d683 --- /dev/null +++ b/src/main/java/domain/piece/nonpawn/Bishop.java @@ -0,0 +1,23 @@ +package domain.piece.nonpawn; + +import domain.piece.Color; +import domain.piece.Type; +import domain.position.Position; + +public class Bishop extends NonPawnPiece { + public Bishop(final Color color) { + super(color); + } + + @Override + protected void validateDirection(final Position source, final Position target) { + if (!source.isDiagonalAt(target)) { + throw new IllegalArgumentException("비숍은 대각선 방향으로만 이동할 수 있습니다."); + } + } + + @Override + public Type type() { + return Type.BISHOP; + } +} diff --git a/src/main/java/domain/piece/nonpawn/King.java b/src/main/java/domain/piece/nonpawn/King.java new file mode 100644 index 00000000000..90ae058ea8c --- /dev/null +++ b/src/main/java/domain/piece/nonpawn/King.java @@ -0,0 +1,23 @@ +package domain.piece.nonpawn; + +import domain.piece.Color; +import domain.piece.Type; +import domain.position.Position; + +public class King extends NonPawnPiece { + public King(final Color color) { + super(color); + } + + @Override + protected void validateDirection(final Position source, final Position target) { + if (!source.isAdjacentAt(target)) { + throw new IllegalArgumentException("킹은 한 번에 1칸만 이동할 수 있습니다."); + } + } + + @Override + public Type type() { + return Type.KING; + } +} diff --git a/src/main/java/domain/piece/nonpawn/Knight.java b/src/main/java/domain/piece/nonpawn/Knight.java new file mode 100644 index 00000000000..86f1474ab59 --- /dev/null +++ b/src/main/java/domain/piece/nonpawn/Knight.java @@ -0,0 +1,23 @@ +package domain.piece.nonpawn; + +import domain.piece.Color; +import domain.piece.Type; +import domain.position.Position; + +public class Knight extends NonPawnPiece { + public Knight(final Color color) { + super(color); + } + + @Override + protected void validateDirection(final Position source, final Position target) { + if (!source.isKnightPositionAt(target)) { + throw new IllegalArgumentException("나이트는 L자 방향으로만 이동할 수 있습니다."); + } + } + + @Override + public Type type() { + return Type.KNIGHT; + } +} diff --git a/src/main/java/domain/piece/nonpawn/NonPawnPiece.java b/src/main/java/domain/piece/nonpawn/NonPawnPiece.java new file mode 100644 index 00000000000..5e092040fe9 --- /dev/null +++ b/src/main/java/domain/piece/nonpawn/NonPawnPiece.java @@ -0,0 +1,32 @@ +package domain.piece.nonpawn; + +import domain.piece.Color; +import domain.piece.Piece; +import domain.position.Position; + +public abstract class NonPawnPiece implements Piece { + private final Color color; + + protected NonPawnPiece(final Color color) { + this.color = color; + } + + @Override + public final void validateMovement(final Position source, final Position target, final Piece other) { + validateColorDifference(other); + validateDirection(source, target); + } + + private void validateColorDifference(final Piece other) { + if (this.color() == other.color()) { + throw new IllegalArgumentException("같은 팀의 말을 잡을 수 없습니다."); + } + } + + protected abstract void validateDirection(final Position source, final Position target); + + @Override + public Color color() { + return color; + } +} diff --git a/src/main/java/domain/piece/nonpawn/Queen.java b/src/main/java/domain/piece/nonpawn/Queen.java new file mode 100644 index 00000000000..abc8f7d9332 --- /dev/null +++ b/src/main/java/domain/piece/nonpawn/Queen.java @@ -0,0 +1,23 @@ +package domain.piece.nonpawn; + +import domain.piece.Color; +import domain.piece.Type; +import domain.position.Position; + +public class Queen extends NonPawnPiece { + public Queen(final Color color) { + super(color); + } + + @Override + protected void validateDirection(final Position source, final Position target) { + if (!source.isDiagonalAt(target) && !source.isStraightAt(target)) { + throw new IllegalArgumentException("퀸은 대각선, 수평, 수직 방향으로만 이동할 수 있습니다."); + } + } + + @Override + public Type type() { + return Type.QUEEN; + } +} diff --git a/src/main/java/domain/piece/nonpawn/Rook.java b/src/main/java/domain/piece/nonpawn/Rook.java new file mode 100644 index 00000000000..eb53fe5c98c --- /dev/null +++ b/src/main/java/domain/piece/nonpawn/Rook.java @@ -0,0 +1,23 @@ +package domain.piece.nonpawn; + +import domain.piece.Color; +import domain.piece.Type; +import domain.position.Position; + +public class Rook extends NonPawnPiece { + public Rook(final Color color) { + super(color); + } + + @Override + protected void validateDirection(final Position source, final Position target) { + if (!source.isStraightAt(target)) { + throw new IllegalArgumentException("룩은 수평, 수직 방향으로만 이동할 수 있습니다."); + } + } + + @Override + public Type type() { + return Type.ROOK; + } +} diff --git a/src/main/java/domain/piece/pawn/BlackPawn.java b/src/main/java/domain/piece/pawn/BlackPawn.java new file mode 100644 index 00000000000..6526648ccba --- /dev/null +++ b/src/main/java/domain/piece/pawn/BlackPawn.java @@ -0,0 +1,30 @@ +package domain.piece.pawn; + +import domain.piece.Color; +import domain.position.Position; +import domain.position.Rank; + +public class BlackPawn extends PawnPiece { + private static final Rank INITIAL_RANK = Rank.SEVEN; + + public BlackPawn() { + super(Color.BLACK); + } + + @Override + protected void validateForwardMovement(final Position source, final Position target) { + if (!source.isUpperRankThan(target)) { + throw new IllegalArgumentException("폰은 앞으로 이동해야 합니다."); + } + } + + @Override + protected boolean isAtSameRank(final Position source) { + return source.isAtSameRank(INITIAL_RANK); + } + + @Override + public Color color() { + return Color.BLACK; + } +} diff --git a/src/main/java/domain/piece/pawn/PawnPiece.java b/src/main/java/domain/piece/pawn/PawnPiece.java new file mode 100644 index 00000000000..c203694428d --- /dev/null +++ b/src/main/java/domain/piece/pawn/PawnPiece.java @@ -0,0 +1,79 @@ +package domain.piece.pawn; + +import domain.piece.Color; +import domain.piece.Piece; +import domain.piece.Type; +import domain.position.Position; + +public abstract class PawnPiece implements Piece { + private static final int INITIAL_MOVE_DISTANCE = 2; + private static final int NORMAL_MOVE_DISTANCE = 1; + + private final Color color; + + protected PawnPiece(Color color) { + this.color = color; + } + + public final void validateMovement(final Position source, final Position target, final Piece other) { + validateColorDifference(other); + validateForwardMovement(source, target); + validateFawnMovement(source, target, other); + } + + private void validateColorDifference(final Piece other) { + if (this.color() == other.color()) { + throw new IllegalArgumentException("같은 팀의 말을 잡을 수 없습니다."); + } + } + + protected abstract void validateForwardMovement(final Position source, final Position target); + + private void validateFawnMovement(final Position source, final Position target, final Piece other) { + if (!isPawnMovement(source, target, other)) { + throw new IllegalArgumentException("잘못된 위치로 이동하고 있습니다."); + } + } + + private boolean isPawnMovement(final Position source, final Position target, final Piece other) { + return isMovingTwoDistanceForward(source, target, other) || + isMovingOneDistanceForward(source, target, other) || + isMovingOneDistanceDiagonal(source, target, other); + } + + private boolean isMovingTwoDistanceForward(final Position source, final Position target, final Piece other) { + return isAtSameRank(source) && source.isStraightAt(target) + && source.isDistanceAt(target, INITIAL_MOVE_DISTANCE) && nonPieceExist(other); + } + + protected abstract boolean isAtSameRank(final Position source); + + private boolean isMovingOneDistanceForward(final Position source, final Position target, final Piece other) { + return source.isStraightAt(target) && source.isDistanceAt(target, NORMAL_MOVE_DISTANCE) && nonPieceExist(other); + } + + private boolean isMovingOneDistanceDiagonal(final Position source, final Position target, final Piece other) { + return source.isDiagonalAt(target) && source.isAdjacentAt(target) && isOpposite(other); + } + + private boolean nonPieceExist(Piece other) { + return other.color().isNeutrality(); + } + + private boolean isOpposite(Piece other) { + if (nonPieceExist(other)) { + return false; + } + return this.color() != other.color(); + } + + @Override + public Color color() { + return color; + } + + @Override + public final Type type() { + return Type.PAWN; + } +} diff --git a/src/main/java/domain/piece/pawn/WhitePawn.java b/src/main/java/domain/piece/pawn/WhitePawn.java new file mode 100644 index 00000000000..9f92d4aea88 --- /dev/null +++ b/src/main/java/domain/piece/pawn/WhitePawn.java @@ -0,0 +1,30 @@ +package domain.piece.pawn; + +import domain.piece.Color; +import domain.position.Position; +import domain.position.Rank; + +public class WhitePawn extends PawnPiece { + private static final Rank INITIAL_RANK = Rank.TWO; + + public WhitePawn() { + super(Color.WHITE); + } + + @Override + protected void validateForwardMovement(final Position source, final Position target) { + if (!source.isLowerRankThan(target)) { + throw new IllegalArgumentException("폰은 앞으로 이동해야 합니다."); + } + } + + @Override + protected boolean isAtSameRank(final Position source) { + return source.isAtSameRank(INITIAL_RANK); + } + + @Override + public Color color() { + return Color.WHITE; + } +} diff --git a/src/main/java/domain/position/File.java b/src/main/java/domain/position/File.java new file mode 100644 index 00000000000..50790c1c8ea --- /dev/null +++ b/src/main/java/domain/position/File.java @@ -0,0 +1,65 @@ +package domain.position; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +public enum File { + A(0), + B(1), + C(2), + D(3), + E(4), + F(5), + G(6), + H(7); + + private final int order; + + File(int order) { + this.order = order; + } + + public static File fromName(final String name) { + try { + return File.valueOf(name); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("존재하지 않은 file입니다."); + } + } + + public static File fromOrder(final int order) { + return Arrays.stream(values()) + .filter(file -> file.order == order) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않은 file입니다.")); + } + + public List between(final File file) { + final List files = IntStream.range(min(this.order, file.order), max(this.order, file.order)) + .skip(1) + .mapToObj(File::fromOrder) + .collect(Collectors.toList()); + if (this.isLaterThan(file)) { + Collections.reverse(files); + } + return files; + } + + private boolean isLaterThan(final File file) { + return this.order > file.order; + } + + int subtract(final File file) { + return this.order - file.order; + } + + public int order() { + return order; + } +} diff --git a/src/main/java/domain/position/Position.java b/src/main/java/domain/position/Position.java new file mode 100644 index 00000000000..04189fca0f0 --- /dev/null +++ b/src/main/java/domain/position/Position.java @@ -0,0 +1,105 @@ +package domain.position; + +import java.util.Objects; + +public class Position { + private final File file; + private final Rank rank; + + public Position(final String fileAndRank) { + this( + File.fromName(fileAndRank.substring(0, 1)), + Rank.fromNumber(Integer.parseInt(fileAndRank.substring(1))) + ); + } + + public Position(final File file, final Rank rank) { + this.file = file; + this.rank = rank; + } + + public boolean isStraightAt(final Position target) { + validateSamePosition(target); + return calculateFileGap(target) == 0 || calculateRankGap(target) == 0; + } + + public boolean isDiagonalAt(final Position target) { + validateSamePosition(target); + return calculateFileGap(target) == calculateRankGap(target); + } + + public boolean isKnightPositionAt(final Position target) { + validateSamePosition(target); + return calculateRankGap(target) * calculateFileGap(target) == 2; + } + + public boolean isAdjacentAt(final Position target) { + validateSamePosition(target); + return Math.max(calculateRankGap(target), calculateFileGap(target)) == 1; + } + + public boolean isDistanceAt(final Position target, final int distance) { + return calculateRankGap(target) + calculateFileGap(target) == distance; + } + + private void validateSamePosition(final Position target) { + if (this.equals(target)) { + throw new IllegalArgumentException("동일한 위치입니다."); + } + } + + public boolean isUpperRankThan(final Position target) { + return this.rank.isUpperThan(target.rank); + } + + public boolean isLowerRankThan(final Position target) { + return this.rank.isLowerThan(target.rank); + } + + public boolean isAtSameRank(final Rank rank) { + return this.rank.isSame(rank); + } + + boolean isVertical(final Position target) { + validateSamePosition(target); + return calculateFileGap(target) == 0 && calculateRankGap(target) > 0; + } + + boolean isHorizontal(final Position target) { + validateSamePosition(target); + return calculateFileGap(target) > 0 && calculateRankGap(target) == 0; + } + + int calculateRankGap(final Position target) { + return Math.abs(rank.subtract(target.rank)); + } + + int calculateFileGap(final Position target) { + return Math.abs(file.subtract(target.file)); + } + + public Rank rank() { + return rank; + } + + public File file() { + return file; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Position position = (Position) o; + return file == position.file && rank == position.rank; + } + + @Override + public int hashCode() { + return Objects.hash(file, rank); + } +} diff --git a/src/main/java/domain/position/Rank.java b/src/main/java/domain/position/Rank.java new file mode 100644 index 00000000000..8c5da658961 --- /dev/null +++ b/src/main/java/domain/position/Rank.java @@ -0,0 +1,62 @@ +package domain.position; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public enum Rank { + EIGHT(8), + SEVEN(7), + SIX(6), + FIVE(5), + FOUR(4), + THREE(3), + TWO(2), + ONE(1); + + private final int number; + + Rank(int number) { + this.number = number; + } + + public static Rank fromNumber(final int number) { + return Arrays.stream(values()) + .filter(rank -> rank.number == number) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않은 rank입니다.")); + } + + public List between(final Rank target) { + final List ranks = IntStream.range(Math.min(this.number, target.number), Math.max(this.number, target.number)) + .skip(1) + .mapToObj(Rank::fromNumber) + .collect(Collectors.toList()); + if (this.isUpperThan(target)) { + Collections.reverse(ranks); + } + return ranks; + } + + boolean isUpperThan(final Rank rank) { + return this.number > rank.number; + } + + boolean isLowerThan(final Rank rank) { + return this.number < rank.number; + } + + boolean isSame(final Rank rank) { + return this.number == rank.number; + } + + int subtract(final Rank rank) { + return this.number - rank.number; + } + + public String number() { + return String.valueOf(number); + } +} diff --git a/src/main/java/domain/position/Route.java b/src/main/java/domain/position/Route.java new file mode 100644 index 00000000000..6c2a3006ca1 --- /dev/null +++ b/src/main/java/domain/position/Route.java @@ -0,0 +1,66 @@ +package domain.position; + +import domain.piece.Piece; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class Route { + private final List positions; + + private Route(final List positions) { + this.positions = positions; + } + + public static Route create(final Position source, final Position target) { + if (source.isKnightPositionAt(target) || source.isAdjacentAt(target)) { + return createEmptyRoute(); + } + if (source.isDiagonalAt(target)) { + return createDiagonalRoute(source, target); + } + if (source.isVertical(target)) { + return createVerticalRoute(source, target); + } + if (source.isHorizontal(target)) { + return createHorizontalRoute(source, target); + } + throw new IllegalArgumentException("잘못된 방향으로 이동하고 있습니다."); + } + + private static Route createEmptyRoute() { + return new Route(List.of()); + } + + private static Route createDiagonalRoute(final Position source, final Position target) { + final List files = source.file().between(target.file()); + final List ranks = source.rank().between(target.rank()); + return IntStream.range(0, files.size()) + .mapToObj(i -> new Position(files.get(i), ranks.get(i))) + .collect(Collectors.collectingAndThen(Collectors.toList(), Route::new)); + } + + private static Route createHorizontalRoute(final Position source, final Position target) { + final List files = source.file().between(target.file()); + return files.stream() + .map(file -> new Position(file, source.rank())) + .collect(Collectors.collectingAndThen(Collectors.toList(), Route::new)); + } + + private static Route createVerticalRoute(final Position source, final Position target) { + final List ranks = source.rank().between(target.rank()); + return ranks.stream() + .map(rank -> new Position(source.file(), rank)) + .collect(Collectors.collectingAndThen(Collectors.toList(), Route::new)); + } + + public boolean isBlocked(final Map board) { + return positions.stream().anyMatch(board::containsKey); + } + + List getRoute() { + return positions; + } +} diff --git a/src/main/java/domain/state/EndState.java b/src/main/java/domain/state/EndState.java new file mode 100644 index 00000000000..9de9df6c693 --- /dev/null +++ b/src/main/java/domain/state/EndState.java @@ -0,0 +1,23 @@ +package domain.state; + +public class EndState implements State { + @Override + public State start() { + throw new UnsupportedOperationException("게임이 종료됐습니다."); + } + + @Override + public State move() { + throw new UnsupportedOperationException("게임이 종료됐습니다."); + } + + @Override + public State end() { + throw new UnsupportedOperationException("게임이 종료됐습니다."); + } + + @Override + public boolean isPlaying() { + return false; + } +} diff --git a/src/main/java/domain/state/ReadyState.java b/src/main/java/domain/state/ReadyState.java new file mode 100644 index 00000000000..6d8ce70ea90 --- /dev/null +++ b/src/main/java/domain/state/ReadyState.java @@ -0,0 +1,23 @@ +package domain.state; + +public class ReadyState implements State { + @Override + public State start() { + return new RunningState(); + } + + @Override + public State move() { + throw new UnsupportedOperationException("게임을 시작해 주세요."); + } + + @Override + public State end() { + throw new UnsupportedOperationException("게임을 시작해 주세요."); + } + + @Override + public boolean isPlaying() { + return true; + } +} diff --git a/src/main/java/domain/state/RunningState.java b/src/main/java/domain/state/RunningState.java new file mode 100644 index 00000000000..3033e4da248 --- /dev/null +++ b/src/main/java/domain/state/RunningState.java @@ -0,0 +1,23 @@ +package domain.state; + +public class RunningState implements State { + @Override + public State start() { + throw new UnsupportedOperationException("게임이 이미 시작됐습니다."); + } + + @Override + public State move() { + return this; + } + + @Override + public State end() { + return new EndState(); + } + + @Override + public boolean isPlaying() { + return true; + } +} diff --git a/src/main/java/domain/state/State.java b/src/main/java/domain/state/State.java new file mode 100644 index 00000000000..65d0e908f99 --- /dev/null +++ b/src/main/java/domain/state/State.java @@ -0,0 +1,8 @@ +package domain.state; + +public interface State { + State start(); + State move(); + State end(); + boolean isPlaying(); +} diff --git a/src/main/java/dto/PieceDto.java b/src/main/java/dto/PieceDto.java new file mode 100644 index 00000000000..70e87feea7e --- /dev/null +++ b/src/main/java/dto/PieceDto.java @@ -0,0 +1,36 @@ +package dto; + +import domain.piece.Color; +import domain.piece.Piece; +import domain.piece.Type; +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; + +public record PieceDto( + String boardFile, + String boardRank, + String color, + String type +) { + public static PieceDto of(Position position, Piece piece) { + return new PieceDto( + position.file().name(), + position.rank().number(), + piece.color().name(), + piece.type().name() + ); + } + + public Position getPosition() { + final File file = File.fromName(boardFile); + final Rank rank = Rank.fromNumber(Integer.parseInt(boardRank)); + return new Position(file, rank); + } + + public Piece getPiece() { + final Color pieceColor = Color.valueOf(color); + final Type pieceType = Type.valueOf(type); + return pieceType.getPiece(pieceColor); + } +} diff --git a/src/main/java/dto/RoomDto.java b/src/main/java/dto/RoomDto.java new file mode 100644 index 00000000000..ed1b23d8766 --- /dev/null +++ b/src/main/java/dto/RoomDto.java @@ -0,0 +1,4 @@ +package dto; + +public record RoomDto(int roomId) { +} diff --git a/src/main/java/dto/StateDto.java b/src/main/java/dto/StateDto.java new file mode 100644 index 00000000000..4ad98c3e687 --- /dev/null +++ b/src/main/java/dto/StateDto.java @@ -0,0 +1,10 @@ +package dto; + + +import domain.piece.Color; + +public record StateDto(String state, int gameId) { + public Color getState() { + return Color.valueOf(state); + } +} diff --git a/src/main/java/dto/UserDto.java b/src/main/java/dto/UserDto.java new file mode 100644 index 00000000000..56a59dcf2d5 --- /dev/null +++ b/src/main/java/dto/UserDto.java @@ -0,0 +1,4 @@ +package dto; + +public record UserDto(String username) { +} diff --git a/src/main/java/service/ChessGameService.java b/src/main/java/service/ChessGameService.java new file mode 100644 index 00000000000..3ac75999b75 --- /dev/null +++ b/src/main/java/service/ChessGameService.java @@ -0,0 +1,67 @@ +package service; + +import database.dao.GameStateDao; +import database.dao.PieceDao; +import domain.ChessGame; +import domain.board.ChessBoard; +import domain.board.ChessBoardFactory; +import domain.piece.Color; +import dto.PieceDto; +import dto.RoomDto; +import dto.StateDto; + +import java.util.List; +import java.util.NoSuchElementException; + +public class ChessGameService { + private final PieceDao pieceDao; + private final GameStateDao gameStateDao; + + public ChessGameService(final PieceDao pieceDao, final GameStateDao gameStateDao) { + this.pieceDao = pieceDao; + this.gameStateDao = gameStateDao; + } + + public ChessGame initializeChessGame(final RoomDto roomDto) { + try { + final StateDto stateDto = loadPreviousState(roomDto); + return new ChessGame(ChessBoardFactory.loadPreviousChessBoard( + loadPreviousPieces(roomDto), stateDto.getState())); + } catch (NoSuchElementException e) { + return new ChessGame(ChessBoardFactory.createInitialChessBoard()); + } + } + + public void saveChessGame(final ChessGame chessGame, final RoomDto roomDto) { + if (chessGame.isGameOver()) { + updateState(new StateDto("GAMEOVER", roomDto.roomId())); + return; + } + + final ChessBoard board = chessGame.getBoard(); + Color turn = chessGame.getTurn(); + updatePieces(roomDto, board.getPieces()); + updateState(new StateDto(turn.name(), roomDto.roomId())); + } + + private List loadPreviousPieces(final RoomDto roomDto) { + return pieceDao.findPieceByGameId(roomDto.roomId()); + } + + private StateDto loadPreviousState(final RoomDto roomDto) { + return gameStateDao.findByGameId(roomDto.roomId()) + .orElseThrow(NoSuchElementException::new); + } + + private void updatePieces(final RoomDto roomDto, final List pieceDtos) { + pieceDao.deleteAllByGameId(roomDto.roomId()); + for (final PieceDto pieceDto : pieceDtos) { + pieceDao.add(roomDto, pieceDto); + } + } + + private void updateState(final StateDto stateDto) { + gameStateDao.deleteByGameId(stateDto.gameId()); + gameStateDao.add(stateDto); + } +} diff --git a/src/main/java/service/GameRoomService.java b/src/main/java/service/GameRoomService.java new file mode 100644 index 00000000000..56b204345a0 --- /dev/null +++ b/src/main/java/service/GameRoomService.java @@ -0,0 +1,29 @@ +package service; + +import database.dao.RoomDao; +import dto.RoomDto; +import dto.UserDto; + +import java.util.List; + +public class GameRoomService { + private final RoomDao roomDao; + + public GameRoomService(final RoomDao roomDao) { + this.roomDao = roomDao; + } + + public List loadActiveRoomAll(final UserDto user) { + return roomDao.findActiveRoomAll(user); + } + + public RoomDto createNewRoom(final UserDto user) { + return roomDao.addNewRoom(user) + .orElseThrow(IllegalStateException::new); + } + + public RoomDto findRoomById(final String roomId) { + return roomDao.find(roomId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 방입니다.")); + } +} diff --git a/src/main/java/service/UserService.java b/src/main/java/service/UserService.java new file mode 100644 index 00000000000..9f5b4b86974 --- /dev/null +++ b/src/main/java/service/UserService.java @@ -0,0 +1,23 @@ +package service; + +import database.dao.UserDao; +import dto.UserDto; + +import java.util.Optional; + +public class UserService { + private final UserDao userDao; + + public UserService(final UserDao userDao) { + this.userDao = userDao; + } + + public UserDto findByUsername(final String username) { + Optional userDto = userDao.find(username); + if (userDto.isEmpty()) { + userDao.add(username); + userDto = userDao.find(username); + } + return userDto.orElseThrow(IllegalStateException::new); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000000..dba766294a5 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,50 @@ +package view; + +import controller.game.command.GameCommand; +import controller.room.command.RoomCommand; +import controller.user.command.UserCommand; +import dto.RoomDto; +import view.command.CommandInput; +import view.command.GameCommandType; +import view.command.RoomCommandType; +import view.command.UserCommandType; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Scanner; + +public class InputView { + private static final Scanner SCANNER = new Scanner(System.in); + private static final String WRONG_COMMAND_ERROR_MESSAGE = "잘못된 명령어입니다."; + + public static GameCommand readGameCommand() { + try { + CommandInput input = readCommandInput(); + return GameCommandType.getCommand(input); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(WRONG_COMMAND_ERROR_MESSAGE); + } + } + + public static RoomCommand readRoomCommand(List rooms) { + try { + CommandInput input = readCommandInput(); + return RoomCommandType.getCommand(input, rooms); + } catch (IllegalArgumentException | NoSuchElementException e) { + throw new IllegalArgumentException(WRONG_COMMAND_ERROR_MESSAGE); + } + } + + public static UserCommand readUserCommand() { + try { + CommandInput input = readCommandInput(); + return UserCommandType.getCommand(input); + } catch (IllegalArgumentException | NoSuchElementException e) { + throw new IllegalArgumentException(WRONG_COMMAND_ERROR_MESSAGE); + } + } + + private static CommandInput readCommandInput() { + return new CommandInput(SCANNER.nextLine().strip()); + } +} diff --git a/src/main/java/view/MessageResolver.java b/src/main/java/view/MessageResolver.java new file mode 100644 index 00000000000..9cbf5417ea5 --- /dev/null +++ b/src/main/java/view/MessageResolver.java @@ -0,0 +1,129 @@ +package view; + +import domain.board.ChessBoard; +import domain.board.Score; +import domain.piece.Piece; +import domain.piece.Type; +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; +import dto.RoomDto; +import dto.UserDto; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static view.command.GameCommandType.END; +import static view.command.GameCommandType.MOVE; +import static view.command.GameCommandType.START; +import static view.command.RoomCommandType.NEW_ROOM; +import static view.command.RoomCommandType.ROOM_SELECTION; +import static view.command.UserCommandType.FIND_USER; + +public class MessageResolver { + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final int BOARD_LENGTH = 8; + private static final Map PIECE_DISPLAY = Map.of( + Type.PAWN, "p", + Type.KNIGHT, "n", + Type.BISHOP, "b", + Type.ROOK, "r", + Type.QUEEN, "q", + Type.KING, "k" + ); + + public String resolveGameStartMessage() { + String gameStartMessage = "> 체스 게임을 시작합니다."; + String gameStartCommandMessage = String.format("> 게임 시작 : %s", START.message()); + String gameEndCommandMessage = String.format("> 게임 종료 : %s", END.message()); + String gameMoveCommandMessage = String.format("> 게임 이동 : %s source위치 target위치 - 예. %s b2 b3", + MOVE.message(), MOVE.message()); + return String.join(LINE_SEPARATOR, gameStartMessage, gameStartCommandMessage, + gameEndCommandMessage, gameMoveCommandMessage); + } + + public String resolveBoardMessage(ChessBoard board) { + List boardMessage = IntStream.rangeClosed(1, 8) + .mapToObj(rank -> resolveOneRank(board, Rank.fromNumber(rank))) + .collect(Collectors.toList()); + Collections.reverse(boardMessage); + return String.join(LINE_SEPARATOR, boardMessage); + } + + private String resolveOneRank(ChessBoard board, Rank targetRank) { + StringBuilder rankMessage = new StringBuilder(".".repeat(BOARD_LENGTH)); + board.getBoard().entrySet().stream() + .filter(entry -> resolvePosition(entry).rank() == targetRank) + .forEach(positionPieceEntry -> updateRankMessage(rankMessage, positionPieceEntry)); + return rankMessage.toString(); + } + + private void updateRankMessage(StringBuilder rankMessage, Map.Entry positionPieceEntry) { + Position position = resolvePosition(positionPieceEntry); + Piece piece = resolvePiece(positionPieceEntry); + File file = position.file(); + rankMessage.setCharAt(file.order(), pieceDisplay(piece).charAt(0)); + } + + private Piece resolvePiece(Map.Entry positionAndPiece) { + return positionAndPiece.getValue(); + } + + private Position resolvePosition(Map.Entry positionAndPiece) { + return positionAndPiece.getKey(); + } + + private String pieceDisplay(Piece piece) { + String pieceName = PIECE_DISPLAY.get(piece.type()); + if (piece.color().isBlack()) { + return pieceName.toUpperCase(); + } + return pieceName; + } + + public String resolveScoreMessage(Score score) { + String whiteScoreMessage = String.format("WHITE 점수: %.1f", score.getWhiteScore()); + String blackScoreMessage = String.format("BLACK 점수: %.1f", score.getBlackScore()); + return String.join(LINE_SEPARATOR, whiteScoreMessage, blackScoreMessage); + } + + public String resolveWinnerMessage(Score score) { + if (score.getWhiteScore() > score.getBlackScore()) { + return "우승자는 WHITE입니다!"; + } + return "우승자는 BLACK입니다!"; + } + + public String resolveRoomGuideMessage(List rooms) { + String newRoomMessage = String.format("> 새로운 방 생성 : %s", NEW_ROOM.message()); + if (rooms.isEmpty()) { + return newRoomMessage; + } + return String.join(LINE_SEPARATOR, newRoomMessage, resolveRooms(rooms)); + } + + private String resolveRooms(List rooms) { + String roomGuideHeaderMessage = String.format("> 입장할 방 선택 : %s 방번호 - 예 %s 1", + ROOM_SELECTION.message(), ROOM_SELECTION.message()); + String roomListMessage = "> 방 목록 : " + rooms.stream() + .map(room -> String.valueOf(room.roomId())) + .collect(Collectors.joining(", ")); + return String.join(LINE_SEPARATOR, roomGuideHeaderMessage, roomListMessage); + } + + public String resolveEnteringRoomMessage(RoomDto roomDto) { + return roomDto.roomId() + "번 방에 입장합니다."; + } + + public String resolveUserNameInputMessage() { + return String.format("> 사용자명을 입력해 주세요 (4~10자) : %s 사용자명 - 예 %s mangcho", + FIND_USER.message(), FIND_USER.message()); + } + + public String resolveUserNameMessage(UserDto user) { + return String.format("%s님 반갑습니다.", user.username()); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000000..4bda2c9d400 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,55 @@ +package view; + +import domain.board.ChessBoard; +import domain.board.Score; +import dto.RoomDto; +import dto.UserDto; + +import java.util.List; + +public class OutputView { + public static final String LINE_SEPARATOR = System.lineSeparator(); + private static final MessageResolver messageResolver = new MessageResolver(); + + public static void printGameGuideMessage() { + printWithLineSeparator(messageResolver.resolveGameStartMessage()); + } + + public static void printBoard(ChessBoard board) { + String boardMessage = messageResolver.resolveBoardMessage(board); + printWithLineSeparator(boardMessage); + } + + public static void printScore(Score score) { + String scoreMessage = messageResolver.resolveScoreMessage(score); + printWithLineSeparator(scoreMessage); + } + + public static void printWinner(Score score) { + printWithLineSeparator(messageResolver.resolveWinnerMessage(score)); + } + + public static void printErrorMessage(Exception e) { + printWithLineSeparator(e.getMessage()); + } + + public static void printGameRoomGuideMessage(List rooms) { + printWithLineSeparator(messageResolver.resolveRoomGuideMessage(rooms)); + } + + public static void printEnteringRoomMessage(RoomDto roomDto) { + printWithLineSeparator(messageResolver.resolveEnteringRoomMessage(roomDto)); + } + + public static void printUserNameInputMessage() { + printWithLineSeparator(messageResolver.resolveUserNameInputMessage()); + } + + public static void printUserNameMessage(UserDto user) { + printWithLineSeparator(messageResolver.resolveUserNameMessage(user)); + } + + private static void printWithLineSeparator(String message) { + System.out.println(LINE_SEPARATOR + message); + } +} diff --git a/src/main/java/view/command/CommandInput.java b/src/main/java/view/command/CommandInput.java new file mode 100644 index 00000000000..ec3c22b8428 --- /dev/null +++ b/src/main/java/view/command/CommandInput.java @@ -0,0 +1,30 @@ +package view.command; + +import java.util.List; +import java.util.NoSuchElementException; + +public class CommandInput { + private final List inputs; + + public CommandInput(final String input) { + this(List.of(input.split(" "))); + } + + private CommandInput(final List inputs) { + this.inputs = inputs; + } + + public String prefix() { + if (inputs.isEmpty()) { + throw new NoSuchElementException(); + } + return inputs.get(0); + } + + public List getArguments() { + return inputs.stream() + .map(String::toUpperCase) + .toList() + .subList(1, inputs.size()); + } +} diff --git a/src/main/java/view/command/GameCommandType.java b/src/main/java/view/command/GameCommandType.java new file mode 100644 index 00000000000..c2f5436acc7 --- /dev/null +++ b/src/main/java/view/command/GameCommandType.java @@ -0,0 +1,43 @@ +package view.command; + +import controller.game.command.EndOnGameCommand; +import controller.game.command.GameCommand; +import controller.game.command.MoveOnGameCommand; +import controller.game.command.StartOnGameCommand; +import controller.game.command.StatusOnGameCommand; + +import java.util.Arrays; +import java.util.List; + +public enum GameCommandType { + START("start", StartOnGameCommand::new), + END("end", EndOnGameCommand::new), + STATUS("status", StatusOnGameCommand::new), + MOVE("move", MoveOnGameCommand::new); + + private final String command; + private final CommandMapper mapper; + + GameCommandType(final String command, final CommandMapper mapper) { + this.command = command; + this.mapper = mapper; + } + + public static GameCommand getCommand(final CommandInput input) { + final GameCommandType commandType = Arrays.stream(GameCommandType.values()) + .filter(command -> input.prefix().equals(command.command)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + + return commandType.mapper.apply(input.getArguments()); + } + + public String message() { + return this.command; + } + + @FunctionalInterface + private interface CommandMapper { + GameCommand apply(List arguments); + } +} diff --git a/src/main/java/view/command/RoomCommandType.java b/src/main/java/view/command/RoomCommandType.java new file mode 100644 index 00000000000..0d3d93ad3af --- /dev/null +++ b/src/main/java/view/command/RoomCommandType.java @@ -0,0 +1,40 @@ +package view.command; + +import controller.room.command.NewRoomOnRoomCommand; +import controller.room.command.RoomCommand; +import controller.room.command.SelectRoomOnRoomCommand; +import dto.RoomDto; + +import java.util.Arrays; +import java.util.List; + +public enum RoomCommandType { + NEW_ROOM("new", (arguments, rooms) -> new NewRoomOnRoomCommand(arguments)), + ROOM_SELECTION("room", SelectRoomOnRoomCommand::new); + + private final String command; + private final CommandMapper mapper; + + RoomCommandType(final String command, final CommandMapper mapper) { + this.command = command; + this.mapper = mapper; + } + + public static RoomCommand getCommand(final CommandInput input, final List rooms) { + final RoomCommandType commandType = Arrays.stream(RoomCommandType.values()) + .filter(command -> input.prefix().equals(command.command)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + + return commandType.mapper.apply(input.getArguments(), rooms); + } + + public String message() { + return this.command; + } + + @FunctionalInterface + private interface CommandMapper { + RoomCommand apply(List arguments, List rooms); + } +} diff --git a/src/main/java/view/command/UserCommandType.java b/src/main/java/view/command/UserCommandType.java new file mode 100644 index 00000000000..a237de049c8 --- /dev/null +++ b/src/main/java/view/command/UserCommandType.java @@ -0,0 +1,37 @@ +package view.command; + +import controller.user.command.FindUserOnDemand; +import controller.user.command.UserCommand; + +import java.util.Arrays; +import java.util.List; + +public enum UserCommandType { + FIND_USER("user", FindUserOnDemand::new); + + private final String command; + private final CommandMapper mapper; + + UserCommandType(final String command, final CommandMapper mapper) { + this.command = command; + this.mapper = mapper; + } + + public static UserCommand getCommand(final CommandInput input) { + final UserCommandType commandType = Arrays.stream(UserCommandType.values()) + .filter(command -> input.prefix().equals(command.command)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + + return commandType.mapper.apply(input.getArguments()); + } + + public String message() { + return this.command; + } + + @FunctionalInterface + private interface CommandMapper { + UserCommand apply(List arguments); + } +} diff --git a/src/main/resources/sample.txt b/src/main/resources/sample.txt new file mode 100644 index 00000000000..5a4f6cc6093 --- /dev/null +++ b/src/main/resources/sample.txt @@ -0,0 +1,14 @@ +[BLACK 승리] +move f2 f3 +move e7 e5 +move g2 g4 +move d8 h4 +move h2 h3 +move h4 e1 + +[WHITE 승리] +move e2 e3 +move f7 f6 +move d1 h5 +move g8 h6 +move h5 e8 diff --git a/src/test/java/db/ConnectionManagerTest.java b/src/test/java/db/ConnectionManagerTest.java new file mode 100644 index 00000000000..b554a5cadf3 --- /dev/null +++ b/src/test/java/db/ConnectionManagerTest.java @@ -0,0 +1,20 @@ +package db; + +import database.ConnectionManager; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; + +import static org.assertj.core.api.Assertions.assertThat; + +class ConnectionManagerTest { + @Test + void connectionTest() { + ConnectionManager connectionManager = new ConnectionManager(); + try (final var connection = connectionManager.getConnection()) { + assertThat(connection).isNotNull(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/domain/ChessGameTest.java b/src/test/java/domain/ChessGameTest.java new file mode 100644 index 00000000000..38b0087b916 --- /dev/null +++ b/src/test/java/domain/ChessGameTest.java @@ -0,0 +1,103 @@ +package domain; + +import domain.board.ChessBoard; +import domain.piece.Color; +import domain.piece.nonpawn.King; +import domain.position.Position; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ChessGameTest { + @Test + void 게임을_시작하기_전에_말을_움직일_수_없다() { + ChessGame game = new ChessGame(new ChessBoard(Map.of())); + + assertThatThrownBy(() -> game.move(new Position("A1"), new Position("A2"))) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("게임을 시작해 주세요."); + } + + @Test + void 게임을_시작하기_전에_종료할_수_없다() { + ChessGame game = new ChessGame(new ChessBoard(Map.of())); + + assertThatThrownBy(game::end) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("게임을 시작해 주세요."); + } + + @Test + void 게임을_시작하고_다시_시작할_수_없다() { + ChessGame game = new ChessGame(new ChessBoard(Map.of())); + + game.start(); + + assertThatThrownBy(game::start) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("게임이 이미 시작됐습니다."); + } + + @Test + void 게임이_생성되기만_하면_게임은_진행_상태이다() { + ChessGame game = new ChessGame(new ChessBoard(Map.of())); + + assertThat(game.isPlaying()).isTrue(); + } + + @Test + void end를_호출하기_전까지_게임은_진행_상태이다() { + ChessGame game = new ChessGame(new ChessBoard(Map.of())); + + game.start(); + + assertThat(game.isPlaying()).isTrue(); + } + + @Test + void end를_호출되면_게임은_종료된다() { + ChessGame game = new ChessGame(new ChessBoard(Map.of())); + + game.start(); + game.end(); + + assertThat(game.isPlaying()).isFalse(); + } + + @Test + void 피스를_움직이고_왕이_죽으면_게임은_오버된다() { + Position A1 = new Position("A1"); + Position A2 = new Position("A2"); + ChessGame game = new ChessGame(new ChessBoard(Map.of( + A1, new King(Color.WHITE), + A2, new King(Color.BLACK)))); + + game.start(); + game.move(A1, A2); + + assertThat(game.isGameOver()).isTrue(); + } + + @Test + void 모든_KING이_살아있으면_게임은_오버되지_않는다() { + ChessGame game = new ChessGame(new ChessBoard(Map.of( + new Position("A1"), new King(Color.WHITE), + new Position("A2"), new King(Color.BLACK)))); + + assertThat(game.isGameOver()).isFalse(); + } + + @Test + void KING이_없으면_게임_오버_상태이다() { + ChessGame game = new ChessGame(new ChessBoard(Map.of())); + + assertThat(game.isGameOver()).isTrue(); + } +} diff --git a/src/test/java/domain/Fixtures.java b/src/test/java/domain/Fixtures.java new file mode 100644 index 00000000000..10f97b10e68 --- /dev/null +++ b/src/test/java/domain/Fixtures.java @@ -0,0 +1,84 @@ +package domain; + +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; + +@SuppressWarnings("unused") +public final class Fixtures { + + public static final Position A1 = new Position(File.A, Rank.ONE); + public static final Position A2 = new Position(File.A, Rank.TWO); + public static final Position A3 = new Position(File.A, Rank.THREE); + public static final Position A4 = new Position(File.A, Rank.FOUR); + public static final Position A5 = new Position(File.A, Rank.FIVE); + public static final Position A6 = new Position(File.A, Rank.SIX); + public static final Position A7 = new Position(File.A, Rank.SEVEN); + public static final Position A8 = new Position(File.A, Rank.EIGHT); + + public static final Position B1 = new Position(File.B, Rank.ONE); + public static final Position B2 = new Position(File.B, Rank.TWO); + public static final Position B3 = new Position(File.B, Rank.THREE); + public static final Position B4 = new Position(File.B, Rank.FOUR); + public static final Position B5 = new Position(File.B, Rank.FIVE); + public static final Position B6 = new Position(File.B, Rank.SIX); + public static final Position B7 = new Position(File.B, Rank.SEVEN); + public static final Position B8 = new Position(File.B, Rank.EIGHT); + + public static final Position C1 = new Position(File.C, Rank.ONE); + public static final Position C2 = new Position(File.C, Rank.TWO); + public static final Position C3 = new Position(File.C, Rank.THREE); + public static final Position C4 = new Position(File.C, Rank.FOUR); + public static final Position C5 = new Position(File.C, Rank.FIVE); + public static final Position C6 = new Position(File.C, Rank.SIX); + public static final Position C7 = new Position(File.C, Rank.SEVEN); + public static final Position C8 = new Position(File.C, Rank.EIGHT); + + public static final Position D1 = new Position(File.D, Rank.ONE); + public static final Position D2 = new Position(File.D, Rank.TWO); + public static final Position D3 = new Position(File.D, Rank.THREE); + public static final Position D4 = new Position(File.D, Rank.FOUR); + public static final Position D5 = new Position(File.D, Rank.FIVE); + public static final Position D6 = new Position(File.D, Rank.SIX); + public static final Position D7 = new Position(File.D, Rank.SEVEN); + public static final Position D8 = new Position(File.D, Rank.EIGHT); + + public static final Position E1 = new Position(File.E, Rank.ONE); + public static final Position E2 = new Position(File.E, Rank.TWO); + public static final Position E3 = new Position(File.E, Rank.THREE); + public static final Position E4 = new Position(File.E, Rank.FOUR); + public static final Position E5 = new Position(File.E, Rank.FIVE); + public static final Position E6 = new Position(File.E, Rank.SIX); + public static final Position E7 = new Position(File.E, Rank.SEVEN); + public static final Position E8 = new Position(File.E, Rank.EIGHT); + + public static final Position F1 = new Position(File.F, Rank.ONE); + public static final Position F2 = new Position(File.F, Rank.TWO); + public static final Position F3 = new Position(File.F, Rank.THREE); + public static final Position F4 = new Position(File.F, Rank.FOUR); + public static final Position F5 = new Position(File.F, Rank.FIVE); + public static final Position F6 = new Position(File.F, Rank.SIX); + public static final Position F7 = new Position(File.F, Rank.SEVEN); + public static final Position F8 = new Position(File.F, Rank.EIGHT); + + public static final Position G1 = new Position(File.G, Rank.ONE); + public static final Position G2 = new Position(File.G, Rank.TWO); + public static final Position G3 = new Position(File.G, Rank.THREE); + public static final Position G4 = new Position(File.G, Rank.FOUR); + public static final Position G5 = new Position(File.G, Rank.FIVE); + public static final Position G6 = new Position(File.G, Rank.SIX); + public static final Position G7 = new Position(File.G, Rank.SEVEN); + public static final Position G8 = new Position(File.G, Rank.EIGHT); + + public static final Position H1 = new Position(File.H, Rank.ONE); + public static final Position H2 = new Position(File.H, Rank.TWO); + public static final Position H3 = new Position(File.H, Rank.THREE); + public static final Position H4 = new Position(File.H, Rank.FOUR); + public static final Position H5 = new Position(File.H, Rank.FIVE); + public static final Position H6 = new Position(File.H, Rank.SIX); + public static final Position H7 = new Position(File.H, Rank.SEVEN); + public static final Position H8 = new Position(File.H, Rank.EIGHT); + + private Fixtures() { + } +} diff --git a/src/test/java/domain/board/ChessBoardTest.java b/src/test/java/domain/board/ChessBoardTest.java new file mode 100644 index 00000000000..37656cbce5a --- /dev/null +++ b/src/test/java/domain/board/ChessBoardTest.java @@ -0,0 +1,127 @@ +package domain.board; + +import domain.piece.Color; +import domain.piece.Piece; +import domain.piece.nonpawn.Queen; +import domain.piece.pawn.BlackPawn; +import domain.piece.pawn.WhitePawn; +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ChessBoardTest { + private static final Color WHITE_TURN = Color.WHITE; + + @Test + void 출발_위치에_기물_존재하지_않으면_예외가_발생한다() { + Position source = new Position(File.F, Rank.FOUR); + Position target = new Position(File.F, Rank.EIGHT); + ChessBoard board = new ChessBoard(Map.of(), WHITE_TURN); + + assertThatThrownBy(() -> board.move(source, target)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("피스가 없습니다."); + } + + @Test + void 기물을_움직일_때_중간에_다른_기물이_있으면_예외가_발생한다() { + Position source = new Position(File.F, Rank.FOUR); + Position target = new Position(File.F, Rank.EIGHT); + Position between = new Position(File.F, Rank.FIVE); + ChessBoard board = new ChessBoard(Map.of( + source, new Queen(Color.WHITE), + between, new BlackPawn()), + WHITE_TURN); + + assertThatThrownBy(() -> board.move(source, target)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("중간에 말이 있어서 이동할 수 없습니다."); + } + + @Test + void 기물을_움직일_때_중간에_다른_기물이_없으면_이동한다() { + Piece piece = new Queen(Color.WHITE); + Position source = new Position(File.F, Rank.FOUR); + Position target = new Position(File.F, Rank.EIGHT); + ChessBoard board = new ChessBoard(Map.of(source, piece), WHITE_TURN); + + board.move(source, target); + assertThat(board.getBoard()) + .containsEntry(target, piece) + .doesNotContainKey(source); + } + + @Test + void 기물을_잡는다() { + Piece piece = new WhitePawn(); + Position source = new Position(File.F, Rank.FOUR); + Position target = new Position(File.G, Rank.FIVE); + ChessBoard board = new ChessBoard(Map.of( + source, piece, + target, new BlackPawn() + ), WHITE_TURN); + + board.move(source, target); + assertThat(board.getBoard()) + .containsEntry(target, piece) + .doesNotContainKey(source); + } + + @Test + void 게임을_시작하고_피스를_한_번_움직이면_다음은_BLACK_턴이다() { + Position resource = new Position(File.F, Rank.FOUR); + Position target = new Position(File.F, Rank.EIGHT); + ChessBoard board = new ChessBoard(Map.of(resource, new Queen(Color.WHITE)), WHITE_TURN); + + board.move(resource, target); + + assertThatThrownBy(() -> board.move(target, resource)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("상대 턴입니다."); + } + + @Test + void 피스가_움직이지_않으면_턴이_바뀌지_않는다() { + Position resource = new Position(File.F, Rank.FOUR); + Position target = new Position(File.F, Rank.EIGHT); + Position between = new Position(File.F, Rank.FIVE); + ChessBoard board = new ChessBoard(Map.of( + resource, new Queen(Color.WHITE), + between, new BlackPawn() + ), WHITE_TURN); + + assertThatThrownBy(() -> board.move(resource, target)) + .isInstanceOf(IllegalArgumentException.class); + assertThatCode(() -> board.move(resource, between)) + .doesNotThrowAnyException(); + } + + @Test + void 피스를_두_번_움직이면_턴이_되돌아온다() { + Position whiteResource = new Position(File.F, Rank.FOUR); + Position whiteTarget = new Position(File.F, Rank.EIGHT); + Position blackResource = new Position(File.A, Rank.FOUR); + Position blackTarget = new Position(File.A, Rank.EIGHT); + ChessBoard board = new ChessBoard(Map.of( + whiteResource, new Queen(Color.WHITE), + blackResource, new Queen(Color.BLACK) + ), WHITE_TURN); + + board.move(whiteResource, whiteTarget); + board.move(blackResource, blackTarget); + + assertThatCode(() -> board.move(whiteTarget, whiteResource)) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/domain/board/ScoreTest.java b/src/test/java/domain/board/ScoreTest.java new file mode 100644 index 00000000000..0fcc539bf7c --- /dev/null +++ b/src/test/java/domain/board/ScoreTest.java @@ -0,0 +1,92 @@ +package domain.board; + +import domain.piece.Piece; +import domain.piece.nonpawn.Bishop; +import domain.piece.nonpawn.King; +import domain.piece.nonpawn.Knight; +import domain.piece.nonpawn.Queen; +import domain.piece.nonpawn.Rook; +import domain.piece.pawn.BlackPawn; +import domain.piece.pawn.WhitePawn; +import domain.position.Position; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static domain.Fixtures.*; +import static domain.piece.Color.BLACK; +import static domain.piece.Color.WHITE; +import static domain.piece.Type.BISHOP; +import static domain.piece.Type.KING; +import static domain.piece.Type.KNIGHT; +import static domain.piece.Type.PAWN; +import static domain.piece.Type.QUEEN; +import static domain.piece.Type.ROOK; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ScoreTest { + @Test + void 모든_피스가_존재하면_점수는_38점이다() { + ChessBoard board = ChessBoardFactory.createInitialChessBoard(); + + Score score = Score.calculate(board.getBoard()); + + assertThat(score.getWhiteScore()).isEqualTo(38.0); + assertThat(score.getBlackScore()).isEqualTo(38.0); + } + + @Test + void 같은_세로_줄에_폰이_3개_존재하면_일점오점_이다() { + Map board = Map.of(A5, new WhitePawn(), A6, new WhitePawn(), A7, new WhitePawn(), + A8, new King(WHITE)); + + Score score = Score.calculate(board); + + double expectedWhiteScore = 0.5 * 3; + assertThat(score.getWhiteScore()).isEqualTo(expectedWhiteScore); + } + + @Test + void 같은_세로_줄에_폰이_3개_그리고_2개_그리고_1개_존재하면_삼점오점_이다() { + Map board = Map.of( + A5, new WhitePawn(), A6, new WhitePawn(), A7, new WhitePawn(), + B5, new WhitePawn(), B6, new WhitePawn(), + C5, new WhitePawn(), + A8, new King(WHITE) + ); + + Score score = Score.calculate(board); + + double expectedWhiteScore = 0.5 * 5 + 1.0; + assertThat(score.getWhiteScore()).isEqualTo(expectedWhiteScore); + } + + @Test + void 일부_피스가_존재하는_경우에_점수를_계산할_수_있다() { + var board = Map.ofEntries( + Map.entry(A7, new WhitePawn()), Map.entry(B7, new WhitePawn()), Map.entry(C7, new WhitePawn()), + Map.entry(D7, new WhitePawn()), Map.entry(E7, new WhitePawn()), Map.entry(F7, new WhitePawn()), + Map.entry(A8, new Rook(WHITE)), Map.entry(B8, new Bishop(WHITE)), Map.entry(C8, new Knight(WHITE)), + Map.entry(E8, new King(WHITE)), Map.entry(F8, new Queen(WHITE)), + Map.entry(A2, new BlackPawn()), Map.entry(B2, new BlackPawn()), Map.entry(C2, new BlackPawn()), + Map.entry(D2, new BlackPawn()), Map.entry(E2, new BlackPawn()), + Map.entry(A1, new Rook(BLACK)), Map.entry(B1, new Bishop(BLACK)), Map.entry(C1, new Knight(BLACK)), + Map.entry(H1, new Rook(BLACK)), Map.entry(G1, new Bishop(BLACK)), Map.entry(F1, new Knight(BLACK)), + Map.entry(E1, new King(BLACK)), Map.entry(D1, new Queen(BLACK)) + ); + + Score score = Score.calculate(board); + + double expectedWhiteScore = PAWN.score() * 6 + ROOK.score() * 1 + BISHOP.score() * 1 + + KNIGHT.score() * 1 + KING.score() + QUEEN.score(); + double expectedBlackScore = PAWN.score() * 5 + ROOK.score() * 2 + BISHOP.score() * 2 + + KNIGHT.score() * 2 + KING.score() + QUEEN.score(); + + assertThat(score.getWhiteScore()).isEqualTo(expectedWhiteScore); + assertThat(score.getBlackScore()).isEqualTo(expectedBlackScore); + } +} diff --git a/src/test/java/domain/dto/PieceDtoTest.java b/src/test/java/domain/dto/PieceDtoTest.java new file mode 100644 index 00000000000..0b5ed0f8c33 --- /dev/null +++ b/src/test/java/domain/dto/PieceDtoTest.java @@ -0,0 +1,88 @@ +package domain.dto; + +import domain.piece.Color; +import domain.piece.Piece; +import domain.piece.nonpawn.Bishop; +import domain.piece.nonpawn.King; +import domain.piece.nonpawn.Knight; +import domain.piece.nonpawn.Queen; +import domain.piece.nonpawn.Rook; +import domain.piece.pawn.BlackPawn; +import domain.piece.pawn.WhitePawn; +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; +import dto.PieceDto; +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 static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class PieceDtoTest { + @Test + void 도메인에서_DTO로_변환한다() { + Position position = new Position(File.A, Rank.ONE); + Piece piece = new King(Color.WHITE); + + PieceDto pieceDto = PieceDto.of(position, piece); + + PieceDto expected = new PieceDto("A", "1", "WHITE", "KING"); + assertThat(pieceDto).isEqualTo(expected); + } + + @Nested + class 피스_변환 { + @Test + void KING으로_변환한다() { + PieceDto pieceDto = new PieceDto("A", "1", "WHITE", "KING"); + + assertThat(pieceDto.getPiece()).isInstanceOf(King.class); + } + + @Test + void QUEEN으로_변환한다() { + PieceDto pieceDto = new PieceDto("A", "1", "WHITE", "QUEEN"); + + assertThat(pieceDto.getPiece()).isInstanceOf(Queen.class); + } + + @Test + void BISHOP으로_변환한다() { + PieceDto pieceDto = new PieceDto("A", "1", "WHITE", "BISHOP"); + + assertThat(pieceDto.getPiece()).isInstanceOf(Bishop.class); + } + + @Test + void ROOK으로_변환한다() { + PieceDto pieceDto = new PieceDto("A", "1", "WHITE", "ROOK"); + + assertThat(pieceDto.getPiece()).isInstanceOf(Rook.class); + } + + @Test + void KNIGHT으로_변환한다() { + PieceDto pieceDto = new PieceDto("A", "1", "WHITE", "KNIGHT"); + + assertThat(pieceDto.getPiece()).isInstanceOf(Knight.class); + } + + @Test + void WHITE_PAWN으로_변환한다() { + PieceDto pieceDto = new PieceDto("A", "1", "WHITE", "PAWN"); + + assertThat(pieceDto.getPiece()).isInstanceOf(WhitePawn.class); + } + + @Test + void BLACK_PAWN으로_변환한다() { + PieceDto pieceDto = new PieceDto("A", "1", "BLACK", "PAWN"); + + assertThat(pieceDto.getPiece()).isInstanceOf(BlackPawn.class); + } + } +} diff --git a/src/test/java/domain/piece/nonpawn/BishopTest.java b/src/test/java/domain/piece/nonpawn/BishopTest.java new file mode 100644 index 00000000000..44f5ea29c1e --- /dev/null +++ b/src/test/java/domain/piece/nonpawn/BishopTest.java @@ -0,0 +1,96 @@ +package domain.piece.nonpawn; + +import domain.piece.Color; +import domain.piece.Piece; +import domain.piece.pawn.BlackPawn; +import domain.piece.pawn.WhitePawn; +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class BishopTest { + private final Piece bishop = new Bishop(Color.WHITE); + + @Test + void 대각선_방향으로_이동할_수_있다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.G, Rank.SEVEN); + Piece other = new BlackPawn(); + + assertThatCode(() -> bishop.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 직선_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.EIGHT); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> bishop.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("비숍은 대각선 방향으로만 이동할 수 있습니다."); + } + + @Test + void L자_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.F, Rank.THREE); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> bishop.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("비숍은 대각선 방향으로만 이동할 수 있습니다."); + } + + @Test + void 정의되지_않은_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.A, Rank.TWO); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> bishop.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("비숍은 대각선 방향으로만 이동할 수 있습니다."); + } + + @Test + void 거리에_상관없이_이동할_수_있다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.H, Rank.EIGHT); + Piece other = new BlackPawn(); + + assertThatCode(() -> bishop.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 이동하지_않으면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.FOUR); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> bishop.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("동일한 위치입니다."); + } + + @Test + void 같은_팀의_말을_잡으면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.E, Rank.FIVE); + Piece other = new WhitePawn(); + + assertThatThrownBy(() -> bishop.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 말을 잡을 수 없습니다."); + } +} diff --git a/src/test/java/domain/piece/nonpawn/KingTest.java b/src/test/java/domain/piece/nonpawn/KingTest.java new file mode 100644 index 00000000000..d73a38edd15 --- /dev/null +++ b/src/test/java/domain/piece/nonpawn/KingTest.java @@ -0,0 +1,64 @@ +package domain.piece.nonpawn; + +import domain.piece.Color; +import domain.piece.Piece; +import domain.piece.pawn.BlackPawn; +import domain.piece.pawn.WhitePawn; +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class KingTest { + private final Piece king = new King(Color.WHITE); + + @Test + void 한_칸만_이동할_수_있다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.E, Rank.FIVE); + Piece other = new BlackPawn(); + + assertThatCode(() -> king.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 한_칸_이상_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.F, Rank.FOUR); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> king.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("킹은 한 번에 1칸만 이동할 수 있습니다"); + } + + @Test + void 이동하지_않으면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.FOUR); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> king.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("동일한 위치입니다."); + } + + @Test + void 같은_팀의_말을_잡으면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.E, Rank.FIVE); + Piece other = new WhitePawn(); + + assertThatThrownBy(() -> king.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 말을 잡을 수 없습니다."); + } +} diff --git a/src/test/java/domain/piece/nonpawn/KnightTest.java b/src/test/java/domain/piece/nonpawn/KnightTest.java new file mode 100644 index 00000000000..7de57bb220c --- /dev/null +++ b/src/test/java/domain/piece/nonpawn/KnightTest.java @@ -0,0 +1,86 @@ +package domain.piece.nonpawn; + +import domain.piece.Color; +import domain.piece.Piece; +import domain.piece.pawn.BlackPawn; +import domain.piece.pawn.WhitePawn; +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class KnightTest { + private final Piece knight = new Knight(Color.WHITE); + + @Test + void L자_방향으로_이동할_수_있다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.F, Rank.THREE); + Piece other = new BlackPawn(); + + assertThatCode(() -> knight.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 대각선_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.G, Rank.SEVEN); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> knight.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("나이트는 L자 방향으로만 이동할 수 있습니다."); + } + + @Test + void 직선_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.EIGHT); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> knight.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("나이트는 L자 방향으로만 이동할 수 있습니다."); + } + + @Test + void 정의되지_않은_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.A, Rank.TWO); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> knight.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("나이트는 L자 방향으로만 이동할 수 있습니다."); + } + + @Test + void 이동하지_않으면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.FOUR); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> knight.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("동일한 위치입니다."); + } + + @Test + void 같은_팀의_말을_잡으면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.E, Rank.FIVE); + Piece other = new WhitePawn(); + + assertThatThrownBy(() -> knight.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 말을 잡을 수 없습니다."); + } +} diff --git a/src/test/java/domain/piece/nonpawn/QueenTest.java b/src/test/java/domain/piece/nonpawn/QueenTest.java new file mode 100644 index 00000000000..480cbd643ba --- /dev/null +++ b/src/test/java/domain/piece/nonpawn/QueenTest.java @@ -0,0 +1,96 @@ +package domain.piece.nonpawn; + +import domain.piece.Color; +import domain.piece.Piece; +import domain.piece.pawn.BlackPawn; +import domain.piece.pawn.WhitePawn; +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class QueenTest { + private final Piece queen = new Queen(Color.WHITE); + + @Test + void 직선_방향으로_이동할_수_있다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.EIGHT); + Piece other = new BlackPawn(); + + assertThatCode(() -> queen.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 대각선_방향으로_이동할_수_있다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.G, Rank.SEVEN); + Piece other = new BlackPawn(); + + assertThatCode(() -> queen.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + + } + + @Test + void L자_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.F, Rank.THREE); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> queen.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("은 대각선, 수평, 수직 방향으로만 이동할 수 있습니다."); + } + + @Test + void 정의되지_않은_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.A, Rank.TWO); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> queen.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("퀸은 대각선, 수평, 수직 방향으로만 이동할 수 있습니다."); + } + + @Test + void 거리에_상관없이_이동할_수_있다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.EIGHT); + Piece other = new BlackPawn(); + + assertThatCode(() -> queen.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 이동하지_않으면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.FOUR); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> queen.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("동일한 위치입니다."); + } + + @Test + void 같은_팀의_말을_잡으면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.E, Rank.FIVE); + Piece other = new WhitePawn(); + + assertThatThrownBy(() -> queen.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 말을 잡을 수 없습니다."); + } +} diff --git a/src/test/java/domain/piece/nonpawn/RookTest.java b/src/test/java/domain/piece/nonpawn/RookTest.java new file mode 100644 index 00000000000..4e8b1edf1bd --- /dev/null +++ b/src/test/java/domain/piece/nonpawn/RookTest.java @@ -0,0 +1,106 @@ +package domain.piece.nonpawn; + +import domain.piece.Color; +import domain.piece.Piece; +import domain.piece.pawn.BlackPawn; +import domain.piece.pawn.WhitePawn; +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class RookTest { + private final Piece rook = new Rook(Color.WHITE); + + @Test + void 수평_방향으로_이동할_수_있다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.EIGHT); + Piece other = new BlackPawn(); + + assertThatCode(() -> rook.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 수직_방향으로_이동할_수_있다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.A, Rank.FOUR); + Piece other = new BlackPawn(); + + assertThatCode(() -> rook.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 대각선_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.G, Rank.SEVEN); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> rook.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("룩은 수평, 수직 방향으로만 이동할 수 있습니다."); + } + + @Test + void L자_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.F, Rank.THREE); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> rook.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("은 수평, 수직 방향으로만 이동할 수 있습니다."); + } + + @Test + void 정의되지_않은_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.A, Rank.TWO); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> rook.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("은 수평, 수직 방향으로만 이동할 수 있습니다."); + } + + @Test + void 거리에_상관없이_이동할_수_있다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.EIGHT); + Piece other = new BlackPawn(); + + assertThatCode(() -> rook.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 이동하지_않으면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.FOUR); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> rook.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("동일한 위치입니다."); + } + + @Test + void 같은_팀의_말을_잡으면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.E, Rank.FIVE); + Piece other = new WhitePawn(); + + assertThatThrownBy(() -> rook.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 말을 잡을 수 없습니다."); + } +} diff --git a/src/test/java/domain/piece/pawn/BlackPawnTest.java b/src/test/java/domain/piece/pawn/BlackPawnTest.java new file mode 100644 index 00000000000..511f4c03c48 --- /dev/null +++ b/src/test/java/domain/piece/pawn/BlackPawnTest.java @@ -0,0 +1,137 @@ +package domain.piece.pawn; + +import domain.piece.Empty; +import domain.piece.Piece; +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class BlackPawnTest { + private final Piece blackPawn = new BlackPawn(); + + @Test + void 직선_방향으로_한_칸_이동할_수_있다() { + Position source = new Position(File.D, Rank.FIVE); + Position target = new Position(File.D, Rank.FOUR); + Piece other = Empty.getInstance(); + + assertThatCode(() -> blackPawn.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 앞이_빈_공간이_아니면_직선_방향으로_이동할_수_없다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.THREE); + Piece other = new WhitePawn(); + + assertThatThrownBy(() -> blackPawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("잘못된 위치로 이동하고 있습니다."); + } + + @Test + void 초기_위치_일_때_직선_방향으로_두_칸_이동할_수_있다() { + Position source = new Position(File.D, Rank.SEVEN); + Position target = new Position(File.D, Rank.FIVE); + Piece other = Empty.getInstance(); + + assertThatCode(() -> blackPawn.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 초기_위치가_아닐_때_직선_방향으로_두_칸_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.SIX); + Position target = new Position(File.D, Rank.FOUR); + Piece other = Empty.getInstance(); + + assertThatThrownBy(() -> blackPawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("잘못된 위치로 이동하고 있습니다."); + } + + @Test + void 대각선_방향에_적이_있으면_이동할_수_있다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.E, Rank.THREE); + Piece other = new WhitePawn(); + + assertThatCode(() -> blackPawn.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 대각선_방향에_적이_없을_때_이동할_경우_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.E, Rank.THREE); + Piece other = Empty.getInstance(); + + assertThatThrownBy(() -> blackPawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("잘못된 위치로 이동하고 있습니다."); + } + + @Test + void 대각선_방향으로_두_칸_이동할_경우_예외가_발생한다() { + Position source = new Position(File.D, Rank.FIVE); + Position target = new Position(File.F, Rank.THREE); + Piece other = new WhitePawn(); + + assertThatThrownBy(() -> blackPawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("잘못된 위치로 이동하고 있습니다."); + } + + @Test + void 뒤로_이동하려고_하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.FIVE); + Piece other = Empty.getInstance(); + + assertThatThrownBy(() -> blackPawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("폰은 앞으로 이동해야 합니다."); + } + + @Test + void L자_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FIVE); + Position target = new Position(File.B, Rank.FOUR); + Piece other = Empty.getInstance(); + + assertThatThrownBy(() -> blackPawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("잘못된 위치로 이동하고 있습니다."); + } + + @Test + void 정의되지_않은_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FIVE); + Position target = new Position(File.G, Rank.ONE); + Piece other = Empty.getInstance(); + + assertThatThrownBy(() -> blackPawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("잘못된 위치로 이동하고 있습니다."); + } + + @Test + void 같은_팀의_말을_잡으면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.E, Rank.THREE); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> blackPawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 말을 잡을 수 없습니다."); + } +} diff --git a/src/test/java/domain/piece/pawn/WhitePawnTest.java b/src/test/java/domain/piece/pawn/WhitePawnTest.java new file mode 100644 index 00000000000..3862de649cf --- /dev/null +++ b/src/test/java/domain/piece/pawn/WhitePawnTest.java @@ -0,0 +1,137 @@ +package domain.piece.pawn; + +import domain.piece.Empty; +import domain.piece.Piece; +import domain.position.File; +import domain.position.Position; +import domain.position.Rank; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class WhitePawnTest { + private final Piece whitePawn = new WhitePawn(); + + @Test + void 직선_방향으로_한_칸_이동할_수_있다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.FIVE); + Piece other = Empty.getInstance(); + + assertThatCode(() -> whitePawn.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 앞이_빈_공간이_아니면_직선_방향으로_이동할_수_없다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.FIVE); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> whitePawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("잘못된 위치로 이동하고 있습니다."); + } + + @Test + void 초기_위치_일_때_직선_방향으로_두_칸_이동할_수_있다() { + Position source = new Position(File.D, Rank.TWO); + Position target = new Position(File.D, Rank.FOUR); + Piece other = Empty.getInstance(); + + assertThatCode(() -> whitePawn.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 초기_위치가_아닐_때_직선_방향으로_두_칸_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.THREE); + Position target = new Position(File.D, Rank.FIVE); + Piece other = Empty.getInstance(); + + assertThatThrownBy(() -> whitePawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("잘못된 위치로 이동하고 있습니다."); + } + + @Test + void 대각선_방향에_적이_있으면_이동할_수_있다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.E, Rank.FIVE); + Piece other = new BlackPawn(); + + assertThatCode(() -> whitePawn.validateMovement(source, target, other)) + .doesNotThrowAnyException(); + } + + @Test + void 대각선_방향에_적이_없을_때_이동할_경우_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.E, Rank.FIVE); + Piece other = Empty.getInstance(); + + assertThatThrownBy(() -> whitePawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("잘못된 위치로 이동하고 있습니다."); + } + + @Test + void 대각선_방향으로_두_칸_이동할_경우_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.F, Rank.SIX); + Piece other = new BlackPawn(); + + assertThatThrownBy(() -> whitePawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("잘못된 위치로 이동하고 있습니다."); + } + + @Test + void 뒤로_이동하려고_하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.D, Rank.THREE); + Piece other = Empty.getInstance(); + + assertThatThrownBy(() -> whitePawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("폰은 앞으로 이동해야 합니다."); + } + + @Test + void L자_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.F, Rank.FIVE); + Piece other = Empty.getInstance(); + + assertThatThrownBy(() -> whitePawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("잘못된 위치로 이동하고 있습니다."); + } + + @Test + void 정의되지_않은_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.A, Rank.EIGHT); + Piece other = Empty.getInstance(); + + assertThatThrownBy(() -> whitePawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("잘못된 위치로 이동하고 있습니다."); + } + + @Test + void 같은_팀의_말을_잡으면_예외가_발생한다() { + Position source = new Position(File.D, Rank.FOUR); + Position target = new Position(File.E, Rank.FIVE); + Piece other = new WhitePawn(); + + assertThatThrownBy(() -> whitePawn.validateMovement(source, target, other)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 말을 잡을 수 없습니다."); + } +} diff --git a/src/test/java/domain/position/FileTest.java b/src/test/java/domain/position/FileTest.java new file mode 100644 index 00000000000..662ee0253de --- /dev/null +++ b/src/test/java/domain/position/FileTest.java @@ -0,0 +1,45 @@ +package domain.position; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class FileTest { + @Test + void 존재하지_않은_file명을_찾을_경우_예외가_발생한다() { + assertThatThrownBy(() -> File.fromName("i")) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("존재하지 않은 file입니다."); + } + + @Test + void A와_F_사이에_존재하는_file_목록을_반환한다() { + File source = File.A; + File target = File.F; + + List betweenFiles = source.between(target); + + assertThat(betweenFiles).containsExactlyElementsOf(List.of( + File.B, File.C, File.D, File.E + )); + } + + @Test + void F와_A_사이에_존재하는_file_목록을_반환한다() { + File source = File.F; + File target = File.A; + + List betweenFiles = source.between(target); + + assertThat(betweenFiles).containsExactlyElementsOf(List.of( + File.E, File.D, File.C, File.B + )); + } +} diff --git a/src/test/java/domain/position/RankTest.java b/src/test/java/domain/position/RankTest.java new file mode 100644 index 00000000000..bda4e82d7ba --- /dev/null +++ b/src/test/java/domain/position/RankTest.java @@ -0,0 +1,52 @@ +package domain.position; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class RankTest { + @Test + void 존재하지_않은_rank를_찾을_경우_예외가_발생한다() { + assertThatThrownBy(() -> Rank.fromNumber(0)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("존재하지 않은 rank입니다."); + } + + @Test + void 존재하는_rank를_찾을_경우_해당_rank을_반환한다() { + Rank rank = Rank.fromNumber(1); + + assertThat(rank).isEqualTo(Rank.ONE); + } + + @Test + void ONE와_EIGHT_사이에_존재하는_rank_목록을_반환한다() { + Rank source = Rank.ONE; + Rank target = Rank.EIGHT; + + List betweenRanks = source.between(target); + + assertThat(betweenRanks).containsExactlyElementsOf(List.of( + Rank.TWO, Rank.THREE, Rank.FOUR, Rank.FIVE, Rank.SIX, Rank.SEVEN + )); + } + + @Test + void EIGHT와_ONE_사이에_존재하는_rank_목록을_반환한다() { + Rank source = Rank.EIGHT; + Rank target = Rank.ONE; + + List betweenRanks = source.between(target); + + assertThat(betweenRanks).containsExactlyElementsOf(List.of( + Rank.SEVEN, Rank.SIX, Rank.FIVE, Rank.FOUR, Rank.THREE, Rank.TWO + )); + } +} diff --git a/src/test/java/domain/position/RouteTest.java b/src/test/java/domain/position/RouteTest.java new file mode 100644 index 00000000000..1781870178f --- /dev/null +++ b/src/test/java/domain/position/RouteTest.java @@ -0,0 +1,259 @@ +package domain.position; + +import domain.piece.Piece; +import domain.piece.pawn.WhitePawn; +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 java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class RouteTest { + @Nested + class 경로_생성 { + @Test + void 인접한_UP_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.F, Rank.FIVE); + Position target = new Position(File.F, Rank.SIX); + + Route route = Route.create(source, target); + assertThat(route.getRoute()).isEmpty(); + } + + @Test + void 인접한_LEFT_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.F, Rank.FIVE); + Position target = new Position(File.E, Rank.FIVE); + + Route route = Route.create(source, target); + assertThat(route.getRoute()).isEmpty(); + } + + @Test + void UP_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.F, Rank.FIVE); + Position target = new Position(File.F, Rank.EIGHT); + + Route route = Route.create(source, target); + assertThat(route.getRoute()) + .hasSize(2) + .containsExactlyInAnyOrderElementsOf(List.of( + new Position(File.F, Rank.SIX), + new Position(File.F, Rank.SEVEN) + )); + } + + @Test + void UP_RIGHT_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.F, Rank.FIVE); + Position target = new Position(File.H, Rank.SEVEN); + + Route route = Route.create(source, target); + assertThat(route.getRoute()) + .hasSize(1) + .containsExactly(new Position(File.G, Rank.SIX)); + } + + @Test + void RIGHT_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.F, Rank.FIVE); + Position target = new Position(File.G, Rank.FIVE); + + Route route = Route.create(source, target); + assertThat(route.getRoute()).isEmpty(); + } + + @Test + void DOWN_RIGHT_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.F, Rank.FIVE); + Position target = new Position(File.H, Rank.THREE); + + Route route = Route.create(source, target); + assertThat(route.getRoute()) + .hasSize(1) + .containsExactly(new Position(File.G, Rank.FOUR)); + } + + @Test + void DOWN_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.F, Rank.FIVE); + Position target = new Position(File.F, Rank.TWO); + + Route route = Route.create(source, target); + assertThat(route.getRoute()) + .hasSize(2) + .containsExactlyInAnyOrderElementsOf(List.of( + new Position(File.F, Rank.FOUR), + new Position(File.F, Rank.THREE) + )); + } + + @Test + void DOWN_LEFT_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.F, Rank.FIVE); + Position target = new Position(File.C, Rank.TWO); + + Route route = Route.create(source, target); + assertThat(route.getRoute()) + .hasSize(2) + .containsExactlyInAnyOrderElementsOf(List.of( + new Position(File.E, Rank.FOUR), + new Position(File.D, Rank.THREE) + )); + } + + @Test + void LEFT_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.F, Rank.FIVE); + Position target = new Position(File.C, Rank.FIVE); + + Route route = Route.create(source, target); + assertThat(route.getRoute()) + .hasSize(2) + .containsExactlyInAnyOrderElementsOf(List.of( + new Position(File.E, Rank.FIVE), + new Position(File.D, Rank.FIVE) + )); + } + + @Test + void UP_LEFT_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.E, Rank.FOUR); + Position target = new Position(File.A, Rank.EIGHT); + + Route route = Route.create(source, target); + assertThat(route.getRoute()) + .hasSize(3) + .containsExactlyInAnyOrderElementsOf(List.of( + new Position(File.D, Rank.FIVE), + new Position(File.C, Rank.SIX), + new Position(File.B, Rank.SEVEN) + )); + } + + @Test + void UP_UP_LEFT_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.E, Rank.FOUR); + Position target = new Position(File.D, Rank.SIX); + + Route route = Route.create(source, target); + assertThat(route.getRoute()).isEmpty(); + } + + @Test + void UP_UP_RIGHT_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.E, Rank.FOUR); + Position target = new Position(File.F, Rank.SIX); + + Route route = Route.create(source, target); + assertThat(route.getRoute()).isEmpty(); + } + + @Test + void RIGHT_RIGHT_UP_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.E, Rank.FOUR); + Position target = new Position(File.G, Rank.FIVE); + + Route route = Route.create(source, target); + assertThat(route.getRoute()).isEmpty(); + } + + @Test + void RIGHT_RIGHT_DOWN_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.E, Rank.FOUR); + Position target = new Position(File.G, Rank.THREE); + + Route route = Route.create(source, target); + assertThat(route.getRoute()).isEmpty(); + } + + @Test + void DOWN_DOWN_RIGHT_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.E, Rank.FOUR); + Position target = new Position(File.F, Rank.TWO); + + Route route = Route.create(source, target); + assertThat(route.getRoute()).isEmpty(); + } + + @Test + void DOWN_DOWN_LEFT_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.E, Rank.FOUR); + Position target = new Position(File.D, Rank.TWO); + + Route route = Route.create(source, target); + assertThat(route.getRoute()).isEmpty(); + } + + @Test + void LEFT_LEFT_DOWN_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.E, Rank.FOUR); + Position target = new Position(File.C, Rank.THREE); + + Route route = Route.create(source, target); + assertThat(route.getRoute()).isEmpty(); + } + + @Test + void LEFT_LEFT_UP_방향으로_이동하는_경로를_반환한다() { + Position source = new Position(File.E, Rank.FOUR); + Position target = new Position(File.C, Rank.FIVE); + + Route route = Route.create(source, target); + assertThat(route.getRoute()).isEmpty(); + } + + @Test + void LEFT_LEFT_LEFT_UP_방향으로_이동하면_예외가_발생한다() { + Position source = new Position(File.E, Rank.FOUR); + Position target = new Position(File.B, Rank.FIVE); + + assertThatThrownBy(() -> Route.create(source, target)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("잘못된 방향으로 이동하고 있습니다."); + } + } + + @Nested + class 경로_막힘 { + @Test + void 경로는_다른_피스가_없으면_막히지_않는다() { + Position source = new Position(File.F, Rank.FIVE); + Position target = new Position(File.F, Rank.SEVEN); + Route route = Route.create(source, target); + + Map emptyBoard = Map.of(); + + assertThat(route.isBlocked(emptyBoard)).isFalse(); + } + + @Test + void 도착_지점에_폰이_있어도_경로는_막히지_않는다() { + Position source = new Position(File.F, Rank.FIVE); + Position target = new Position(File.F, Rank.SIX); + Route route = Route.create(source, target); + + Map board = Map.of(target, new WhitePawn()); + + assertThat(route.isBlocked(board)).isFalse(); + } + + @Test + void 출발_지점과_도착_지점_사이에_폰이_있으면_경로가_막힌다() { + Position source = new Position(File.F, Rank.FIVE); + Position target = new Position(File.C, Rank.FIVE); + Route route = Route.create(source, target); + + Map board = Map.of(new Position(File.D, Rank.FIVE), new WhitePawn()); + + assertThat(route.isBlocked(board)).isTrue(); + } + } +} diff --git a/src/test/java/repository/GameStateMockDao.java b/src/test/java/repository/GameStateMockDao.java new file mode 100644 index 00000000000..0ca2c02f5e9 --- /dev/null +++ b/src/test/java/repository/GameStateMockDao.java @@ -0,0 +1,24 @@ +package repository; + +import database.dao.GameStateDao; +import dto.StateDto; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class GameStateMockDao implements GameStateDao { + private final Map repository = new HashMap<>(); + + public void add(StateDto stateDto) { + repository.put(stateDto.gameId(), stateDto); + } + + public Optional findByGameId(final int gameId) { + return Optional.ofNullable(repository.get(gameId)); + } + + public void deleteByGameId(final int gameId) { + repository.remove(gameId); + } +} diff --git a/src/test/java/repository/PieceMockDao.java b/src/test/java/repository/PieceMockDao.java new file mode 100644 index 00000000000..8c766ebaf55 --- /dev/null +++ b/src/test/java/repository/PieceMockDao.java @@ -0,0 +1,40 @@ +package repository; + +import database.dao.PieceDao; +import dto.PieceDto; +import dto.RoomDto; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class PieceMockDao implements PieceDao { + private Map repository = new HashMap<>(); + + public void add(final RoomDto room, final PieceDto piece) { + String key = generateKey(room, piece); + repository.put(key, piece); + } + + public List findPieceByGameId(final int gameId) { + return repository.entrySet().stream() + .filter(entry -> extractGameIdFromKey(entry.getKey()) == gameId) + .map(Map.Entry::getValue) + .toList(); + } + + public void deleteAllByGameId(final int gameId) { + repository = repository.entrySet().stream() + .filter(entry -> extractGameIdFromKey(entry.getKey()) != gameId) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private String generateKey(RoomDto room, PieceDto piece) { + return room.roomId() + "-" + piece.boardFile() + "-" + piece.boardRank(); + } + + private int extractGameIdFromKey(String key) { + return Integer.parseInt(key.split("-")[0]); + } +} diff --git a/src/test/java/repository/RoomMockDao.java b/src/test/java/repository/RoomMockDao.java new file mode 100644 index 00000000000..e74b144aeda --- /dev/null +++ b/src/test/java/repository/RoomMockDao.java @@ -0,0 +1,50 @@ +package repository; + +import database.dao.RoomDao; +import dto.RoomDto; +import dto.StateDto; +import dto.UserDto; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class RoomMockDao implements RoomDao { + private final Map roomRepository; + private final Map turnRepository; + + public RoomMockDao(Map roomRepository, Map turnRepository) { + this.roomRepository = roomRepository; + this.turnRepository = turnRepository; + } + + public void add(UserDto userDto, RoomDto roomDto) { + roomRepository.put(roomDto, userDto); + } + + public Optional addNewRoom(UserDto userDto) { + int newRoomId = roomRepository.keySet() + .stream() + .mapToInt(RoomDto::roomId) + .max() + .orElse(0) + 1; + RoomDto newRoom = new RoomDto(newRoomId); + roomRepository.put(newRoom, userDto); + return Optional.of(newRoom); + } + + public Optional find(final String roomId) { + return roomRepository.keySet().stream() + .filter(room -> room.roomId() == Integer.parseInt(roomId)) + .findFirst(); + } + + public List findActiveRoomAll(final UserDto user) { + return roomRepository.entrySet().stream() + .filter(entry -> entry.getValue().equals(user)) + .filter(entry -> !Objects.equals(turnRepository.get(entry.getKey().roomId()).state(), "GAMEOVER")) + .map(Map.Entry::getKey) + .toList(); + } +} diff --git a/src/test/java/service/ChessGameServiceTest.java b/src/test/java/service/ChessGameServiceTest.java new file mode 100644 index 00000000000..21b32c1a436 --- /dev/null +++ b/src/test/java/service/ChessGameServiceTest.java @@ -0,0 +1,92 @@ +package service; + +import domain.ChessGame; +import domain.board.ChessBoard; +import domain.piece.Color; +import domain.piece.Piece; +import domain.piece.nonpawn.King; +import domain.piece.pawn.WhitePawn; +import domain.position.Position; +import dto.PieceDto; +import dto.RoomDto; +import dto.StateDto; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import database.dao.GameStateDao; +import repository.GameStateMockDao; +import database.dao.PieceDao; +import repository.PieceMockDao; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ChessGameServiceTest { + private static final PieceDto A2WhitePawn = new PieceDto("A", "2", "WHITE", "PAWN"); + private static final PieceDto B2WhitePawn = new PieceDto("B", "2", "WHITE", "PAWN"); + private static final PieceDto C2WhitePawn = new PieceDto("C", "2", "WHITE", "PAWN"); + private static final RoomDto room1 = new RoomDto(1); + + private final PieceDao pieceDao = new PieceMockDao(); + private final GameStateDao gameStateDao = new GameStateMockDao(); + private final ChessGameService chessGameService = new ChessGameService(pieceDao, gameStateDao); + + @BeforeEach + void setData() { + pieceDao.add(room1, A2WhitePawn); + pieceDao.add(room1, B2WhitePawn); + pieceDao.add(room1, C2WhitePawn); + gameStateDao.add(new StateDto("WHITE", 1)); + } + + @AfterEach + void rollback() { + pieceDao.deleteAllByGameId(1); + gameStateDao.deleteByGameId(1); + } + + @Test + void 이전_게임의_피스_데이터를_불러온다() { + ChessGame chessGame = chessGameService.initializeChessGame(room1); + + assertThat(chessGame.getBoard().getPieces()) + .containsExactlyInAnyOrder(A2WhitePawn, B2WhitePawn, C2WhitePawn); + } + + @Test + void 이전_게임의_턴_데이터를_불러온다() { + ChessGame chessGame = chessGameService.initializeChessGame(room1); + + assertThat(chessGame.getTurn()) + .isEqualTo(Color.WHITE); + } + + @Test + void 게임_데이터를_갱신한다() { + RoomDto roomDto = room1; + Position A3 = new Position("A3"); + Position B3 = new Position("B3"); + Position C3 = new Position("C3"); + Map pawnMap = Map.of( + A3, new King(Color.WHITE), + B3, new King(Color.BLACK), + C3, new WhitePawn()); + ChessGame chessGame = new ChessGame(new ChessBoard(pawnMap)); + + chessGameService.saveChessGame(chessGame, roomDto); + chessGame = chessGameService.initializeChessGame(roomDto); + + assertThat(chessGame.getBoard().getPieces()) + .containsExactlyInAnyOrder( + PieceDto.of(A3, new King(Color.WHITE)), + PieceDto.of(B3, new King(Color.BLACK)), + PieceDto.of(C3, new WhitePawn())); + assertThat(chessGame.getBoard().getTurn()) + .isEqualTo(Color.WHITE); + } +} diff --git a/src/test/java/service/GameRoomServiceTest.java b/src/test/java/service/GameRoomServiceTest.java new file mode 100644 index 00000000000..be5585db203 --- /dev/null +++ b/src/test/java/service/GameRoomServiceTest.java @@ -0,0 +1,63 @@ +package service; + +import database.dao.RoomDao; +import dto.RoomDto; +import dto.StateDto; +import dto.UserDto; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import repository.RoomMockDao; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class GameRoomServiceTest { + private final RoomDto room1 = new RoomDto(1); + private final RoomDto room2 = new RoomDto(2); + private final RoomDto room3 = new RoomDto(3); + private final UserDto MANGCHO = new UserDto("mangcho"); + + private final Map roomRepository = Map.of( + room1, MANGCHO, + room2, MANGCHO, + room3, MANGCHO); + + private final Map turnRepository = Map.of( + room1.roomId(), new StateDto("GAMEOVER", room1.roomId()), + room2.roomId(), new StateDto("WHITE", room2.roomId()), + room3.roomId(), new StateDto("BLACK", room3.roomId())); + + @Test + void 활성화된_모든_방을_불러온다() { + RoomDao roomDao = new RoomMockDao(roomRepository, turnRepository); + GameRoomService gameRoomService = new GameRoomService(roomDao); + + assertThat(gameRoomService.loadActiveRoomAll(MANGCHO)) + .containsExactlyInAnyOrder(room2, room3); + } + + @Test + void ID로_방을_찾는다() { + RoomDao roomDao = new RoomMockDao(roomRepository, turnRepository); + GameRoomService gameRoomService = new GameRoomService(roomDao); + + RoomDto findRoom = gameRoomService.findRoomById("1"); + + assertThat(findRoom).isEqualTo(room1); + } + + @Test + void 세번째_방까지_생성된_시점에_새로_생성한_방은_4번_방이다() { + RoomDao roomDao = new RoomMockDao(new HashMap<>(roomRepository), new HashMap<>()); + GameRoomService gameRoomService = new GameRoomService(roomDao); + + RoomDto newRoom = gameRoomService.createNewRoom(MANGCHO); + + assertThat(newRoom).isEqualTo(new RoomDto(4)); + } +}