diff --git a/.gitignore b/.gitignore index 6c01878138..e08e0dce99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ HELP.md .gradle +.gradle-user-home/ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/** @@ -30,3 +31,5 @@ out/ ### VS Code ### .vscode/ +docs/record_memory.md +docs/pr_content.md diff --git a/README.md b/README.md index 9775dda0ae..7e3f380d02 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,266 @@ # java-janggi 장기 미션 저장소 + + +## 구현 흐름 + +전체 게임 진행 흐름은 아래 플로우 차트를 기준으로 설계했다. + +- 보드 세팅 후 턴이 시작된다. +- 현재 턴의 팀(`TeamColor`)은 자신의 기물 중 하나를 선택한다. +- 선택한 기물의 이동 가능 후보 위치를 확인한 뒤 목적지를 선택한다. +- 이동 이후 포획 여부, 점수, 장군/외통수 여부를 순서대로 판정한다. +- 시간 초과 여부를 확인하고, 종료 조건이 아니라면 현재 장기판 상태를 출력한 뒤 턴을 교체한다. + +## 프로젝트 구조 + + + + + + +
+ + + +
+ 게임 흐름도 +
+ + + +
+ 클래스 다이어그램 +
+ +### 주요 도메인 객체 설명 + +- `Board` + - 현재 장기판 상태를 관리하는 중심 객체 + - `Map`를 사용해 특정 위치의 기물 조회, 특정 기물의 현재 위치 조회, 이동 가능 경로 검증, 실제 이동 반영을 담당 + - 장기판의 "현재 상태"와 그 상태를 기준으로 한 판정을 한곳에 모으기 위해 별도 객체로 두었다. +- `Position` + - 장기판 위 한 칸의 위치를 표현 + - 유효한 행(Row)과 열(Column) 범위를 검증하는 값 객체로 사용 + - 보드의 거의 모든 규칙이 위치를 기준으로 동작하므로, 좌표를 원시값으로 흩뿌리지 않기 위해 분리했다. +- `Route` + - 기물의 실제 이동 경로를 나타낸다. + - 시작 위치, 도착 위치, 그리고 중간에 지나가는 칸들을 함께 가진다. + - 장기에서는 "어디로 가는가", "중간에 무엇을 지나가는가" 두 가지 모두 중요하므로, 도착지만 표현하는 대신 경로 전체를 표현하는 정보를 묶어서 객체로 다룬다. +- `MovePath` + - 기물이 따르는 이동 규칙 템플릿 + - ex. 마와 상처럼 여러 단계 방향 조합으로 움직이는 기물의 규칙을 표현하는 데 사용 + - 실제 장기판 좌표가 계산된 결과는 `Route`, 규칙 자체는 `MovePath`로 나누어 규칙 정의와 실행 결과를 분리 +- `Piece` + - 장기 기물 하나를 의미 + - 자신의 진영(`TeamColor`), 종류(`PieceType`), 이동 규칙(`MoveStrategy`)을 함께 가진다. + - 기물마다 이동 방식이 다르기 때문에, 기물 종류와 전략을 연결하는 축으로 두었다. +- `PieceType`, `TeamColor` + - 기물의 종류와 진영을 표현하는 분류 체계 + - 문자열 비교나 매직 넘버 대신 명확한 도메인 값으로 다루기 위해 분리 +- `MoveStrategy` + - 기물별 이동 규칙을 추상화한 전략 인터페이스 + - 차, 포, 마, 상, 왕, 사, 졸이 서로 다른 이동 규칙을 가지므로, 조건문을 한곳에 몰아넣지 않고 전략 객체로 분리 +- `InitialFormationStrategy` + - 게임 시작 시 초/한의 상차림 규칙을 표현하는 전략 + - 안상, 바깥상, 좌상, 우상처럼 초기 배치 변형이 존재하므로, 초기 세팅 규칙을 독립된 전략으로 분리 +- `BoardInitializer` + - 선택된 상차림 전략을 받아 초기 장기판 상태를 조립 + - 초기 기물 생성과 보드 생성 과정을 게임 진행 객체와 분리해, 시작 세팅 책임을 독립시켰다. +- `TurnManager` + - 현재 턴의 진영 정보 저장 및 턴 전환만 관리하는 객체 +- `GameRunner` + - 입력, 출력, 초기 세팅, 턴 진행을 연결해 실제 게임 루프를 실행 + - 도메인 규칙 자체를 담기보다, 여러 도메인 객체를 조합해 사용자와 상호작용하는 흐름을 담당 + +### 사이클1 구현 내용 + +- [x] [기능구현] 게임에서 사용할 분류 체계 정의 + - 전역에서 공통으로 사용하는 값의 종류를 정리 + - ex) 팀 구분, 기물 종류, 게임 상태 + +- [x] [기능구현][예외처리] 장기판에서 사용할 좌표 체계를 정의 + - `Position`, `Direction`, `Route`가 어떤 의미를 가지는지 먼저 확정 + - 좌표 체계는 `Position.of(row, column)` 기준으로 정리 + - 행은 위에서 아래로 `0..9`, 열은 왼쪽에서 오른쪽으로 `0..8` + - [예외처리] 유효하지 않은 좌표 생성 방지 + +- [x] [기능구현] 기물의 이동 규칙을 어떤 방식으로 표현할지 확정 + - 이동 규칙을 `MovePath`와 `Route` 생성 기반으로 표현 + - 단순 방향 목록뿐 아니라 중간 경로까지 포함하도록 정리 + - 궁성 영역 관련 규칙은 제외 + +- [x] [기능구현] 기물이 자신의 팀, 종류, 이동 규칙을 가지도록 공통 구조를 만든다 + - 모든 기물이 공통적으로 가져야 하는 속성과 동작을 정의 + - `Piece`가 `TeamColor`, `PieceType`, `MoveStrategy`를 조합하도록 구성 + +- [x] [기능구현] 각 기물을 게임에서 사용할 수 있는 형태로 구체화 + - 상, 마, 차, 졸, 사, 왕, 포가 생성 가능하도록 구현 + - 생성 시 팀 정보와 이동 규칙이 자연스럽게 연결되도록 구성 + +- [x] [상태관리][예외처리] 장기판의 현재 기물 배치 상태 저장, 조회 기능 구현 + - `Map` 기반으로 현재 배치 상태를 저장하고 조회 + - 특정 위치의 기물 조회, 특정 기물의 현재 위치 조회 가능 + - [예외처리] 보드에 없는 기물 이동 시 예외 처리 + +- [x] [초기화] 게임 시작 시 장기판을 초기 상태로 세팅 + - 초기 기물 생성 + - 초기 배치 좌표 정의 + - 초기 장기판 세팅 + +- [x] [기능구현] 규칙이 단순한 기물의 이동 가능 후보 경로를 계산 + - `졸`, `사`, `왕`의 후보 경로 계산 구현 + - 궁성 영역 관련 규칙은 제외 + +- [x] [기능구현] 규칙이 복잡한 기물의 이동 가능 후보 경로를 계산 + - `차`, `포`, `마`, `상`의 후보 경로 계산 구현 + - 차와 포는 직선 장거리 경로를 생성하도록 구현 + +- [x] [판정] 장기판이 빈 칸 여부와 경로 차단 여부를 판단하는 기능 구현 + - 목적지 점유 여부와 이동 경로 중간 칸 차단 여부를 함께 판단 + - 포의 다리 규칙도 중간 경로 정보를 활용해 판정 + +- [x] [판정][예외처리] 기물이 선택한 목적지로 실제 이동 가능한지 판정하는 기능 구현 + - 기물이 만든 이동 후보와 현재 장기판 상태를 함께 고려 + - 아군이 있는 칸인지, 적군이 있는 칸인지, 중간 경로가 막혔는지 등을 종합적으로 판정 + - 궁성 관련 판정은 제외 + - [예외처리] 이동 불가능한 경우 명확히 거부 + +- [x] [기능구현][상태관리][예외처리] 장기판에 기물을 배치하고 이동하는 기능 구현 + - 초기 배치와 실제 게임 중 이동 모두 처리할 수 있도록 구현 + - [x] [상태관리] 이동 후 보드 상태 갱신 + - [예외처리] 잘못된 이동 요청(범위 밖, 규칙 위반) 방지 + +- [x] [출력] 초기 장기판 상태를 출력할 수 있도록 + - 초기화 결과를 콘솔 보드 형태로 확인 가능 + +- [x] [출력] 특정 기물의 이동 후보 또는 이동 결과를 확인할 수 있도록 + - 선택 가능한 기물 목록 출력 + - 선택한 기물의 이동 후보 경로 출력 + - 이동 결과 출력 + +- [x] [상태관리] `TeamColor` 기반으로 현재 턴과 진영 상태를 관리 + - 별도의 Player 객체 없이 현재 턴 팀을 기준으로 게임 흐름을 제어 + - 현재 턴 확인 및 턴 교체 구현 + +- [x] [기능구현][상태관리] 포획이 발생했을 때 게임 상태 갱신 + - 이동한 목적지에 상대 기물이 있으면 기존 기물을 덮어써 포획 처리 + - [x] [상태관리] `Map` 보드 상태를 기준으로 점유 정보를 갱신 + +- [ ] [기능구현] 현재 보드 상태를 기준으로 팀별 점수를 계산 + - 시간 초과 시 `Map`에 남아 있는 기물을 기준으로 점수를 계산 + +- [x] [게임흐름] 턴 교체 + - 현재 턴 `TeamColor` 확인 + - 다음 턴 `TeamColor` 확인 + +- [x] [게임흐름][입력][출력] 최소 플레이가 가능한 게임 루프 생성 +- [x] [출력] 현재 턴 팀의 기물 목록 출력 + - [x] [입력] 기물 선택 입력 처리 + - [x] [출력] 이동 후보 위치 출력 + - [x] [입력] 목적지 선택 입력 처리 + - [x] [기능구현] 이동 수행 + - [x] [출력] 현재 장기판 상태 출력 + - [x] [게임흐름] 턴 교체 + - 장군/외통수/시간 제한 배제하고 한 턴씩 진행 되도록 + +- [x] [입력][게임흐름][예외처리] 잘못된 입력이 들어왔을 때 같은 흐름 안에서 다시 선택할 수 있도록 + - [x] [입력] 존재하지 않는 기물 선택 + - [x] [입력] 이동 불가능한 목적지 선택 + - [x] [입력] 잘못된 형식의 입력 처리 + - [x] [예외처리] 입력 오류 발생 시 재입력 유도 + - [x] [게임흐름] 입력 오류가 났을 때 게임 전체가 깨지지 않고 현재 단계만 다시 진행되도록 + +--- + +### 사이클2 구현 내용 + + + +- [ ] [판정] 장군 멍군 구현 + - 현재 보드 상태 기준으로 장군 여부를 판단 + - 한 턴 종료 후 필수적으로 확인 + +- [ ] [판정] 장군 상태에서 벗어날 수 있는 수가 있는지 판정할 수 있도록 + - 외통수 여부를 판단 + - 외통수면 즉시 승리 및 게임 종료로 연결 + +- [ ] [게임흐름][판정] 게임 종료 조건이 발생했을 때 게임 종료하도록 + - 외통수로 종료 + - 점수 판정으로 종료 + - 종료 시 더 이상 턴이 진행되지 않도록 상태 변경 + +- [ ] [게임흐름] 시간 제한 규칙을 게임 흐름에 포함 + - 전체 시간 기준 + - 게임 시작 후 시간 측정 시작, 매 턴 종료 시 보드 상태 출력 후 시간 공지 + +- [ ] [판정][게임흐름] 시간 초과 시 점수로 승패를 판정 + - 시간 초과 감지 + - 각 플레이어 점수 계산 + - 승패 판정 + - 게임 종료 처리 +--- + +## TDD 시나리오 + +### Position 시나리오 +- 장기판 범위 내의 좌표로 생성할 시 정상 생성. +- 장기판 범위 밖의 좌표로 생성할 시에는 예외 처리 +- 행(`row`)과 열(`column`) 범위를 각각 검증 + + +### JanggiBoard 시나리오 +- 보드 상태는 `Map` 형태로 관리한다. +- Initializer로부터 초기화되었을 때 모든 말들의 위치정보를 가지고 있다. +- 기물과 현재 판 상태를 기반으로 이동 가능한지 여부를 반환한다. + - 범위를 초과하면 이동할 수 없다 + - 이동하고자 하는 곳에 같은 팀 기물이 있으면 이동할 수 없다 + - 이동 경로에 다른 기물이 있으면 규칙에 맞지 않는 경우 이동할 수 없다 + +### MoveStrategy 시나리오 +- 각 말들의 MoveStrategy가 우리가 의도한 규칙에 맞게 `MovePath`를 생성한다. + - 졸은 전진과 좌우 한 칸 + - 차는 상하좌우 장거리 + - 포는 상하좌우 장거리 + 다리 규칙 +- 같은 팀 말이 있는 도착지로는 이동할 수 없다. + - 상 + - 마 + - 차 + - 졸 + - 포: intermediateRoutes에서 가져온 말들의 개수가 1이고, 포가 아닌 경우만 이동 가능 + - 사 + - 왕 +- 장기 보드와 협력하여 이동 가능한 위치 정보를 받아 루트 정보들을 생성한다. + +### Initializer 시나리오 +- 게임 초기의 말들의 위치 정보를 `Map` 형태로 가지고 있다. +- `Position(row, column)` 기준으로 초/한의 초기 배치를 올바르게 구성한다. + +### TurnManager 시나리오 +- 현재 턴의 `TeamColor`가 무엇인지 반환한다. +- 턴이 바뀌면 `CHO`와 `HAN`이 스왑된다. +- 별도의 Player 객체 없이 팀 색상만으로 턴을 관리한다. + + +### Runner 시나리오 +- 기물 선택, 이동 위치 선택 입/출력을 진행한다. +- Initializer에게 `Map` 기반 장기판을 세팅하는 함수를 호출한다. +- 현재 턴 팀이 정상적인 기물을 선택하고, 정상적인 이동 목적지를 입력하면 보드 상태를 갱신한다. +- TurnManager에게 턴을 교체하는 함수를 호출한다. +- 현재 턴 팀이 정상적인 기물 번호를 선택하지 않으면, 예외를 발생하고 기물 선택으로 돌아온다. +- 현재 턴 팀이 정상적인 이동 목적지를 입력하지 않으면, 예외를 발생하고 현재 턴 흐름 안에서 다시 선택한다. + + + + + + +## 미션 중 기록 + +- 상태 위치를 결정할 때 고민한 순간 1회 +- 불변/캡슐화를 적용한 코드 1곳 +- 규칙 적용으로 변경한 설계 1곳 +- 조건문을 다형성으로 대체한 코드 1곳 +- 인터페이스/추상경 클래스를 도입한 이유 +- 새 기물 추가 시 변경 범위 테스트 (가상으로) diff --git a/docs/cycle1_mission_record.md b/docs/cycle1_mission_record.md new file mode 100644 index 0000000000..a15906b805 --- /dev/null +++ b/docs/cycle1_mission_record.md @@ -0,0 +1,268 @@ +# Cycle1 Mission Record + +현재 코드 기준으로 사이클1 미션 기록 항목을 정리했다. 각 항목은 실제 구현 코드 일부를 스니펫으로 인용해 근거를 남겼다. + +## 1. 상태 위치를 결정할 때 고민한 순간 1회 + +규칙에 따른 말의 이동을 구현하기 위해서는 "현재 위치에서 말의 규칙을 따라 어디까지 갈 수 있는가"를 다룰 객체 `Route`가 필요했다. 이 고민은 `MoveStrategy.makeRoutes()`에 담겨있다. + +단순히 방향만 나열하는 것이 아니라 다음 세 가지를 함께 다뤄야 했다. + +- 현재 위치에서 한 단계씩 좌표를 진행할 것 +- 중간 경로를 별도로 모을 것(장애물 여부에 따라 이동 가능성 체크) +- 보드 범위를 벗어나는 경로는 버릴 것 + +이 고민의 결과가 아래 코드다. + +```java +default List makeRoutes(Position curPos, TeamColor teamColor) { + List validRoutes = new ArrayList<>(); + List paths = getPaths(teamColor); + + for (MovePath path : paths) { + createRoute(curPos, path.steps()).ifPresent(validRoutes::add); + } + + return validRoutes; +} +``` + +여기서는 "좌표 하나"가 아니라 "시작점, 도착점, 중간 경로"를 모두 가진 `Route`를 만들도록 설계를 밀어 올린 점이 중요했다. + +## 2. 불변/캡슐화를 적용한 코드 1곳 + +불변과 캡슐화를 적용하기 위한 기준은 + +> 객체가 어떤 동작을 수행하기보다 값을 표현하는 역할이라면 불변 객체로 설계한다. + +였다. 이 기준을 현재 코드에서 가장 잘 보여주는 예시는 `MovePath`, `Route`, `Row`, `Column`이다. + +### record로 만든 값 객체 + +```java +public record MovePath(List steps) {} +``` + +```java +public record Route(Position startPos, Position endPos, List intermeidateNodes) {} +``` + +`MovePath`와 `Route`는 "이동 규칙을 설명하는 값"이지, '스스로 상태를 변경하며 행동하는 객체'가 아니다. 그래서 `record`로 두고 setter 없이 불변 값처럼 사용했다. + +Position도 record로 두고 필요한 메서드는 추가로 구현하면 되지 않을까 생각했으나, 공식 문서를 기반으로 "데이터만 담는 클래스"를 간결하게 만들기 위한 문법이라는 것을 확인하고 도메인 규칙을 알고 행동(보드 크기에 맞는 값으로 검증)해야 하는 Position은 클래스로 유지했다. + + +```java +public class Position { + private final Row row; + private final Column column; + + private Position(Row row, Column column) { + this.row = row; + this.column = column; + } + + public static Position of(int row, int column) { + return new Position(new Row(row), new Column(column)); + } +} +``` + +`Position`도 내부 필드를 `final`로 두고, 직접 원시값을 받지 않고 `Row`, `Column`을 조합해서 생성한다는 점에서 값 객체 성격을 유지하고 있다. + +## 3. 규칙 적용으로 변경한 설계 1곳 + +이번 미션에서 적용한 규칙 중 하나는 + +### 불변 객체 기준 + +- **If:** 객체가 어떤 동작을 수행하기보다 ‘값’을 표현하는 역할이라면 + → **Then:** 해당 객체는 불변 객체로 설계한다. +- 기준 + - 생성 이후 상태가 변경되지 않아야 한다. + - setter를 제공하지 않는다. + - 모든 필드는 final로 선언하는 것을 기본으로 한다. + +이 규칙을 적용하면서 "값이 없음"을 `null` 대신 `Optional`로 다루도록 수정했다. + +예를 들어 보드 출력에서는 원래 빈 칸을 표현하기 위해 `orElse(null)`을 사용했지만, 현재는 `Optional`를 그대로 전달한다. + +```java +for (int column = 0; column <= 8; column++) { + Optional piece = board.findPiece(Position.of(row, column)); + line.append(" ").append(formatBoardCell(piece)).append(" │"); +} +``` + +```java +private String formatBoardCell(Optional piece) { + if (piece.isEmpty()) { + return " "; + } + Piece actualPiece = piece.get(); + ... +} +``` + +또한 중간 기물 조회도 `Map.get()` 결과를 직접 `null` 비교하지 않고 `Optional::stream`으로 처리한다. + +```java +public List getBlockingPieces(Route route) { + return route.intermeidateNodes().stream() + .map(this::findPiece) + .flatMap(Optional::stream) + .toList(); +} +``` + +반면 `Position.equals(Object o)`의 `o == null` 검사는 자바의 동등성 비교 계약을 지키기 위한 표준 구현으로 보고 유지했다. 이 코드는 도메인 상태를 `null`로 표현하는 것이 아니라, 외부에서 전달된 비교 대상의 유효성을 검사하는 로직이기 때문에 유지하기로 했다. + +## 4. 조건문을 다형성으로 대체한 코드 1곳 + +가장 대표적인 부분은 `MoveStrategy` 구조다. 기물별 이동 규칙을 `if-else`로 한 클래스에 몰아넣지 않고, 전략 객체로 분리했다. + +`Piece`는 실제 이동 계산을 직접 하지 않고 `moveStrategy`에 위임한다. + +```java +public class Piece { + private final TeamColor teamColor; + private final PieceType pieceType; + private final MoveStrategy moveStrategy; + + public List makeRoutes(Position from) { + return moveStrategy.makeRoutes(from, teamColor); + } + + public boolean canMove(Route route, List blockingPieces, Optional destinationPiece) { + return moveStrategy.canMove(route, blockingPieces, destinationPiece, teamColor); + } +} +``` + +기물별 규칙은 각 전략 클래스가 담당한다. + +```java +public class PawnMoveStrategy implements MoveStrategy { + @Override + public List getPaths(TeamColor teamColor) { + if (teamColor == TeamColor.CHO) { + return List.of( + new MovePath(List.of(Direction.NORTH)), + new MovePath(List.of(Direction.EAST)), + new MovePath(List.of(Direction.WEST)) + ); + } + + return List.of( + new MovePath(List.of(Direction.SOUTH)), + new MovePath(List.of(Direction.EAST)), + new MovePath(List.of(Direction.WEST)) + ); + } +} +``` + +```java +public class RookMoveStrategy implements MoveStrategy { + @Override + public List getPaths(TeamColor teamColor) { + List paths = new ArrayList<>(); + addStraightPaths(paths, Direction.NORTH); + addStraightPaths(paths, Direction.SOUTH); + addStraightPaths(paths, Direction.EAST); + addStraightPaths(paths, Direction.WEST); + return paths; + } +} +``` + +`Piece.createMoveStrategy()`도 현재는 `EnumMap` 조회로 전략을 연결하고 있다. 핵심 이동 규칙 자체는 `MoveStrategy` 다형성으로 분리되어 있고, 실제 행동 차이는 각 구현체가 맡고 있다는 점에서 "한 곳에서 모든 규칙을 조건문으로 처리하던 구조"보다 책임이 명확해졌다. + +## 5. 인터페이스/추상클래스를 도입한 이유 + +### MoveStrategy 인터페이스를 도입한 이유 + +기물마다 이동 방식은 다르지만, 외부에서 볼 때 공통적인 동작이 존재했다. + +- 이동 경로를 만든다. +- 현재 보드 상태를 기준으로 실제 이동 가능 여부를 판단한다. + +이 공통으로 이루어지는 동작을 `MoveStrategy` 인터페이스로 추상화 했다. + +```java +public interface MoveStrategy { + + List getPaths(TeamColor teamColor); + + default List makeRoutes(Position curPos, TeamColor teamColor) { ... } + + default boolean canMove(Route route, List blockingPieces, Optional destinationPiece, TeamColor myTeam) { + if (!blockingPieces.isEmpty()) { + return false; + } + + return destinationPiece.isEmpty() || destinationPiece.get().getTeamColor() != myTeam; + } +} +``` + +이렇게 해두면 `Piece`는 "이 말이 졸인지 차인지"를 몰라도 된다. 그냥 `moveStrategy.makeRoutes(...)`, `moveStrategy.canMove(...)`만 호출하면 된다. + +여기서 기물 종류가 늘어나더라도 `Piece`가 협력하는 방식은 유지할 수 있다. + +### InitialFormationStrategy 추상클래스를 도입한 이유 + +상차림 선택지마다 달라지는 것은 상,마의 배치 뿐이고, 이 두 기물을 제외하고 공통으로 고정되는 배치는 같았다. + +이 공통 부분을 추상클래스로 끌어올렸다. + +```java +public abstract class InitialFormationStrategy { + + public final Map setUpPieces(TeamColor teamColor) { + Map formationPieces = setupFormation(teamColor); + Map fixedPieces = placeFixedPieces(teamColor); + Map allPieces = new HashMap<>(); + + allPieces.putAll(formationPieces); + allPieces.putAll(fixedPieces); + + return allPieces; + } + + protected abstract Map setupFormation(TeamColor teamColor); +} +``` + +그리고 실제 차이는 하위 클래스에서만 구현하도록 했다. + +```java +public class InnerFormationStrategy extends InitialFormationStrategy { + @Override + protected Map setupFormation(TeamColor teamColor) { + Map formation = new HashMap<>(); + int row; + if (teamColor.equals(TeamColor.CHO)) { + row = 9; + formation.put(Position.of(row, 1), Piece.of(teamColor, PieceType.HORSE)); + formation.put(Position.of(row, 2), Piece.of(teamColor, PieceType.ELEPHANT)); + ... + } + ... + return formation; + } +} +``` + +"공통 초기화 절차"를 고정하고, 전략별 차이만 하위 클래스에 위임하도록 추상 클래스를 도입하였다고 할 수 있다. + +## 6. 새 기물 추가 시 변경 범위 테스트(가상) + +현재 `PieceType`에 새로운 기물을 하나 추가한다고 가정하면, 지금 구조에서는 변경 범위가 크지 않다. + +우선 새 `PieceType` 값을 추가하고, 해당 기물의 이동 규칙을 담을 `MoveStrategy` 구현체를 만든다. 그 다음 `Piece.createMoveStrategy()`에만 전략 연결을 추가하면 `Piece -> MoveStrategy -> Route` 흐름 안으로 기능을 구현할 수 있다. + +초기 배치에 포함해야 한다면 `InitialFormationStrategy` 계열에서 좌표만 추가하면 되고, 테스트는 "경로 생성 테스트", "차단/포획 판정 테스트", "초기 배치 테스트" 에 새 기물에 대한 케이스를 추가하면 된다. + +지금 구조에서는 새 기물 추가 시 수정 지점이 완전히 없지는 않지만, "이동 규칙", "전략 연결", "초기 배치", "테스트"로 범위를 예측 가능하다. + diff --git a/docs/images/class-diagram.png b/docs/images/class-diagram.png new file mode 100644 index 0000000000..1ee5e2b95b Binary files /dev/null and b/docs/images/class-diagram.png differ diff --git a/docs/images/flowchart.png b/docs/images/flowchart.png new file mode 100644 index 0000000000..624ac48974 Binary files /dev/null and b/docs/images/flowchart.png differ diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/main/java/BoardInitializer.java b/src/main/java/BoardInitializer.java new file mode 100644 index 0000000000..f7ad6b0204 --- /dev/null +++ b/src/main/java/BoardInitializer.java @@ -0,0 +1,27 @@ +import domain.board.Board; +import domain.piece.Piece; +import domain.board.Position; +import domain.piece.TeamColor; +import java.util.HashMap; +import java.util.Map; +import strategy.formation.InitialFormationStrategy; + +public class BoardInitializer { + + private final InitialFormationStrategy choStrategy; + private final InitialFormationStrategy hanStrategy; + + public BoardInitializer(InitialFormationStrategy choStrategy, InitialFormationStrategy hanStrategy) { + this.choStrategy = choStrategy; + this.hanStrategy = hanStrategy; + } + + public Board initialize() { + final Map board = new HashMap<>(); + board.putAll(choStrategy.createInitialPieces(TeamColor.CHO)); + board.putAll(hanStrategy.createInitialPieces(TeamColor.HAN)); + return new Board(board); + } +} + + diff --git a/src/main/java/GameRunner.java b/src/main/java/GameRunner.java new file mode 100644 index 0000000000..affd22e5fc --- /dev/null +++ b/src/main/java/GameRunner.java @@ -0,0 +1,147 @@ +import domain.board.Board; +import domain.piece.Piece; +import domain.board.Position; +import domain.board.Route; +import domain.piece.TeamColor; +import domain.game.TurnManager; +import io.InputView; +import io.OutputView; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import strategy.formation.InitialFormationStrategy; +import strategy.formation.InnerFormationStrategy; +import strategy.formation.LeftFormationStrategy; +import strategy.formation.OuterFormationStrategy; +import strategy.formation.RightFormationStrategy; + +public class GameRunner { + private static final int FIRST_OPTION_NUMBER = 1; + private static final int BACK_OPTION_NUMBER = 0; + private static final int ZERO_BASE_INDEX_OFFSET = 1; + private static final List FORMATIONS = List.of( + new InnerFormationStrategy(), + new OuterFormationStrategy(), + new LeftFormationStrategy(), + new RightFormationStrategy() + ); + + private final InputView inputView; + private final OutputView outputView; + private final TurnManager turnManager; + + public GameRunner() { + this(new InputView(), new OutputView(), new TurnManager()); + } + + public GameRunner(InputView inputView, OutputView outputView, TurnManager turnManager) { + this.inputView = inputView; + this.outputView = outputView; + this.turnManager = turnManager; + } + + public void run() { + outputView.printGameStart(); + + InitialFormationStrategy choStrategy = chooseFormationStrategy(TeamColor.CHO); + InitialFormationStrategy hanStrategy = chooseFormationStrategy(TeamColor.HAN); + + BoardInitializer boardInitializer = new BoardInitializer(choStrategy, hanStrategy); + Board board = boardInitializer.initialize(); + + outputView.printBoard(board); + + while (true) { + playTurn(board); + } + } + + private InitialFormationStrategy chooseFormationStrategy(TeamColor teamColor) { + while (true) { + outputView.printFormationSelectionPrompt(teamColor); + try { + return createFormationStrategy(inputView.readFormationChoice(teamColor)); + } catch (RuntimeException exception) { + outputView.printError("상차림 입력이 올바르지 않습니다."); + } + } + } + + private void playTurn(Board board) { + final TeamColor currentTurn = turnManager.getCurrentTurn(); + outputView.printCurrentTurn(currentTurn); + outputView.printBoard(board); + + while (true) { + try { + final Piece selectedPiece = choosePiece(board, currentTurn); + final Optional selectedRoute = chooseRoute(board, selectedPiece); + if (selectedRoute.isEmpty()) { + continue; + } + + final Position destination = selectedRoute.get().endPos(); + board.move(selectedPiece, destination); + outputView.printMoveResult(selectedPiece, destination); + turnManager.advanceTurn(); + return; + } catch (RuntimeException exception) { + outputView.printError(exception.getMessage()); + } + } + } + + private InitialFormationStrategy createFormationStrategy(int choice) { + validateFormationChoice(choice); + return FORMATIONS.get(choice - ZERO_BASE_INDEX_OFFSET); + } + + private void validateFormationChoice(int choice) { + if (choice >= FIRST_OPTION_NUMBER && choice <= FORMATIONS.size()) { + return; + } + throw new IllegalArgumentException("상차림 번호는 1~4 사이여야 합니다."); + } + + private Piece choosePiece(Board board, TeamColor currentTurn) { + List> pieces = board.findPiecesByTeam(currentTurn); + outputView.printPieceOptions(pieces); + + final int pieceChoice = inputView.readPieceChoice(currentTurn); + return getSelectedPiece(pieces, pieceChoice); + } + + private Optional chooseRoute(Board board, Piece selectedPiece) { + final List routes = board.findMovableRoutes(selectedPiece); + validateMovableRoutes(routes); + outputView.printRouteOptions(routes); + + final int routeChoice = inputView.readRouteChoice(); + if (routeChoice == BACK_OPTION_NUMBER) { + return Optional.empty(); + } + return Optional.of(getSelectedRoute(routes, routeChoice)); + } + + private void validateMovableRoutes(List routes) { + if (routes.isEmpty()) { + throw new IllegalArgumentException("선택한 기물은 이동 가능한 경로가 없습니다."); + } + } + + private Piece getSelectedPiece(List> pieces, int pieceChoice) { + if (pieceChoice < FIRST_OPTION_NUMBER || pieceChoice > pieces.size()) { + throw new IllegalArgumentException("기물 번호가 범위를 벗어났습니다."); + } + return pieces.get(pieceChoice - ZERO_BASE_INDEX_OFFSET).getValue(); + } + + private Route getSelectedRoute(List routes, int routeChoice) { + if (routeChoice < FIRST_OPTION_NUMBER || routeChoice > routes.size()) { + throw new IllegalArgumentException("경로 번호가 범위를 벗어났습니다."); + } + return routes.get(routeChoice - ZERO_BASE_INDEX_OFFSET); + } +} + + diff --git a/src/main/java/Main.java b/src/main/java/Main.java new file mode 100644 index 0000000000..41792dcbdf --- /dev/null +++ b/src/main/java/Main.java @@ -0,0 +1,8 @@ +public class Main { + + public static void main(String[] args) { + new GameRunner().run(); + } +} + + diff --git a/src/main/java/domain/board/Board.java b/src/main/java/domain/board/Board.java new file mode 100644 index 0000000000..a266499544 --- /dev/null +++ b/src/main/java/domain/board/Board.java @@ -0,0 +1,85 @@ +package domain.board; + +import domain.piece.Piece; +import domain.piece.TeamColor; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class Board { + + private final Map pieces; + + public Board(Map pieces) { + this.pieces = new HashMap<>(pieces); + } + + public List getBlockingPieces(Route route) { + return route.intermediatePositions().stream() + .map(this::findPiece) + .flatMap(Optional::stream) + .toList(); + } + + public Optional getDestinationPiece(Route route) { + return Optional.ofNullable(pieces.get(route.endPos())); + } + + public Optional findPositionOf(Piece piece) { + return pieces.entrySet().stream() + .filter(entry -> entry.getValue() == piece) + .map(Map.Entry::getKey) + .findFirst(); + } + + public Optional findPiece(Position position) { + return Optional.ofNullable(pieces.get(position)); + } + + public List> findPiecesByTeam(TeamColor teamColor) { + return pieces.entrySet().stream() + .filter(entry -> entry.getValue().getTeamColor() == teamColor) + .sorted((left, right) -> { + final int rowCompare = Integer.compare(left.getKey().row(), right.getKey().row()); + if (rowCompare != 0) { + return rowCompare; + } + return Integer.compare(left.getKey().column(), right.getKey().column()); + }) + .toList(); + } + + public List findMovableRoutes(Piece piece) { + final Position currentPosition = findPositionOf(piece) + .orElseThrow(() -> new IllegalArgumentException("보드에 없는 기물입니다.")); + + return piece.makeRoutes(currentPosition).stream() + .filter(route -> route.endPos().isInsideBoard()) + .filter(route -> piece.canMove(route, getBlockingPieces(route), getDestinationPiece(route))) + .toList(); + } + + public void move(Piece piece, Position destination) { + final Position currentPosition = findPositionOf(piece) + .orElseThrow(() -> new IllegalArgumentException("보드에 없는 기물입니다.")); + + final Route route = piece.makeRoutes(currentPosition).stream() + .filter(candidate -> candidate.endPos().equals(destination)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("해당 기물은 목적지로 이동할 수 없습니다.")); + + final List blockingPieces = getBlockingPieces(route); + final Optional destinationPiece = getDestinationPiece(route); + + if (!piece.canMove(route, blockingPieces, destinationPiece)) { + throw new IllegalArgumentException("현재 판 상태에서는 해당 목적지로 이동할 수 없습니다."); + } + + pieces.remove(currentPosition); + pieces.put(destination, piece); + } +} + + + diff --git a/src/main/java/domain/board/Column.java b/src/main/java/domain/board/Column.java new file mode 100644 index 0000000000..bc3dcfe8f7 --- /dev/null +++ b/src/main/java/domain/board/Column.java @@ -0,0 +1,22 @@ +package domain.board; + +public record Column(int value) { + private static final int COLUMN_MIN_SIZE = 0; + private static final int COLUMN_MAX_SIZE = 8; + + public Column { + validateColumn(value); + } + + private void validateColumn(int number) { + if (number < COLUMN_MIN_SIZE) { + throw new IllegalArgumentException("열의 최소 값은 " + COLUMN_MIN_SIZE + "입니다."); + } + if (number > COLUMN_MAX_SIZE) { + throw new IllegalArgumentException("열의 최대 값은 " + COLUMN_MAX_SIZE + "입니다."); + } + } +} + + + diff --git a/src/main/java/domain/board/Direction.java b/src/main/java/domain/board/Direction.java new file mode 100644 index 0000000000..d7cd527d28 --- /dev/null +++ b/src/main/java/domain/board/Direction.java @@ -0,0 +1,30 @@ +package domain.board; + +public enum Direction { + NORTH(-1, 0), + SOUTH(1, 0), + EAST(0, 1), + WEST(0, -1), + NORTH_EAST(-1, 1), + NORTH_WEST(-1, -1), + SOUTH_EAST(1, 1), + SOUTH_WEST(1, -1); + + private final int dRow; + private final int dColumn; + + Direction(int dRow, int dColumn) { + this.dRow = dRow; + this.dColumn = dColumn; + } + + public int dRow() { + return dRow; + } + + public int dColumn() { + return dColumn; + } +} + + diff --git a/src/main/java/domain/board/MovePath.java b/src/main/java/domain/board/MovePath.java new file mode 100644 index 0000000000..6df40d5aad --- /dev/null +++ b/src/main/java/domain/board/MovePath.java @@ -0,0 +1,9 @@ +package domain.board; + +import java.util.List; + +public record MovePath(List steps) {} + + + + diff --git a/src/main/java/domain/board/Position.java b/src/main/java/domain/board/Position.java new file mode 100644 index 0000000000..7fbaf591ec --- /dev/null +++ b/src/main/java/domain/board/Position.java @@ -0,0 +1,98 @@ +package domain.board; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class Position { + private static final int BOARD_MIN_INDEX = 0; + private static final int BOARD_MAX_ROW = 9; + private static final int BOARD_MAX_COLUMN = 8; + private static final int BOARD_COLUMN_SIZE = BOARD_MAX_COLUMN + 1; + private static final Map CACHE = createCache(); + + private final Row row; + + private final Column column; + private Position(Row row, Column column) { + this.row = row; + this.column = column; + } + + public Position next(Direction direction) { + final int nextRow = this.row.value() + direction.dRow(); + final int nextCol = this.column.value() + direction.dColumn(); + + return Position.of(nextRow, nextCol); + } + + public static Position of(int row, int column) { + validateBoardIndex(row, column); + return CACHE.get(toCacheKey(row, column)); + } + + public int row() { + return row.value(); + } + + public int column() { + return column.value(); + } + + public boolean isInsideBoard() { + return row() >= BOARD_MIN_INDEX && row() <= BOARD_MAX_ROW + && column() >= BOARD_MIN_INDEX && column() <= BOARD_MAX_COLUMN; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + final Position position = (Position) o; + return Objects.equals(row, position.row) && Objects.equals(column, position.column); + } + + @Override + public int hashCode() { + return Objects.hash(row, column); + } + + @Override + public String toString() { + return "(" + row() + "," + column() + ")"; + } + + private static Map createCache() { + final Map cache = new HashMap<>(); + for (int row = BOARD_MIN_INDEX; row <= BOARD_MAX_ROW; row++) { + for (int column = BOARD_MIN_INDEX; column <= BOARD_MAX_COLUMN; column++) { + cache.put(toCacheKey(row, column), new Position(new Row(row), new Column(column))); + } + } + return cache; + } + + private static int toCacheKey(int row, int column) { + return row * BOARD_COLUMN_SIZE + column; + } + + private static void validateBoardIndex(int row, int column) { + if (row < BOARD_MIN_INDEX) { + throw new IllegalArgumentException("행의 최소 값은 " + BOARD_MIN_INDEX + "입니다."); + } + if (row > BOARD_MAX_ROW) { + throw new IllegalArgumentException("행의 최대 값은 " + BOARD_MAX_ROW + "입니다."); + } + if (column < BOARD_MIN_INDEX) { + throw new IllegalArgumentException("열의 최소 값은 " + BOARD_MIN_INDEX + "입니다."); + } + if (column > BOARD_MAX_COLUMN) { + throw new IllegalArgumentException("열의 최대 값은 " + BOARD_MAX_COLUMN + "입니다."); + } + } + +} + + + diff --git a/src/main/java/domain/board/Route.java b/src/main/java/domain/board/Route.java new file mode 100644 index 0000000000..84e7d32576 --- /dev/null +++ b/src/main/java/domain/board/Route.java @@ -0,0 +1,8 @@ +package domain.board; + +import java.util.List; + +public record Route(Position startPos, Position endPos, List intermediatePositions) {} + + + diff --git a/src/main/java/domain/board/Row.java b/src/main/java/domain/board/Row.java new file mode 100644 index 0000000000..21078c6ca9 --- /dev/null +++ b/src/main/java/domain/board/Row.java @@ -0,0 +1,22 @@ +package domain.board; + +public record Row(int value) { + private static final int ROW_MIN_SIZE = 0; + private static final int ROW_MAX_SIZE = 9; + + public Row { + validateRow(value); + } + + private void validateRow(int value) { + if (value < ROW_MIN_SIZE) { + throw new IllegalArgumentException("행의 최소 값은 " + ROW_MIN_SIZE + "입니다."); + } + if (value > ROW_MAX_SIZE) { + throw new IllegalArgumentException("행의 최대 값은 " + ROW_MAX_SIZE + "입니다."); + } + } +} + + + diff --git a/src/main/java/domain/game/TurnManager.java b/src/main/java/domain/game/TurnManager.java new file mode 100644 index 0000000000..5847d5be39 --- /dev/null +++ b/src/main/java/domain/game/TurnManager.java @@ -0,0 +1,30 @@ +package domain.game; + +import domain.piece.TeamColor; + +public class TurnManager { + + private TeamColor currentTurn; + private int moveCount; + + public TurnManager() { + currentTurn = TeamColor.CHO; + moveCount = 0; + } + + public void advanceTurn() { + moveCount += 1; + if (currentTurn.equals(TeamColor.CHO)) { + currentTurn = TeamColor.HAN; + return; + } + currentTurn = TeamColor.CHO; + } + + public TeamColor getCurrentTurn() { + return currentTurn; + } +} + + + diff --git a/src/main/java/domain/piece/Piece.java b/src/main/java/domain/piece/Piece.java new file mode 100644 index 0000000000..0ee9f48a1f --- /dev/null +++ b/src/main/java/domain/piece/Piece.java @@ -0,0 +1,70 @@ +package domain.piece; + +import domain.board.Position; +import domain.board.Route; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import strategy.move.CannonMoveStrategy; +import strategy.move.ElephantMoveStrategy; +import strategy.move.GuardMoveStrategy; +import strategy.move.HorseMoveStrategy; +import strategy.move.KingMoveStrategy; +import strategy.move.MoveStrategy; +import strategy.move.PawnMoveStrategy; +import strategy.move.RookMoveStrategy; + +public class Piece { + private static final Map MOVE_STRATEGIES = createMoveStrategies(); + + private final TeamColor teamColor; + private final PieceType pieceType; + private final MoveStrategy moveStrategy; + + private Piece(TeamColor teamColor, PieceType pieceType, MoveStrategy moveStrategy) { + this.teamColor = teamColor; + this.pieceType = pieceType; + this.moveStrategy = moveStrategy; + } + + public PieceType getPieceType() { + return this.pieceType; + } + + public TeamColor getTeamColor() { + return this.teamColor; + } + + public List makeRoutes(Position from) { + return moveStrategy.makeRoutes(from, teamColor); + } + + public boolean canMove(Route route, List blockingPieces, Optional destinationPiece) { + return moveStrategy.canMove(route, blockingPieces, destinationPiece, teamColor); + } + + public static Piece of(TeamColor teamColor, PieceType pieceType) { + return new Piece(teamColor, pieceType, createMoveStrategy(pieceType)); + } + + private static MoveStrategy createMoveStrategy(PieceType pieceType) { + return Optional.ofNullable(MOVE_STRATEGIES.get(pieceType)) + .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 기물 타입입니다.")); + } + + private static Map createMoveStrategies() { + final Map moveStrategies = new EnumMap<>(PieceType.class); + moveStrategies.put(PieceType.CANNON, new CannonMoveStrategy()); + moveStrategies.put(PieceType.ELEPHANT, new ElephantMoveStrategy()); + moveStrategies.put(PieceType.GUARD, new GuardMoveStrategy()); + moveStrategies.put(PieceType.HORSE, new HorseMoveStrategy()); + moveStrategies.put(PieceType.KING, new KingMoveStrategy()); + moveStrategies.put(PieceType.PAWN, new PawnMoveStrategy()); + moveStrategies.put(PieceType.ROOK, new RookMoveStrategy()); + return moveStrategies; + } +} + + + diff --git a/src/main/java/domain/piece/PieceType.java b/src/main/java/domain/piece/PieceType.java new file mode 100644 index 0000000000..c3af68adec --- /dev/null +++ b/src/main/java/domain/piece/PieceType.java @@ -0,0 +1,16 @@ +package domain.piece; + +public enum PieceType { + CANNON, + ELEPHANT, + GUARD, + HORSE, + KING, + PAWN, + ROOK + + +} + + + diff --git a/src/main/java/domain/piece/TeamColor.java b/src/main/java/domain/piece/TeamColor.java new file mode 100644 index 0000000000..5ef729d9e6 --- /dev/null +++ b/src/main/java/domain/piece/TeamColor.java @@ -0,0 +1,16 @@ +package domain.piece; + +public enum TeamColor { + CHO, + HAN; + + public String displayName() { + if (this == CHO) { + return "초"; + } + return "한"; + } +} + + + diff --git a/src/main/java/io/InputView.java b/src/main/java/io/InputView.java new file mode 100644 index 0000000000..e2c97abdad --- /dev/null +++ b/src/main/java/io/InputView.java @@ -0,0 +1,38 @@ +package io; + +import domain.piece.TeamColor; +import java.util.Scanner; + +public class InputView { + private static final String FORMATION_CHOICE_PROMPT = " 상차림 선택 (1. 안상 2. 바깥상 3. 좌상 4. 우상) > "; + private static final String PIECE_CHOICE_PROMPT = " 차례, 선택할 기물 번호 > "; + private static final String ROUTE_CHOICE_PROMPT = "이동할 경로 번호 선택 (0은 뒤로가기) > "; + + private final Scanner scanner; + + public InputView() { + this(new Scanner(System.in)); + } + + public InputView(Scanner scanner) { + this.scanner = scanner; + } + + public int readFormationChoice(TeamColor teamColor) { + System.out.print(teamColor.displayName() + FORMATION_CHOICE_PROMPT); + return Integer.parseInt(scanner.nextLine().trim()); + } + + public int readPieceChoice(TeamColor teamColor) { + System.out.print(teamColor.displayName() + PIECE_CHOICE_PROMPT); + return Integer.parseInt(scanner.nextLine().trim()); + } + + public int readRouteChoice() { + System.out.print(ROUTE_CHOICE_PROMPT); + return Integer.parseInt(scanner.nextLine().trim()); + } + +} + + diff --git a/src/main/java/io/OutputView.java b/src/main/java/io/OutputView.java new file mode 100644 index 0000000000..79534c3ff3 --- /dev/null +++ b/src/main/java/io/OutputView.java @@ -0,0 +1,145 @@ +package io; + +import domain.board.Board; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.board.Route; +import domain.piece.TeamColor; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class OutputView { + private static final int BOARD_LAST_ROW = 9; + private static final int BOARD_LAST_COLUMN = 8; + private static final String EMPTY_CELL = " "; + private static final String GAME_START_MESSAGE = "장기 게임을 시작합니다."; + private static final String FORMATION_SELECTION_MESSAGE = " 상차림을 선택하세요."; + private static final String INNER_FORMATION_OPTION = "1. 안상차림"; + private static final String OUTER_FORMATION_OPTION = "2. 바깥상차림"; + private static final String LEFT_FORMATION_OPTION = "3. 좌상차림"; + private static final String RIGHT_FORMATION_OPTION = "4. 우상차림"; + private static final String CURRENT_TURN_MESSAGE = "현재 턴: "; + private static final String PIECE_SELECTION_MESSAGE = "선택 가능한 기물:"; + private static final String ROUTE_SELECTION_MESSAGE = "이동 가능한 경로:"; + private static final String BACK_OPTION_MESSAGE = "0. 뒤로가기"; + private static final String CURRENT_BOARD_MESSAGE = "현재 장기판"; + private static final String BOARD_HEADER = " 0 1 2 3 4 5 6 7 8"; + private static final String BOARD_TOP_BORDER = " ┌────┬────┬────┬────┬────┬────┬────┬────┬────┐"; + private static final String BOARD_MIDDLE_BORDER = " ├────┼────┼────┼────┼────┼────┼────┼────┼────┤"; + private static final String BOARD_BOTTOM_BORDER = " └────┴────┴────┴────┴────┴────┴────┴────┴────┘"; + private static final String ROW_PREFIX_FORMAT = "%2d │"; + private static final String CELL_SEPARATOR = " │"; + private static final String ROUTE_FORMAT = "%d. %s -> %s"; + private static final String PIECE_OPTION_FORMAT = "%d. %s%s"; + private static final String MOVE_RESULT_FORMAT = "%s 가 %s 로 이동했습니다."; + private static final String ERROR_PREFIX = "[ERROR] "; + private static final Map PIECE_SYMBOLS = createPieceSymbols(); + private static final String CHO_COLOR = "\u001B[38;5;71m"; + private static final String HAN_COLOR = "\u001B[38;5;167m"; + private static final String RESET = "\u001B[0m"; + + public void printGameStart() { + System.out.println(GAME_START_MESSAGE); + } + + public void printFormationSelectionPrompt(TeamColor teamColor) { + System.out.println(teamColor.displayName() + FORMATION_SELECTION_MESSAGE); + System.out.println(INNER_FORMATION_OPTION); + System.out.println(OUTER_FORMATION_OPTION); + System.out.println(LEFT_FORMATION_OPTION); + System.out.println(RIGHT_FORMATION_OPTION); + } + + public void printCurrentTurn(TeamColor teamColor) { + System.out.println(); + System.out.println(CURRENT_TURN_MESSAGE + teamColor.displayName()); + } + + public void printPieceOptions(List> pieces) { + System.out.println(PIECE_SELECTION_MESSAGE); + for (int index = 0; index < pieces.size(); index++) { + final Map.Entry entry = pieces.get(index); + System.out.println(PIECE_OPTION_FORMAT.formatted(index + 1, formatPiece(entry.getValue()), entry.getKey())); + } + } + + public void printRouteOptions(List routes) { + System.out.println(ROUTE_SELECTION_MESSAGE); + System.out.println(BACK_OPTION_MESSAGE); + for (int index = 0; index < routes.size(); index++) { + final Route route = routes.get(index); + System.out.println(ROUTE_FORMAT.formatted(index + 1, route.startPos(), route.endPos())); + } + } + + public void printBoard(Board board) { + System.out.println(); + System.out.println(CURRENT_BOARD_MESSAGE); + System.out.println(BOARD_HEADER); + System.out.println(BOARD_TOP_BORDER); + for (int row = 0; row <= BOARD_LAST_ROW; row++) { + final StringBuilder line = new StringBuilder(); + line.append(ROW_PREFIX_FORMAT.formatted(row)); + for (int column = 0; column <= BOARD_LAST_COLUMN; column++) { + final Optional piece = board.findPiece(Position.of(row, column)); + line.append(" ").append(formatBoardCell(piece)).append(CELL_SEPARATOR); + } + System.out.println(line); + if (row < BOARD_LAST_ROW) { + System.out.println(BOARD_MIDDLE_BORDER); + } + } + System.out.println(BOARD_BOTTOM_BORDER); + } + + public void printMoveResult(Piece piece, Position destination) { + System.out.println(MOVE_RESULT_FORMAT.formatted(formatPiece(piece), destination)); + } + + public void printError(String message) { + System.out.println(ERROR_PREFIX + message); + } + + private String formatBoardCell(Optional piece) { + if (piece.isEmpty()) { + return EMPTY_CELL; + } + final Piece actualPiece = piece.get(); + final String symbol = formatBoardSymbol(actualPiece); + + if (actualPiece.getTeamColor() == TeamColor.CHO) { + return CHO_COLOR + symbol + RESET; + } + return HAN_COLOR + symbol + RESET; + } + + private String formatPiece(Piece piece) { + return findPieceSymbol(piece.getPieceType()); + } + + private String formatBoardSymbol(Piece piece) { + return findPieceSymbol(piece.getPieceType()); + } + + private String findPieceSymbol(PieceType pieceType) { + return Optional.ofNullable(PIECE_SYMBOLS.get(pieceType)) + .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 기물 타입입니다.")); + } + + private static Map createPieceSymbols() { + final Map pieceSymbols = new EnumMap<>(PieceType.class); + pieceSymbols.put(PieceType.ROOK, "차"); + pieceSymbols.put(PieceType.HORSE, "마"); + pieceSymbols.put(PieceType.ELEPHANT, "상"); + pieceSymbols.put(PieceType.GUARD, "사"); + pieceSymbols.put(PieceType.KING, "왕"); + pieceSymbols.put(PieceType.CANNON, "포"); + pieceSymbols.put(PieceType.PAWN, "졸"); + return pieceSymbols; + } +} + + diff --git a/src/main/java/strategy/formation/InitialFormationStrategy.java b/src/main/java/strategy/formation/InitialFormationStrategy.java new file mode 100644 index 0000000000..ce73a448f5 --- /dev/null +++ b/src/main/java/strategy/formation/InitialFormationStrategy.java @@ -0,0 +1,77 @@ +package strategy.formation; + +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.piece.TeamColor; +import java.util.HashMap; +import java.util.Map; + +public abstract class InitialFormationStrategy { + protected static final int HAN_BACK_RANK_ROW = 0; + protected static final int CHO_BACK_RANK_ROW = 9; + private static final int HAN_KING_ROW = 1; + private static final int HAN_CANNON_ROW = 2; + private static final int HAN_PAWN_ROW = 3; + private static final int CHO_KING_ROW = 8; + private static final int CHO_CANNON_ROW = 7; + private static final int CHO_PAWN_ROW = 6; + private static final int LEFT_EDGE_COLUMN = 0; + private static final int LEFT_CANNON_COLUMN = 1; + private static final int PALACE_LEFT_GUARD_COLUMN = 3; + private static final int PALACE_CENTER_COLUMN = 4; + private static final int PALACE_RIGHT_GUARD_COLUMN = 5; + private static final int RIGHT_CANNON_COLUMN = 7; + private static final int RIGHT_EDGE_COLUMN = 8; + private static final int PAWN_COLUMN_INTERVAL = 2; + + public final Map createInitialPieces(TeamColor teamColor) { + final Map formationPieces = createFormationPieces(teamColor); + final Map fixedPieces = placeFixedPieces(teamColor); + final Map allPieces = new HashMap<>(); + + allPieces.putAll(formationPieces); + allPieces.putAll(fixedPieces); + + return allPieces; + } + + protected abstract Map createFormationPieces(TeamColor teamColor); + + protected final int findBackRankRow(TeamColor teamColor) { + if (teamColor == TeamColor.CHO) { + return CHO_BACK_RANK_ROW; + } + return HAN_BACK_RANK_ROW; + } + + private Map placeFixedPieces(TeamColor teamColor) { + if (teamColor == TeamColor.HAN) { + return createFixedMap(teamColor, HAN_BACK_RANK_ROW, HAN_KING_ROW, HAN_CANNON_ROW, HAN_PAWN_ROW); + } + return createFixedMap(teamColor, CHO_BACK_RANK_ROW, CHO_KING_ROW, CHO_CANNON_ROW, CHO_PAWN_ROW); + } + + private Map createFixedMap(TeamColor teamColor, int backRankRow, int kingRow, int cannonRow, int pawnRow) { + final Map map = new HashMap<>(); + + map.put(Position.of(backRankRow, LEFT_EDGE_COLUMN), Piece.of(teamColor, PieceType.ROOK)); + map.put(Position.of(backRankRow, RIGHT_EDGE_COLUMN), Piece.of(teamColor, PieceType.ROOK)); + + map.put(Position.of(backRankRow, PALACE_LEFT_GUARD_COLUMN), Piece.of(teamColor, PieceType.GUARD)); + map.put(Position.of(backRankRow, PALACE_RIGHT_GUARD_COLUMN), Piece.of(teamColor, PieceType.GUARD)); + + map.put(Position.of(kingRow, PALACE_CENTER_COLUMN), Piece.of(teamColor, PieceType.KING)); + + map.put(Position.of(cannonRow, LEFT_CANNON_COLUMN), Piece.of(teamColor, PieceType.CANNON)); + map.put(Position.of(cannonRow, RIGHT_CANNON_COLUMN), Piece.of(teamColor, PieceType.CANNON)); + + for (int column = LEFT_EDGE_COLUMN; column <= RIGHT_EDGE_COLUMN; column += PAWN_COLUMN_INTERVAL) { + map.put(Position.of(pawnRow, column), Piece.of(teamColor, PieceType.PAWN)); + } + + return map; + } +} + + diff --git a/src/main/java/strategy/formation/InnerFormationStrategy.java b/src/main/java/strategy/formation/InnerFormationStrategy.java new file mode 100644 index 0000000000..945739fff7 --- /dev/null +++ b/src/main/java/strategy/formation/InnerFormationStrategy.java @@ -0,0 +1,34 @@ +package strategy.formation; + +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.piece.TeamColor; +import java.util.HashMap; +import java.util.Map; + +public class InnerFormationStrategy extends InitialFormationStrategy { + private static final int LEFT_HORSE_COLUMN = 1; + private static final int LEFT_ELEPHANT_COLUMN = 2; + private static final int RIGHT_ELEPHANT_COLUMN = 6; + private static final int RIGHT_HORSE_COLUMN = 7; + + @Override + protected Map createFormationPieces(TeamColor teamColor) { + if (teamColor.equals(TeamColor.CHO)) { + return createFormation(teamColor, CHO_BACK_RANK_ROW); + } + return createFormation(teamColor, HAN_BACK_RANK_ROW); + } + + private Map createFormation(TeamColor teamColor, int row) { + final Map formation = new HashMap<>(); + formation.put(Position.of(row, LEFT_HORSE_COLUMN), Piece.of(teamColor, PieceType.HORSE)); + formation.put(Position.of(row, LEFT_ELEPHANT_COLUMN), Piece.of(teamColor, PieceType.ELEPHANT)); + formation.put(Position.of(row, RIGHT_ELEPHANT_COLUMN), Piece.of(teamColor, PieceType.ELEPHANT)); + formation.put(Position.of(row, RIGHT_HORSE_COLUMN), Piece.of(teamColor, PieceType.HORSE)); + return formation; + } +} + + diff --git a/src/main/java/strategy/formation/LeftFormationStrategy.java b/src/main/java/strategy/formation/LeftFormationStrategy.java new file mode 100644 index 0000000000..9355be71f7 --- /dev/null +++ b/src/main/java/strategy/formation/LeftFormationStrategy.java @@ -0,0 +1,29 @@ +package strategy.formation; + +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.piece.TeamColor; +import java.util.HashMap; +import java.util.Map; + +public class LeftFormationStrategy extends InitialFormationStrategy { + private static final int LEFT_HORSE_COLUMN = 1; + private static final int LEFT_ELEPHANT_COLUMN = 2; + private static final int RIGHT_HORSE_COLUMN = 6; + private static final int RIGHT_ELEPHANT_COLUMN = 7; + + @Override + protected Map createFormationPieces(TeamColor teamColor) { + final Map formation = new HashMap<>(); + final int row = findBackRankRow(teamColor); + + formation.put(Position.of(row, LEFT_HORSE_COLUMN), Piece.of(teamColor, PieceType.HORSE)); + formation.put(Position.of(row, LEFT_ELEPHANT_COLUMN), Piece.of(teamColor, PieceType.ELEPHANT)); + formation.put(Position.of(row, RIGHT_HORSE_COLUMN), Piece.of(teamColor, PieceType.HORSE)); + formation.put(Position.of(row, RIGHT_ELEPHANT_COLUMN), Piece.of(teamColor, PieceType.ELEPHANT)); + return formation; + } +} + + diff --git a/src/main/java/strategy/formation/OuterFormationStrategy.java b/src/main/java/strategy/formation/OuterFormationStrategy.java new file mode 100644 index 0000000000..7dc5d1207d --- /dev/null +++ b/src/main/java/strategy/formation/OuterFormationStrategy.java @@ -0,0 +1,29 @@ +package strategy.formation; + +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.piece.TeamColor; +import java.util.HashMap; +import java.util.Map; + +public class OuterFormationStrategy extends InitialFormationStrategy { + private static final int LEFT_ELEPHANT_COLUMN = 1; + private static final int LEFT_HORSE_COLUMN = 2; + private static final int RIGHT_HORSE_COLUMN = 6; + private static final int RIGHT_ELEPHANT_COLUMN = 7; + + @Override + protected Map createFormationPieces(TeamColor teamColor) { + final Map formation = new HashMap<>(); + final int row = findBackRankRow(teamColor); + + formation.put(Position.of(row, LEFT_ELEPHANT_COLUMN), Piece.of(teamColor, PieceType.ELEPHANT)); + formation.put(Position.of(row, LEFT_HORSE_COLUMN), Piece.of(teamColor, PieceType.HORSE)); + formation.put(Position.of(row, RIGHT_HORSE_COLUMN), Piece.of(teamColor, PieceType.HORSE)); + formation.put(Position.of(row, RIGHT_ELEPHANT_COLUMN), Piece.of(teamColor, PieceType.ELEPHANT)); + return formation; + } +} + + diff --git a/src/main/java/strategy/formation/RightFormationStrategy.java b/src/main/java/strategy/formation/RightFormationStrategy.java new file mode 100644 index 0000000000..8e672a5e42 --- /dev/null +++ b/src/main/java/strategy/formation/RightFormationStrategy.java @@ -0,0 +1,29 @@ +package strategy.formation; + +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.piece.TeamColor; +import java.util.HashMap; +import java.util.Map; + +public class RightFormationStrategy extends InitialFormationStrategy { + private static final int LEFT_ELEPHANT_COLUMN = 1; + private static final int LEFT_HORSE_COLUMN = 2; + private static final int RIGHT_ELEPHANT_COLUMN = 6; + private static final int RIGHT_HORSE_COLUMN = 7; + + @Override + protected Map createFormationPieces(TeamColor teamColor) { + final Map formation = new HashMap<>(); + final int row = findBackRankRow(teamColor); + + formation.put(Position.of(row, LEFT_ELEPHANT_COLUMN), Piece.of(teamColor, PieceType.ELEPHANT)); + formation.put(Position.of(row, LEFT_HORSE_COLUMN), Piece.of(teamColor, PieceType.HORSE)); + formation.put(Position.of(row, RIGHT_ELEPHANT_COLUMN), Piece.of(teamColor, PieceType.ELEPHANT)); + formation.put(Position.of(row, RIGHT_HORSE_COLUMN), Piece.of(teamColor, PieceType.HORSE)); + return formation; + } +} + + diff --git a/src/main/java/strategy/move/CannonMoveStrategy.java b/src/main/java/strategy/move/CannonMoveStrategy.java new file mode 100644 index 0000000000..6bef6ecef1 --- /dev/null +++ b/src/main/java/strategy/move/CannonMoveStrategy.java @@ -0,0 +1,64 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Route; +import domain.piece.TeamColor; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class CannonMoveStrategy implements MoveStrategy { + private static final int MIN_DISTANCE = 1; + private static final int MAX_STRAIGHT_DISTANCE = 9; + private static final int REQUIRED_BRIDGE_COUNT = 1; + + @Override + public List getPaths(TeamColor teamColor) { + final List paths = new ArrayList<>(); + + addStraightPaths(paths, Direction.NORTH); + addStraightPaths(paths, Direction.SOUTH); + addStraightPaths(paths, Direction.EAST); + addStraightPaths(paths, Direction.WEST); + + return paths; + } + + private void addStraightPaths(List paths, Direction direction) { + for (int distance = MIN_DISTANCE; distance <= MAX_STRAIGHT_DISTANCE; distance++) { + final List steps = new ArrayList<>(); + for (int i = 0; i < distance; i++) { + steps.add(direction); + } + paths.add(new MovePath(steps)); + } + } + + @Override + public boolean canMove(Route route, List blockingPieces, Optional destinationPiece, TeamColor myTeam) { + if (blockingPieces.size() != REQUIRED_BRIDGE_COUNT) { + return false; + } + + final Piece bridgePiece = blockingPieces.getFirst(); + if (bridgePiece.getPieceType() == PieceType.CANNON) { + return false; + } + + if (destinationPiece.isEmpty()) { + return true; + } + + final Piece targetPiece = destinationPiece.get(); + if (targetPiece.getPieceType() == PieceType.CANNON) { + return false; + } + + return targetPiece.getTeamColor() != myTeam; + } +} + + diff --git a/src/main/java/strategy/move/ElephantMoveStrategy.java b/src/main/java/strategy/move/ElephantMoveStrategy.java new file mode 100644 index 0000000000..20b10f6683 --- /dev/null +++ b/src/main/java/strategy/move/ElephantMoveStrategy.java @@ -0,0 +1,28 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.TeamColor; +import java.util.List; + +public class ElephantMoveStrategy implements MoveStrategy { + + private static final List PATHS = List.of( + new MovePath(List.of(Direction.NORTH, Direction.NORTH_EAST, Direction.NORTH_EAST)), + new MovePath(List.of(Direction.NORTH, Direction.NORTH_WEST, Direction.NORTH_WEST)), + new MovePath(List.of(Direction.SOUTH, Direction.SOUTH_EAST, Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.SOUTH, Direction.SOUTH_WEST, Direction.SOUTH_WEST)), + new MovePath(List.of(Direction.EAST, Direction.NORTH_EAST, Direction.NORTH_EAST)), + new MovePath(List.of(Direction.EAST, Direction.SOUTH_EAST, Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.WEST, Direction.NORTH_WEST, Direction.NORTH_WEST)), + new MovePath(List.of(Direction.WEST, Direction.SOUTH_WEST, Direction.SOUTH_WEST)) + ); + + @Override + public List getPaths(TeamColor teamColor) { + return PATHS; + } + +} + + diff --git a/src/main/java/strategy/move/GuardMoveStrategy.java b/src/main/java/strategy/move/GuardMoveStrategy.java new file mode 100644 index 0000000000..3ff3e132ee --- /dev/null +++ b/src/main/java/strategy/move/GuardMoveStrategy.java @@ -0,0 +1,27 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.TeamColor; +import java.util.List; + +public class GuardMoveStrategy implements MoveStrategy { + + private static final List PATHS = List.of( + new MovePath(List.of(Direction.NORTH)), + new MovePath(List.of(Direction.SOUTH)), + new MovePath(List.of(Direction.EAST)), + new MovePath(List.of(Direction.WEST)), + new MovePath(List.of(Direction.NORTH_EAST)), + new MovePath(List.of(Direction.NORTH_WEST)), + new MovePath(List.of(Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.SOUTH_WEST)) + ); + + @Override + public List getPaths(TeamColor teamColor) { + return PATHS; + } +} + + diff --git a/src/main/java/strategy/move/HorseMoveStrategy.java b/src/main/java/strategy/move/HorseMoveStrategy.java new file mode 100644 index 0000000000..4640c9a276 --- /dev/null +++ b/src/main/java/strategy/move/HorseMoveStrategy.java @@ -0,0 +1,27 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.TeamColor; +import java.util.List; + +public class HorseMoveStrategy implements MoveStrategy { + + private static final List PATHS = List.of( + new MovePath(List.of(Direction.NORTH, Direction.NORTH_EAST)), + new MovePath(List.of(Direction.NORTH, Direction.NORTH_WEST)), + new MovePath(List.of(Direction.SOUTH, Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.SOUTH, Direction.SOUTH_WEST)), + new MovePath(List.of(Direction.EAST, Direction.NORTH_EAST)), + new MovePath(List.of(Direction.EAST, Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.WEST, Direction.NORTH_WEST)), + new MovePath(List.of(Direction.WEST, Direction.SOUTH_WEST)) + ); + + @Override + public List getPaths(TeamColor teamColor) { + return PATHS; + } +} + + diff --git a/src/main/java/strategy/move/KingMoveStrategy.java b/src/main/java/strategy/move/KingMoveStrategy.java new file mode 100644 index 0000000000..9b7c7c194a --- /dev/null +++ b/src/main/java/strategy/move/KingMoveStrategy.java @@ -0,0 +1,27 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.TeamColor; +import java.util.List; + +public class KingMoveStrategy implements MoveStrategy { + + private static final List PATHS = List.of( + new MovePath(List.of(Direction.NORTH)), + new MovePath(List.of(Direction.SOUTH)), + new MovePath(List.of(Direction.EAST)), + new MovePath(List.of(Direction.WEST)), + new MovePath(List.of(Direction.NORTH_EAST)), + new MovePath(List.of(Direction.NORTH_WEST)), + new MovePath(List.of(Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.SOUTH_WEST)) + ); + + @Override + public List getPaths(TeamColor teamColor) { + return PATHS; + } +} + + diff --git a/src/main/java/strategy/move/MoveStrategy.java b/src/main/java/strategy/move/MoveStrategy.java new file mode 100644 index 0000000000..b62577e733 --- /dev/null +++ b/src/main/java/strategy/move/MoveStrategy.java @@ -0,0 +1,69 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.Piece; +import domain.board.Position; +import domain.board.Route; +import domain.piece.TeamColor; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public interface MoveStrategy { + + List getPaths(TeamColor teamColor); + + default List makeRoutes(Position curPos, TeamColor teamColor) { + final List validRoutes = new ArrayList<>(); + final List paths = getPaths(teamColor); + + for (final MovePath path : paths) { + createRoute(curPos, path.steps()).ifPresent(validRoutes::add); + } + + return validRoutes; + } + + private Optional createRoute(Position startPos, List steps) { + final List intermediates = new ArrayList<>(); + Position currentPos = startPos; + + for (int index = 0; index < steps.size(); index++) { + final Optional nextPosition = findNextPosition(currentPos, steps.get(index)); + if (nextPosition.isEmpty()) { + return Optional.empty(); + } + currentPos = nextPosition.get(); + + if (isIntermediateStep(index, steps.size())) { + intermediates.add(currentPos); + } + } + + return Optional.of(new Route(startPos, currentPos, intermediates)); + } + + private Optional findNextPosition(Position currentPos, Direction direction) { + try { + return Optional.of(currentPos.next(direction)); + } catch (IllegalArgumentException exception) { + return Optional.empty(); + } + } + + private boolean isIntermediateStep(int stepIndex, int totalSteps) { + return stepIndex < totalSteps - 1; + } + + default boolean canMove(Route route, List blockingPieces, Optional destinationPiece, TeamColor myTeam) { + if (!blockingPieces.isEmpty()) { + return false; + } + + return destinationPiece.isEmpty() || destinationPiece.get().getTeamColor() != myTeam; + } + +} + + diff --git a/src/main/java/strategy/move/PawnMoveStrategy.java b/src/main/java/strategy/move/PawnMoveStrategy.java new file mode 100644 index 0000000000..dc7fe58e10 --- /dev/null +++ b/src/main/java/strategy/move/PawnMoveStrategy.java @@ -0,0 +1,29 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.TeamColor; +import java.util.List; + +public class PawnMoveStrategy implements MoveStrategy{ + + + @Override + public List getPaths(TeamColor teamColor) { + if (teamColor == TeamColor.CHO) { + return List.of( + new MovePath(List.of(Direction.NORTH)), + new MovePath(List.of(Direction.EAST)), + new MovePath(List.of(Direction.WEST)) + ); + } + + return List.of( + new MovePath(List.of(Direction.SOUTH)), + new MovePath(List.of(Direction.EAST)), + new MovePath(List.of(Direction.WEST)) + ); + } +} + + diff --git a/src/main/java/strategy/move/RookMoveStrategy.java b/src/main/java/strategy/move/RookMoveStrategy.java new file mode 100644 index 0000000000..4cce138595 --- /dev/null +++ b/src/main/java/strategy/move/RookMoveStrategy.java @@ -0,0 +1,37 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.TeamColor; +import java.util.ArrayList; +import java.util.List; + +public class RookMoveStrategy implements MoveStrategy { + private static final int MIN_DISTANCE = 1; + private static final int MAX_STRAIGHT_DISTANCE = 9; + + @Override + public List getPaths(TeamColor teamColor) { + final List paths = new ArrayList<>(); + + addStraightPaths(paths, Direction.NORTH); + addStraightPaths(paths, Direction.SOUTH); + addStraightPaths(paths, Direction.EAST); + addStraightPaths(paths, Direction.WEST); + + return paths; + } + + private void addStraightPaths(List paths, Direction direction) { + for (int distance = MIN_DISTANCE; distance <= MAX_STRAIGHT_DISTANCE; distance++) { + final List steps = new ArrayList<>(); + for (int i = 0; i < distance; i++) { + steps.add(direction); + } + paths.add(new MovePath(steps)); + } + } + +} + + diff --git a/src/test/java/BoardInitializerTest.java b/src/test/java/BoardInitializerTest.java new file mode 100644 index 0000000000..6e3d4a00dc --- /dev/null +++ b/src/test/java/BoardInitializerTest.java @@ -0,0 +1,92 @@ +import domain.board.Board; +import domain.piece.PieceType; +import domain.board.Position; + +import domain.piece.TeamColor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + + + +import static org.assertj.core.api.Assertions.assertThat; +import strategy.formation.InitialFormationStrategy; +import strategy.formation.InnerFormationStrategy; + +class BoardInitializerTest { + + InitialFormationStrategy choStrategy; + InitialFormationStrategy hanStrategy; + Board board; + + @BeforeEach + public void setUp(){ + choStrategy = new InnerFormationStrategy(); + hanStrategy = new InnerFormationStrategy(); + BoardInitializer boardInitializer = new BoardInitializer(choStrategy, hanStrategy); + board = boardInitializer.initialize(); + } + + + @Nested + class 초기화 { + @Test + void 초나라_한나라_전략을_주입받아_총_32개의_기물이_세팅된_초기_장기판을_반환한다() { + assertThat(board.findPiecesByTeam(TeamColor.CHO)).hasSize(16); + assertThat(board.findPiecesByTeam(TeamColor.HAN)).hasSize(16); + assertThat(board.findPiece(Position.of(8, 4))).get().extracting("pieceType").isEqualTo(PieceType.KING); + assertThat(board.findPiece(Position.of(0, 2))).get().extracting("pieceType").isEqualTo(PieceType.ELEPHANT); + } + } + + @Nested + class 기물목록 { + @Test + void 초나라_기물_리스트를_생성한다() { + assertThat(board.findPiecesByTeam(TeamColor.CHO)).hasSize(16); + assertThat( + board.findPiecesByTeam(TeamColor.CHO).stream() + .map(entry -> entry.getValue()) + .filter(p -> p.getPieceType() == PieceType.ELEPHANT) + ).hasSize(2); + + assertThat( + board.findPiecesByTeam(TeamColor.CHO).stream() + .map(entry -> entry.getValue()) + .filter(p -> p.getPieceType() == PieceType.HORSE) + ).hasSize(2); + + assertThat( + board.findPiecesByTeam(TeamColor.CHO).stream() + .map(entry -> entry.getValue()) + .filter(p -> p.getPieceType() == PieceType.CANNON) + ).hasSize(2); + + assertThat( + board.findPiecesByTeam(TeamColor.CHO).stream() + .map(entry -> entry.getValue()) + .filter(p -> p.getPieceType() == PieceType.ROOK) + ).hasSize(2); + + assertThat( + board.findPiecesByTeam(TeamColor.CHO).stream() + .map(entry -> entry.getValue()) + .filter(p -> p.getPieceType() == PieceType.GUARD) + ).hasSize(2); + + assertThat( + board.findPiecesByTeam(TeamColor.CHO).stream() + .map(entry -> entry.getValue()) + .filter(p -> p.getPieceType() == PieceType.PAWN) + ).hasSize(5); + + assertThat( + board.findPiecesByTeam(TeamColor.CHO).stream() + .map(entry -> entry.getValue()) + .filter(p -> p.getPieceType() == PieceType.KING) + ).hasSize(1); + } + } +} + + diff --git a/src/test/java/domain/board/BoardTest.java b/src/test/java/domain/board/BoardTest.java new file mode 100644 index 0000000000..a11837bb71 --- /dev/null +++ b/src/test/java/domain/board/BoardTest.java @@ -0,0 +1,117 @@ +package domain.board; + +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.piece.TeamColor; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class BoardTest { + + private Board board; + private Piece choPawn; + private Piece hanHorse; + private Piece choCannon; + + @BeforeEach + void setUp() { + choPawn = Piece.of(TeamColor.CHO, PieceType.PAWN); + hanHorse = Piece.of(TeamColor.HAN, PieceType.HORSE); + choCannon = Piece.of(TeamColor.CHO, PieceType.CANNON); + board = new Board(Map.of( + Position.of(3, 4), choPawn, + Position.of(2, 4), hanHorse, + Position.of(1, 4), choCannon + )); + } + + @Nested + class 사실조회 { + @Test + void 경로_중간에_놓인_기물들을_순서대로_반환한다() { + Route route = new Route( + Position.of(4, 4), + Position.of(1, 4), + java.util.List.of(Position.of(3, 4), Position.of(2, 4)) + ); + + assertThat(board.getBlockingPieces(route)).containsExactly(choPawn, hanHorse); + } + + @Test + void 도착지_기물이_있으면_반환한다() { + Route route = new Route( + Position.of(4, 4), + Position.of(1, 4), + java.util.List.of(Position.of(3, 4), Position.of(2, 4)) + ); + + assertThat(board.getDestinationPiece(route)).contains(choCannon); + } + + @Test + void 도착지_기물이_없으면_빈_Optional을_반환한다() { + Route route = new Route( + Position.of(4, 4), + Position.of(0, 4), + java.util.List.of(Position.of(3, 4), Position.of(2, 4), Position.of(1, 4)) + ); + + assertThat(board.getDestinationPiece(route)).isEqualTo(Optional.empty()); + } + } + + @Nested + class 위치조회 { + @Test + void 특정_기물의_현재_위치를_찾는다() { + assertThat(board.findPositionOf(hanHorse)).contains(Position.of(2, 4)); + } + + @Test + void 팀에_속한_기물들을_좌표순으로_조회한다() { + assertThat(board.findPiecesByTeam(TeamColor.CHO)) + .extracting(entry -> entry.getKey()) + .containsExactly(Position.of(1, 4), Position.of(3, 4)); + } + } + + @Nested + class 이동 { + @Test + void 기물과_도착지를_받아_실제_이동을_반영한다() { + final Piece movingPawn = Piece.of(TeamColor.CHO, PieceType.PAWN); + final Board movableBoard = new Board(Map.of( + Position.of(4, 4), movingPawn + )); + + movableBoard.move(movingPawn, Position.of(3, 4)); + + assertThat(movableBoard.findPositionOf(movingPawn)).contains(Position.of(3, 4)); + assertThat(movableBoard.findPiece(Position.of(4, 4))).isEmpty(); + assertThat(movableBoard.findPiece(Position.of(3, 4))).contains(movingPawn); + } + + @Test + void 현재_판_상태를_기준으로_기물의_이동_가능_경로를_반환한다() { + final Piece movingPawn = Piece.of(TeamColor.CHO, PieceType.PAWN); + final Piece allyPiece = Piece.of(TeamColor.CHO, PieceType.GUARD); + final Board movableBoard = new Board(Map.of( + Position.of(4, 4), movingPawn, + Position.of(4, 5), allyPiece + )); + + assertThat(movableBoard.findMovableRoutes(movingPawn)) + .extracting(Route::endPos) + .containsExactlyInAnyOrder(Position.of(3, 4), Position.of(4, 3)); + } + } +} + + + diff --git a/src/test/java/domain/board/ColumnTest.java b/src/test/java/domain/board/ColumnTest.java new file mode 100644 index 0000000000..df8ed79b10 --- /dev/null +++ b/src/test/java/domain/board/ColumnTest.java @@ -0,0 +1,45 @@ +package domain.board; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class ColumnTest { + + @Nested + class 생성 { + @Test + void 유효한_값으로_행_객체가_생성된_경우() { + int input = 3; + + Column column = new Column(input); + + assertThat(column.value()).isEqualTo(3); + } + } + + @Nested + class 예외 { + @Test + void 값이_최솟값보다_작으면_예외를_발행한다() { + int input = -1; + + assertThatThrownBy(() -> new Column(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("열의 최소 값은 0입니다."); + } + + @Test + void 값이_최댓값을_초과하면_예외를_발행한다() { + int input = 19; + + assertThatThrownBy(() -> new Column(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("열의 최대 값은 8입니다."); + } + } +} + + + diff --git a/src/test/java/domain/board/PositionTest.java b/src/test/java/domain/board/PositionTest.java new file mode 100644 index 0000000000..5c312fb48b --- /dev/null +++ b/src/test/java/domain/board/PositionTest.java @@ -0,0 +1,40 @@ +package domain.board; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class PositionTest { + + @Nested + class 생성 { + @Test + void 같은_좌표를_요청하면_캐시된_객체를_재사용한다() { + final Position first = Position.of(3, 4); + final Position second = Position.of(3, 4); + + assertThat(first).isSameAs(second); + } + } + + @Nested + class 예외 { + @Test + void 행이_범위를_벗어나면_예외를_발생한다() { + assertThatThrownBy(() -> Position.of(-1, 4)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("행의 최소 값은 0입니다."); + } + + @Test + void 열이_범위를_벗어나면_예외를_발생한다() { + assertThatThrownBy(() -> Position.of(4, 9)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("열의 최대 값은 8입니다."); + } + } +} + + + diff --git a/src/test/java/domain/board/RowTest.java b/src/test/java/domain/board/RowTest.java new file mode 100644 index 0000000000..a96fc96c26 --- /dev/null +++ b/src/test/java/domain/board/RowTest.java @@ -0,0 +1,45 @@ +package domain.board; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class RowTest { + + @Nested + class 생성 { + @Test + void 유효한_값으로_행_객체가_생성된_경우() { + int input = 3; + + Row row = new Row(input); + + assertThat(row.value()).isEqualTo(3); + } + } + + @Nested + class 예외 { + @Test + void 값이_최솟값보다_작으면_예외를_발행한다() { + int input = -1; + + assertThatThrownBy(() -> new Row(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("행의 최소 값은 0입니다."); + } + + @Test + void 값이_최댓값을_초과하면_예외를_발행한다() { + int input = 19; + + assertThatThrownBy(() -> new Row(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("행의 최대 값은 9입니다."); + } + } +} + + + diff --git a/src/test/java/domain/game/TurnManagerTest.java b/src/test/java/domain/game/TurnManagerTest.java new file mode 100644 index 0000000000..65704d906e --- /dev/null +++ b/src/test/java/domain/game/TurnManagerTest.java @@ -0,0 +1,37 @@ +package domain.game; + +import domain.piece.TeamColor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +public class TurnManagerTest { + + TurnManager turnManager; + + @BeforeEach + public void setUp(){ + turnManager = new TurnManager(); + } + + @Nested + class 현재턴 { + @Test + public void 현재_턴이_누구인지_반환한다() { + assertThat(turnManager.getCurrentTurn()).isEqualTo(TeamColor.CHO); + } + } + + @Nested + class 턴진행 { + @Test + public void 턴이_바뀌면_반대팀이_현재_턴이_된다() { + turnManager.advanceTurn(); + assertThat(turnManager.getCurrentTurn()).isEqualTo(TeamColor.HAN); + } + } +} + + + diff --git a/src/test/java/domain/piece/PieceTest.java b/src/test/java/domain/piece/PieceTest.java new file mode 100644 index 0000000000..2e15c27bc0 --- /dev/null +++ b/src/test/java/domain/piece/PieceTest.java @@ -0,0 +1,44 @@ +package domain.piece; + +import domain.board.Position; +import domain.board.Route; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PieceTest { + + @Nested + class 전략위임 { + @Test + void 기물은_자기_전략으로_후보_경로를_생성한다() { + final Piece piece = Piece.of(TeamColor.CHO, PieceType.PAWN); + + assertThat(piece.makeRoutes(Position.of(2, 3))).containsExactlyInAnyOrder( + new Route(Position.of(2, 3), Position.of(1, 3), List.of()), + new Route(Position.of(2, 3), Position.of(2, 4), List.of()), + new Route(Position.of(2, 3), Position.of(2, 2), List.of()) + ); + } + + @Test + void 기물은_자기_전략으로_이동_가능_여부를_판단한다() { + final Piece piece = Piece.of(TeamColor.CHO, PieceType.PAWN); + final Route route = new Route(Position.of(2, 3), Position.of(1, 3), List.of()); + + final boolean canMove = piece.canMove( + route, + List.of(), + Optional.of(Piece.of(TeamColor.HAN, PieceType.HORSE)) + ); + + assertThat(canMove).isTrue(); + } + } +} + + + diff --git a/src/test/java/strategy/formation/InitialFormationStrategyTest.java b/src/test/java/strategy/formation/InitialFormationStrategyTest.java new file mode 100644 index 0000000000..f1ae92b38b --- /dev/null +++ b/src/test/java/strategy/formation/InitialFormationStrategyTest.java @@ -0,0 +1,72 @@ +package strategy.formation; + +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.piece.TeamColor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class InitialFormationStrategyTest { + + private InitialFormationStrategy strategy; + + @BeforeEach + void setUp() { + strategy = new TestStrategy(); + } + + @Nested + class 한나라 { + @Test + void 고정_기물들이_올바른_위치에_배치된다() { + Map result = strategy.createInitialPieces(TeamColor.HAN); + + assertThat(result).hasSize(12); + assertThat(result.get(Position.of(0, 0)).getPieceType()).isEqualTo(PieceType.ROOK); + assertThat(result.get(Position.of(0, 8)).getPieceType()).isEqualTo(PieceType.ROOK); + assertThat(result.get(Position.of(0, 3)).getPieceType()).isEqualTo(PieceType.GUARD); + assertThat(result.get(Position.of(0, 5)).getPieceType()).isEqualTo(PieceType.GUARD); + assertThat(result.get(Position.of(1, 4)).getPieceType()).isEqualTo(PieceType.KING); + assertThat(result.get(Position.of(2, 1)).getPieceType()).isEqualTo(PieceType.CANNON); + assertThat(result.get(Position.of(2, 7)).getPieceType()).isEqualTo(PieceType.CANNON); + assertThat(result.get(Position.of(3, 0)).getPieceType()).isEqualTo(PieceType.PAWN); + assertThat(result.get(Position.of(3, 4)).getPieceType()).isEqualTo(PieceType.PAWN); + assertThat(result.get(Position.of(3, 8)).getPieceType()).isEqualTo(PieceType.PAWN); + } + } + + @Nested + class 초나라 { + @Test + void 고정_기물들이_올바른_위치에_배치된다() { + Map result = strategy.createInitialPieces(TeamColor.CHO); + + assertThat(result).hasSize(12); + assertThat(result.get(Position.of(9, 0)).getPieceType()).isEqualTo(PieceType.ROOK); + assertThat(result.get(Position.of(9, 8)).getPieceType()).isEqualTo(PieceType.ROOK); + assertThat(result.get(Position.of(9, 3)).getPieceType()).isEqualTo(PieceType.GUARD); + assertThat(result.get(Position.of(9, 5)).getPieceType()).isEqualTo(PieceType.GUARD); + assertThat(result.get(Position.of(8, 4)).getPieceType()).isEqualTo(PieceType.KING); + assertThat(result.get(Position.of(7, 1)).getPieceType()).isEqualTo(PieceType.CANNON); + assertThat(result.get(Position.of(7, 7)).getPieceType()).isEqualTo(PieceType.CANNON); + assertThat(result.get(Position.of(6, 0)).getPieceType()).isEqualTo(PieceType.PAWN); + assertThat(result.get(Position.of(6, 4)).getPieceType()).isEqualTo(PieceType.PAWN); + assertThat(result.get(Position.of(6, 8)).getPieceType()).isEqualTo(PieceType.PAWN); + } + } +} + +class TestStrategy extends InitialFormationStrategy { + @Override + protected Map createFormationPieces(TeamColor teamColor) { + return Map.of(); + } +} + + diff --git a/src/test/java/strategy/formation/InnerFormationStrategyTest.java b/src/test/java/strategy/formation/InnerFormationStrategyTest.java new file mode 100644 index 0000000000..5dfbd1073f --- /dev/null +++ b/src/test/java/strategy/formation/InnerFormationStrategyTest.java @@ -0,0 +1,54 @@ +package strategy.formation; + +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.piece.TeamColor; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class InnerFormationStrategyTest { + + private InitialFormationStrategy initialFormationStrategy; + + @BeforeEach + public void setUp() { + initialFormationStrategy = new InnerFormationStrategy(); + } + + + @Nested + class 한나라 { + @Test + public void 안상차림일_때_마와_상의_좌표가_올바르다() { + Map formation = initialFormationStrategy.createInitialPieces(TeamColor.HAN); + + assertThat(formation.size()).isEqualTo(16); + assertThat(formation.get(Position.of(0, 2)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(0, 6)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(0, 1)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(0, 7)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(1, 4)).getPieceType()).isEqualTo(PieceType.KING); + } + } + + @Nested + class 초나라 { + @Test + public void 안상차림일_때_마와_상의_좌표가_올바르다() { + Map formation = initialFormationStrategy.createInitialPieces(TeamColor.CHO); + + assertThat(formation.size()).isEqualTo(16); + assertThat(formation.get(Position.of(9, 2)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(9, 6)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(9, 7)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(9, 1)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(8, 4)).getPieceType()).isEqualTo(PieceType.KING); + } + } +} + + diff --git a/src/test/java/strategy/formation/LeftFormationStrategyTest.java b/src/test/java/strategy/formation/LeftFormationStrategyTest.java new file mode 100644 index 0000000000..c090903420 --- /dev/null +++ b/src/test/java/strategy/formation/LeftFormationStrategyTest.java @@ -0,0 +1,54 @@ +package strategy.formation; + +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.piece.TeamColor; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class LeftFormationStrategyTest { + + private InitialFormationStrategy initialFormationStrategy; + + @BeforeEach + public void setUp() { + initialFormationStrategy = new LeftFormationStrategy(); + } + + @Nested + class 한나라 { + @Test + public void 좌상차림일_때_마와_상의_좌표가_올바르다() { + Map formation = initialFormationStrategy.createInitialPieces(TeamColor.HAN); + + assertThat(formation.size()).isEqualTo(16); + assertThat(formation.get(Position.of(0, 1)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(0, 2)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(0, 6)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(0, 7)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(1, 4)).getPieceType()).isEqualTo(PieceType.KING); + } + } + + @Nested + class 초나라 { + @Test + public void 좌상차림일_때_마와_상의_좌표가_올바르다() { + Map formation = initialFormationStrategy.createInitialPieces(TeamColor.CHO); + + assertThat(formation.size()).isEqualTo(16); + assertThat(formation.get(Position.of(9, 1)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(9, 2)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(9, 6)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(9, 7)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(8, 4)).getPieceType()).isEqualTo(PieceType.KING); + } + } +} + + diff --git a/src/test/java/strategy/formation/OuterFormationStrategyTest.java b/src/test/java/strategy/formation/OuterFormationStrategyTest.java new file mode 100644 index 0000000000..b4b7c450f0 --- /dev/null +++ b/src/test/java/strategy/formation/OuterFormationStrategyTest.java @@ -0,0 +1,53 @@ +package strategy.formation; + +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.piece.TeamColor; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class OuterFormationStrategyTest { + + private InitialFormationStrategy initialFormationStrategy; + + @BeforeEach + public void setUp() { + initialFormationStrategy = new OuterFormationStrategy(); + } + + @Nested + class 한나라 { + @Test + public void 바깥상차림일_때_마와_상의_좌표가_올바르다() { + Map formation = initialFormationStrategy.createInitialPieces(TeamColor.HAN); + + assertThat(formation.size()).isEqualTo(16); + assertThat(formation.get(Position.of(0, 1)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(0, 7)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(0, 2)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(0, 6)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(1, 4)).getPieceType()).isEqualTo(PieceType.KING); + } + } + + @Nested + class 초나라 { + @Test + public void 바깥상차림일_때_마와_상의_좌표가_올바르다() { + Map formation = initialFormationStrategy.createInitialPieces(TeamColor.CHO); + + assertThat(formation.size()).isEqualTo(16); + assertThat(formation.get(Position.of(9, 1)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(9, 7)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(9, 2)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(9, 6)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(8, 4)).getPieceType()).isEqualTo(PieceType.KING); + } + } +} + + diff --git a/src/test/java/strategy/formation/RightFormationStrategyTest.java b/src/test/java/strategy/formation/RightFormationStrategyTest.java new file mode 100644 index 0000000000..898cd67e82 --- /dev/null +++ b/src/test/java/strategy/formation/RightFormationStrategyTest.java @@ -0,0 +1,54 @@ +package strategy.formation; + +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.piece.TeamColor; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class RightFormationStrategyTest { + + private InitialFormationStrategy initialFormationStrategy; + + @BeforeEach + public void setUp() { + initialFormationStrategy = new RightFormationStrategy(); + } + + @Nested + class 한나라 { + @Test + public void 우상차림일_때_마와_상의_좌표가_올바르다() { + Map formation = initialFormationStrategy.createInitialPieces(TeamColor.HAN); + + assertThat(formation.size()).isEqualTo(16); + assertThat(formation.get(Position.of(0, 1)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(0, 2)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(0, 6)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(0, 7)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(1, 4)).getPieceType()).isEqualTo(PieceType.KING); + } + } + + @Nested + class 초나라 { + @Test + public void 우상차림일_때_마와_상의_좌표가_올바르다() { + Map formation = initialFormationStrategy.createInitialPieces(TeamColor.CHO); + + assertThat(formation.size()).isEqualTo(16); + assertThat(formation.get(Position.of(9, 1)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(9, 2)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(9, 6)).getPieceType()).isEqualTo(PieceType.ELEPHANT); + assertThat(formation.get(Position.of(9, 7)).getPieceType()).isEqualTo(PieceType.HORSE); + assertThat(formation.get(Position.of(8, 4)).getPieceType()).isEqualTo(PieceType.KING); + } + } +} + + diff --git a/src/test/java/strategy/move/CannonMoveStrategyTest.java b/src/test/java/strategy/move/CannonMoveStrategyTest.java new file mode 100644 index 0000000000..a6a1ab0849 --- /dev/null +++ b/src/test/java/strategy/move/CannonMoveStrategyTest.java @@ -0,0 +1,173 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.board.Route; +import domain.piece.TeamColor; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CannonMoveStrategyTest { + + @Nested + class 이동경로 { + @Test + public void 포는_초나라에서_동서남북_직선_경로를_보드_끝까지_가진다() { + MoveStrategy strategy = new CannonMoveStrategy(); + List paths = strategy.getPaths(TeamColor.CHO); + + assertThat(paths).hasSize(36); + assertThat(paths).contains( + new MovePath(List.of(Direction.NORTH)), + new MovePath(List.of(Direction.NORTH, Direction.NORTH, Direction.NORTH, Direction.NORTH)), + new MovePath(List.of(Direction.SOUTH)), + new MovePath(List.of(Direction.SOUTH, Direction.SOUTH, Direction.SOUTH, Direction.SOUTH)), + new MovePath(List.of(Direction.EAST)), + new MovePath(List.of(Direction.EAST, Direction.EAST, Direction.EAST, Direction.EAST)), + new MovePath(List.of(Direction.WEST)), + new MovePath(List.of(Direction.WEST, Direction.WEST, Direction.WEST, Direction.WEST)) + ); + } + + @Test + public void 포는_현재위치에서_여러칸_떨어진_직선_목적지_경로를_생성한다() { + MoveStrategy strategy = new CannonMoveStrategy(); + List routes = strategy.makeRoutes(Position.of(4, 4), TeamColor.HAN); + + assertThat(routes).contains( + new Route(Position.of(4, 4), Position.of(0, 4), + List.of(Position.of(3, 4), Position.of(2, 4), Position.of(1, 4))), + new Route(Position.of(4, 4), Position.of(4, 8), + List.of(Position.of(4, 5), Position.of(4, 6), Position.of(4, 7))), + new Route(Position.of(4, 4), Position.of(8, 4), + List.of(Position.of(5, 4), Position.of(6, 4), Position.of(7, 4))) + ); + } + } + + @Nested + class 차단검사 { + @Test + public void 포는_다리가_되는_기물이_하나도_없으면_지나갈수_없다(){ + MoveStrategy moveStrategy = new CannonMoveStrategy(); + Route route = new Route(Position.of(4, 4), Position.of(1, 4), List.of(Position.of(3, 4), Position.of(2, 4))); + + boolean canMove = moveStrategy.canMove(route, List.of(), Optional.empty(), TeamColor.CHO); + assertThat(canMove).isFalse(); + } + + @Test + public void 포는_다리가_되는_기물이_포이면_지나갈수_없다(){ + MoveStrategy moveStrategy = new CannonMoveStrategy(); + Route route = new Route(Position.of(4, 4), Position.of(1, 4), List.of(Position.of(3, 4), Position.of(2, 4))); + + boolean canMove = moveStrategy.canMove( + route, + List.of(Piece.of(TeamColor.CHO, PieceType.CANNON)), + Optional.empty(), + TeamColor.CHO + ); + assertThat(canMove).isFalse(); + } + + @Test + public void 포는_다리가_되는_기물이_둘_이상이면_지나갈수_없다() { + MoveStrategy moveStrategy = new CannonMoveStrategy(); + Route route = new Route(Position.of(4, 4), Position.of(0, 4), List.of(Position.of(3, 4), Position.of(2, 4), Position.of(1, 4))); + + boolean canMove = moveStrategy.canMove( + route, + List.of( + Piece.of(TeamColor.CHO, PieceType.PAWN), + Piece.of(TeamColor.HAN, PieceType.HORSE) + ), + Optional.empty(), + TeamColor.CHO + ); + + assertThat(canMove).isFalse(); + } + + @Test + public void 포는_도착지에_상대방의_포가_있으면_잡을수_없다(){ + MoveStrategy moveStrategy = new CannonMoveStrategy(); + + Piece bridgePawn = Piece.of(TeamColor.CHO, PieceType.PAWN); + Piece targetCannon = Piece.of(TeamColor.HAN, PieceType.CANNON); + + boolean canMove = moveStrategy.canMove( + new Route(Position.of(4, 4), Position.of(1, 4), List.of(Position.of(3, 4), Position.of(2, 4))), + List.of(bridgePawn), + Optional.of(targetCannon), + TeamColor.CHO + ); + assertThat(canMove).isFalse(); + } + + @Test + public void 포는_도착지에_같은팀_기물이_있으면_이동할수_없다(){ + MoveStrategy moveStrategy = new CannonMoveStrategy(); + + Piece bridgeRook = Piece.of(TeamColor.HAN, PieceType.ROOK); + Piece targetHorse = Piece.of(TeamColor.CHO, PieceType.HORSE); + + boolean canMove = moveStrategy.canMove( + new Route(Position.of(4, 4), Position.of(1, 4), List.of(Position.of(3, 4), Position.of(2, 4))), + List.of(bridgeRook), + Optional.of(targetHorse), + TeamColor.CHO + ); + assertThat(canMove).isFalse(); + } + + @Test + public void 포는_일반_다리를_넘어_빈칸으로_정상적으로_이동가능하다(){ + MoveStrategy moveStrategy = new CannonMoveStrategy(); + + Piece bridgePawn = Piece.of(TeamColor.CHO, PieceType.PAWN); + + boolean canMove = moveStrategy.canMove( + new Route(Position.of(4, 4), Position.of(1, 4), List.of(Position.of(3, 4), Position.of(2, 4))), + List.of(bridgePawn), + Optional.empty(), + TeamColor.CHO + ); + assertThat(canMove).isTrue(); + } + + @Test + public void 포는_일반_다리를_넘어_도착지에_있는_적군기물을_포획가능하다(){ + MoveStrategy moveStrategy = new CannonMoveStrategy(); + + Piece bridgeHorse = Piece.of(TeamColor.CHO, PieceType.HORSE); + Piece targetRook = Piece.of(TeamColor.HAN, PieceType.ROOK); + + boolean canMove = moveStrategy.canMove( + new Route(Position.of(4, 4), Position.of(1, 4), List.of(Position.of(3, 4), Position.of(2, 4))), + List.of(bridgeHorse), + Optional.of(targetRook), + TeamColor.CHO + ); + assertThat(canMove).isTrue(); + } + + @Test + public void 포는_한칸_이동처럼_중간기물이_없는_경로로는_이동할수_없다() { + MoveStrategy moveStrategy = new CannonMoveStrategy(); + Route route = new Route(Position.of(4, 4), Position.of(3, 4), List.of()); + + boolean canMove = moveStrategy.canMove(route, List.of(), Optional.empty(), TeamColor.CHO); + + assertThat(canMove).isFalse(); + } + } +} + + diff --git a/src/test/java/strategy/move/ElephantMoveStrategyTest.java b/src/test/java/strategy/move/ElephantMoveStrategyTest.java new file mode 100644 index 0000000000..b897e50cb3 --- /dev/null +++ b/src/test/java/strategy/move/ElephantMoveStrategyTest.java @@ -0,0 +1,98 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.board.Route; +import domain.piece.TeamColor; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ElephantMoveStrategyTest { + + @Nested + class 이동경로 { + @Test + public void 초나라_상은_직진1칸_대각선2칸으로_이루어진_8개의_경로를_가진다() { + MoveStrategy strategy = new ElephantMoveStrategy(); + List paths = strategy.getPaths(TeamColor.CHO); + + assertThat(paths).hasSize(8); + assertThat(paths).contains( + new MovePath(List.of(Direction.NORTH, Direction.NORTH_EAST, Direction.NORTH_EAST)), + new MovePath(List.of(Direction.NORTH, Direction.NORTH_WEST, Direction.NORTH_WEST)), + new MovePath(List.of(Direction.EAST, Direction.NORTH_EAST, Direction.NORTH_EAST)) + ); + } + + @Test + public void 한나라_상은_직진1칸_대각선2칸으로_이루어진_8개의_경로를_가진다() { + MoveStrategy strategy = new ElephantMoveStrategy(); + List paths = strategy.getPaths(TeamColor.HAN); + + assertThat(paths).hasSize(8); + assertThat(paths).contains( + new MovePath(List.of(Direction.NORTH, Direction.NORTH_EAST, Direction.NORTH_EAST)), + new MovePath(List.of(Direction.NORTH, Direction.NORTH_WEST, Direction.NORTH_WEST)), + new MovePath(List.of(Direction.EAST, Direction.NORTH_EAST, Direction.NORTH_EAST)) + ); + } + } + + @Nested + class 차단검사 { + @Test + public void 상은_장애물이_없으면_지나갈수_있다(){ + MoveStrategy moveStrategy = new PawnMoveStrategy(); + Route route = new Route(Position.of(4, 4), Position.of(1, 2), List.of(Position.of(3, 4), Position.of(2, 3))); + + boolean canMove = moveStrategy.canMove(route, List.of(), Optional.empty(), TeamColor.CHO); + + assertThat(canMove).isTrue(); + } + + @Test + public void 상은_장애물이_하나라도_있으면_지나갈수_없다(){ + MoveStrategy moveStrategy = new PawnMoveStrategy(); + Route route = new Route(Position.of(4, 4), Position.of(1, 2), List.of(Position.of(3, 4), Position.of(2, 3))); + + boolean canMove = moveStrategy.canMove( + route, + List.of(Piece.of(TeamColor.CHO, PieceType.CANNON)), + Optional.empty(), + TeamColor.CHO + ); + + assertThat(canMove).isFalse(); + } + } + + @Nested + class 좌표생성 { + @Test + public void 상이_정상적으로_진행경로_좌표를_안다() { + Position curPos = Position.of(4, 4); + MoveStrategy moveStrategy = new ElephantMoveStrategy(); + + List routes = moveStrategy.makeRoutes(curPos, TeamColor.CHO); + assertThat(routes).containsExactlyInAnyOrder( + new Route(curPos, Position.of(1, 2), List.of(Position.of(3, 4), Position.of(2, 3))), + new Route(curPos, Position.of(1, 6), List.of(Position.of(3, 4), Position.of(2, 5))), + new Route(curPos, Position.of(7, 2), List.of(Position.of(5, 4), Position.of(6, 3))), + new Route(curPos, Position.of(7, 6), List.of(Position.of(5, 4), Position.of(6, 5))), + new Route(curPos, Position.of(2, 1), List.of(Position.of(4, 3), Position.of(3, 2))), + new Route(curPos, Position.of(6, 1), List.of(Position.of(4, 3), Position.of(5, 2))), + new Route(curPos, Position.of(2, 7), List.of(Position.of(4, 5), Position.of(3, 6))), + new Route(curPos, Position.of(6, 7), List.of(Position.of(4, 5), Position.of(5, 6))) + ); + } + } +} + + diff --git a/src/test/java/strategy/move/GuardMoveStrategyTest.java b/src/test/java/strategy/move/GuardMoveStrategyTest.java new file mode 100644 index 0000000000..6352c3581d --- /dev/null +++ b/src/test/java/strategy/move/GuardMoveStrategyTest.java @@ -0,0 +1,108 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.board.Route; +import domain.piece.TeamColor; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GuardMoveStrategyTest { + + @Nested + class 이동경로 { + @Test + public void 초나라_사는_8방향으로_이동_가능하다() { + MoveStrategy strategy = new KingMoveStrategy(); + List paths = strategy.getPaths(TeamColor.CHO); + + assertThat(paths).hasSize(8); + assertThat(paths).containsExactlyInAnyOrder( + new MovePath(List.of(Direction.NORTH)), + new MovePath(List.of(Direction.SOUTH)), + new MovePath(List.of(Direction.EAST)), + new MovePath(List.of(Direction.WEST)), + new MovePath(List.of(Direction.NORTH_EAST)), + new MovePath(List.of(Direction.NORTH_WEST)), + new MovePath(List.of(Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.SOUTH_WEST)) + ); + } + + @Test + public void 한나라_사는_8방향으로_이동_가능하다() { + MoveStrategy strategy = new KingMoveStrategy(); + List paths = strategy.getPaths(TeamColor.HAN); + + assertThat(paths).hasSize(8); + assertThat(paths).containsExactlyInAnyOrder( + new MovePath(List.of(Direction.NORTH)), + new MovePath(List.of(Direction.SOUTH)), + new MovePath(List.of(Direction.EAST)), + new MovePath(List.of(Direction.WEST)), + new MovePath(List.of(Direction.NORTH_EAST)), + new MovePath(List.of(Direction.NORTH_WEST)), + new MovePath(List.of(Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.SOUTH_WEST)) + ); + } + } + + @Nested + class 차단검사 { + @Test + public void 사는_장애물이_없으면_지나갈수_있다(){ + MoveStrategy moveStrategy = new PawnMoveStrategy(); + Route route = new Route(Position.of(1, 4), Position.of(0, 4), List.of()); + + boolean canMove = moveStrategy.canMove(route, List.of(), Optional.empty(), TeamColor.CHO); + + assertThat(canMove).isTrue(); + } + + @Test + public void 사는_장애물이_하나라도_있으면_지나갈수_없다(){ + MoveStrategy moveStrategy = new PawnMoveStrategy(); + Route route = new Route(Position.of(1, 4), Position.of(0, 4), List.of()); + + boolean canMove = moveStrategy.canMove( + route, + List.of(Piece.of(TeamColor.CHO, PieceType.CANNON)), + Optional.empty(), + TeamColor.CHO + ); + + assertThat(canMove).isFalse(); + } + } + + @Nested + class 좌표생성 { + @Test + public void 왕이_정상적으로_진행경로_좌표를_안다() { + Position curPos = Position.of(1, 4); + MoveStrategy moveStrategy = new GuardMoveStrategy(); + + List routes = moveStrategy.makeRoutes(curPos, TeamColor.CHO); + assertThat(routes).containsExactlyInAnyOrder( + new Route(curPos, Position.of(0, 4), List.of()), + new Route(curPos, Position.of(2, 4), List.of()), + new Route(curPos, Position.of(1, 3), List.of()), + new Route(curPos, Position.of(1, 5), List.of()), + new Route(curPos, Position.of(0, 3), List.of()), + new Route(curPos, Position.of(0, 5), List.of()), + new Route(curPos, Position.of(2, 3), List.of()), + new Route(curPos, Position.of(2, 5), List.of()) + ); + } + } +} + + diff --git a/src/test/java/strategy/move/HorseMoveStrategyTest.java b/src/test/java/strategy/move/HorseMoveStrategyTest.java new file mode 100644 index 0000000000..cc6e4a1b55 --- /dev/null +++ b/src/test/java/strategy/move/HorseMoveStrategyTest.java @@ -0,0 +1,108 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.board.Route; +import domain.piece.TeamColor; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HorseMoveStrategyTest { + + @Nested + class 이동경로 { + @Test + public void 초나라_마는_직진1칸_대각선1칸으로_이루어진_8개의_경로를_가진다() { + MoveStrategy strategy = new HorseMoveStrategy(); + List paths = strategy.getPaths(TeamColor.CHO); + + assertThat(paths).hasSize(8); + assertThat(paths).contains( + new MovePath(List.of(Direction.NORTH, Direction.NORTH_EAST)), + new MovePath(List.of(Direction.NORTH, Direction.NORTH_WEST)), + new MovePath(List.of(Direction.EAST, Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.EAST, Direction.NORTH_EAST)), + new MovePath(List.of(Direction.SOUTH, Direction.SOUTH_WEST)), + new MovePath(List.of(Direction.SOUTH, Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.WEST, Direction.NORTH_WEST)), + new MovePath(List.of(Direction.WEST, Direction.SOUTH_WEST)) + ); + } + + @Test + public void 한나라_마는_직진1칸_대각선1칸으로_이루어진_8개의_경로를_가진다() { + MoveStrategy strategy = new HorseMoveStrategy(); + List paths = strategy.getPaths(TeamColor.HAN); + + assertThat(paths).hasSize(8); + assertThat(paths).contains( + new MovePath(List.of(Direction.NORTH, Direction.NORTH_EAST)), + new MovePath(List.of(Direction.NORTH, Direction.NORTH_WEST)), + new MovePath(List.of(Direction.EAST, Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.EAST, Direction.NORTH_EAST)), + new MovePath(List.of(Direction.SOUTH, Direction.SOUTH_WEST)), + new MovePath(List.of(Direction.SOUTH, Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.WEST, Direction.NORTH_WEST)), + new MovePath(List.of(Direction.WEST, Direction.SOUTH_WEST)) + ); + } + } + + @Nested + class 차단검사 { + @Test + public void 마는_장애물이_없으면_지나갈수_있다(){ + MoveStrategy moveStrategy = new PawnMoveStrategy(); + Route route = new Route(Position.of(4, 4), Position.of(2, 3), List.of(Position.of(3, 4))); + + boolean canMove = moveStrategy.canMove(route, List.of(), Optional.empty(), TeamColor.CHO); + + assertThat(canMove).isTrue(); + } + + @Test + public void 마는_장애물이_하나라도_있으면_지나갈수_없다(){ + MoveStrategy moveStrategy = new PawnMoveStrategy(); + Route route = new Route(Position.of(4, 4), Position.of(2, 3), List.of(Position.of(3, 4))); + + boolean canMove = moveStrategy.canMove( + route, + List.of(Piece.of(TeamColor.CHO, PieceType.CANNON)), + Optional.empty(), + TeamColor.CHO + ); + + assertThat(canMove).isFalse(); + } + } + + @Nested + class 좌표생성 { + @Test + public void 마가_정상적으로_진행경로_좌표를_안다() { + Position curPos = Position.of(4, 4); + MoveStrategy moveStrategy = new HorseMoveStrategy(); + + List routes = moveStrategy.makeRoutes(curPos, TeamColor.CHO); + assertThat(routes).containsExactlyInAnyOrder( + new Route(curPos, Position.of(2, 3), List.of(Position.of(3, 4))), + new Route(curPos, Position.of(2, 5), List.of(Position.of(3, 4))), + new Route(curPos, Position.of(6, 3), List.of(Position.of(5, 4))), + new Route(curPos, Position.of(6, 5), List.of(Position.of(5, 4))), + new Route(curPos, Position.of(3, 2), List.of(Position.of(4, 3))), + new Route(curPos, Position.of(5, 2), List.of(Position.of(4, 3))), + new Route(curPos, Position.of(3, 6), List.of(Position.of(4, 5))), + new Route(curPos, Position.of(5, 6), List.of(Position.of(4, 5))) + ); + } + } +} + + diff --git a/src/test/java/strategy/move/KingMoveStrategyTest.java b/src/test/java/strategy/move/KingMoveStrategyTest.java new file mode 100644 index 0000000000..9649a0c369 --- /dev/null +++ b/src/test/java/strategy/move/KingMoveStrategyTest.java @@ -0,0 +1,108 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.board.Route; +import domain.piece.TeamColor; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KingMoveStrategyTest { + + @Nested + class 이동경로 { + @Test + public void 초나라_왕은_8방향으로_이동_가능하다() { + MoveStrategy strategy = new KingMoveStrategy(); + List paths = strategy.getPaths(TeamColor.CHO); + + assertThat(paths).hasSize(8); + assertThat(paths).containsExactlyInAnyOrder( + new MovePath(List.of(Direction.NORTH)), + new MovePath(List.of(Direction.SOUTH)), + new MovePath(List.of(Direction.EAST)), + new MovePath(List.of(Direction.WEST)), + new MovePath(List.of(Direction.NORTH_EAST)), + new MovePath(List.of(Direction.NORTH_WEST)), + new MovePath(List.of(Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.SOUTH_WEST)) + ); + } + + @Test + public void 한나라_왕은_8방향으로_이동_가능하다() { + MoveStrategy strategy = new KingMoveStrategy(); + List paths = strategy.getPaths(TeamColor.HAN); + + assertThat(paths).hasSize(8); + assertThat(paths).containsExactlyInAnyOrder( + new MovePath(List.of(Direction.NORTH)), + new MovePath(List.of(Direction.SOUTH)), + new MovePath(List.of(Direction.EAST)), + new MovePath(List.of(Direction.WEST)), + new MovePath(List.of(Direction.NORTH_EAST)), + new MovePath(List.of(Direction.NORTH_WEST)), + new MovePath(List.of(Direction.SOUTH_EAST)), + new MovePath(List.of(Direction.SOUTH_WEST)) + ); + } + } + + @Nested + class 차단검사 { + @Test + public void 왕은_장애물이_없으면_지나갈수_있다(){ + MoveStrategy moveStrategy = new PawnMoveStrategy(); + Route route = new Route(Position.of(1, 4), Position.of(0, 4), List.of()); + + boolean canMove = moveStrategy.canMove(route, List.of(), Optional.empty(), TeamColor.CHO); + + assertThat(canMove).isTrue(); + } + + @Test + public void 왕은_장애물이_하나라도_있으면_지나갈수_없다(){ + MoveStrategy moveStrategy = new PawnMoveStrategy(); + Route route = new Route(Position.of(1, 4), Position.of(0, 4), List.of()); + + boolean canMove = moveStrategy.canMove( + route, + List.of(Piece.of(TeamColor.CHO, PieceType.CANNON)), + Optional.empty(), + TeamColor.CHO + ); + + assertThat(canMove).isFalse(); + } + } + + @Nested + class 좌표생성 { + @Test + public void 왕이_정상적으로_진행경로_좌표를_안다() { + Position curPos = Position.of(1, 4); + MoveStrategy moveStrategy = new KingMoveStrategy(); + + List routes = moveStrategy.makeRoutes(curPos, TeamColor.CHO); + assertThat(routes).containsExactlyInAnyOrder( + new Route(curPos, Position.of(0, 4), List.of()), + new Route(curPos, Position.of(2, 4), List.of()), + new Route(curPos, Position.of(1, 3), List.of()), + new Route(curPos, Position.of(1, 5), List.of()), + new Route(curPos, Position.of(0, 3), List.of()), + new Route(curPos, Position.of(0, 5), List.of()), + new Route(curPos, Position.of(2, 3), List.of()), + new Route(curPos, Position.of(2, 5), List.of()) + ); + } + } +} + + diff --git a/src/test/java/strategy/move/MoveStrategyTest.java b/src/test/java/strategy/move/MoveStrategyTest.java new file mode 100644 index 0000000000..d949eaee4d --- /dev/null +++ b/src/test/java/strategy/move/MoveStrategyTest.java @@ -0,0 +1,10 @@ +package strategy.move; + +import org.junit.jupiter.api.Test; + +public class MoveStrategyTest { + + +} + + diff --git a/src/test/java/strategy/move/PawnMoveStrategyTest.java b/src/test/java/strategy/move/PawnMoveStrategyTest.java new file mode 100644 index 0000000000..ba16694d6b --- /dev/null +++ b/src/test/java/strategy/move/PawnMoveStrategyTest.java @@ -0,0 +1,108 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.board.Route; +import domain.piece.TeamColor; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PawnMoveStrategyTest { + + public Piece piece; + + @BeforeEach + public void setUp() { + piece = Piece.of(TeamColor.CHO, PieceType.PAWN); + } + + @Nested + class 이동경로 { + @Test + public void 초나라_졸은_북동서로_이동_가능하다() { + MoveStrategy moveStrategy = new PawnMoveStrategy(); + List movePathList = moveStrategy.getPaths(TeamColor.CHO); + assertThat(movePathList).contains(new MovePath(List.of(Direction.NORTH))); + assertThat(movePathList).contains(new MovePath(List.of(Direction.WEST))); + assertThat(movePathList).contains(new MovePath(List.of(Direction.EAST))); + assertThat(movePathList).doesNotContain(new MovePath(List.of(Direction.SOUTH))); + } + + @Test + public void 한나라_졸은_남동서로_이동_가능하다() { + MoveStrategy moveStrategy = new PawnMoveStrategy(); + List movePaths = moveStrategy.getPaths(TeamColor.HAN); + assertThat(movePaths).contains(new MovePath(List.of(Direction.SOUTH))); + assertThat(movePaths).contains(new MovePath(List.of(Direction.WEST))); + assertThat(movePaths).contains(new MovePath(List.of(Direction.EAST))); + assertThat(movePaths).doesNotContain(new MovePath(List.of(Direction.NORTH))); + } + } + + @Nested + class 좌표생성 { + @Test + public void 초나라_졸이_정상적으로_진행경로_좌표를_안다(){ + Position curPos = Position.of(2,3); + MoveStrategy moveStrategy = new PawnMoveStrategy(); + + List routes = moveStrategy.makeRoutes(curPos, TeamColor.CHO); + assertThat(routes).containsExactlyInAnyOrder( + new Route(curPos, Position.of(1, 3), List.of()), + new Route(curPos, Position.of(2, 4), List.of()), + new Route(curPos, Position.of(2, 2), List.of()) + ); + } + + @Test + public void 한나라_졸은_현재위치와_이동방향을_기반으로_이동가능한_좌표들_구한다(){ + MoveStrategy moveStrategy = new PawnMoveStrategy(); + Position curPos = Position.of(3,4); + + List possibleRoutes = moveStrategy.makeRoutes(curPos, TeamColor.HAN); + assertThat(possibleRoutes).containsExactlyInAnyOrder( + new Route(curPos,Position.of(4,4),List.of()), + new Route(curPos,Position.of(3,5),List.of()), + new Route(curPos,Position.of(3,3),List.of()) + ); + } + } + + @Nested + class 차단검사 { + @Test + public void 졸은_장애물이_없으면_지나갈수_있다(){ + MoveStrategy moveStrategy = new PawnMoveStrategy(); + Route route = new Route(Position.of(4, 4), Position.of(3, 4), List.of()); + + boolean canMove = moveStrategy.canMove(route, List.of(), Optional.empty(), TeamColor.CHO); + + assertThat(canMove).isTrue(); + } + + @Test + public void 졸은_장애물이_하나라도_있으면_지나갈수_없다(){ + MoveStrategy moveStrategy = new PawnMoveStrategy(); + Route route = new Route(Position.of(4, 4), Position.of(3, 4), List.of()); + + boolean canMove = moveStrategy.canMove( + route, + List.of(Piece.of(TeamColor.CHO,PieceType.CANNON)), + Optional.empty(), + TeamColor.CHO + ); + + assertThat(canMove).isFalse(); + } + } +} + + diff --git a/src/test/java/strategy/move/RookMoveStrategyTest.java b/src/test/java/strategy/move/RookMoveStrategyTest.java new file mode 100644 index 0000000000..32ed858879 --- /dev/null +++ b/src/test/java/strategy/move/RookMoveStrategyTest.java @@ -0,0 +1,99 @@ +package strategy.move; + +import domain.board.Direction; +import domain.board.MovePath; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.board.Position; +import domain.board.Route; +import domain.piece.TeamColor; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RookMoveStrategyTest { + + @Nested + class 이동경로 { + @Test + public void 차는_초나라에서_동서남북_직선_경로를_보드_끝까지_가진다() { + MoveStrategy strategy = new RookMoveStrategy(); + List paths = strategy.getPaths(TeamColor.CHO); + + assertThat(paths).hasSize(36); + assertThat(paths).contains( + new MovePath(List.of(Direction.NORTH)), + new MovePath(List.of(Direction.NORTH, Direction.NORTH, Direction.NORTH, Direction.NORTH)), + new MovePath(List.of(Direction.SOUTH)), + new MovePath(List.of(Direction.SOUTH, Direction.SOUTH, Direction.SOUTH, Direction.SOUTH)), + new MovePath(List.of(Direction.EAST)), + new MovePath(List.of(Direction.EAST, Direction.EAST, Direction.EAST, Direction.EAST)), + new MovePath(List.of(Direction.WEST)), + new MovePath(List.of(Direction.WEST, Direction.WEST, Direction.WEST, Direction.WEST)) + ); + } + + @Test + public void 차는_현재위치에서_여러칸_떨어진_직선_목적지_경로를_생성한다() { + MoveStrategy strategy = new RookMoveStrategy(); + List routes = strategy.makeRoutes(Position.of(4, 4), TeamColor.HAN); + + assertThat(routes).contains( + new Route(Position.of(4, 4), Position.of(0, 4), + List.of(Position.of(3, 4), Position.of(2, 4), Position.of(1, 4))), + new Route(Position.of(4, 4), Position.of(4, 8), + List.of(Position.of(4, 5), Position.of(4, 6), Position.of(4, 7))), + new Route(Position.of(4, 4), Position.of(4, 0), + List.of(Position.of(4, 3), Position.of(4, 2), Position.of(4, 1))) + ); + } + } + + @Nested + class 차단검사 { + @Test + public void 차는_장애물이_없으면_지나갈수_있다(){ + MoveStrategy moveStrategy = new RookMoveStrategy(); + Route route = new Route(Position.of(4, 4), Position.of(1, 4), List.of(Position.of(3, 4), Position.of(2, 4))); + + boolean canMove = moveStrategy.canMove(route, List.of(), Optional.empty(), TeamColor.CHO); + + assertThat(canMove).isTrue(); + } + + @Test + public void 차는_장애물이_하나라도_있으면_지나갈수_없다(){ + MoveStrategy moveStrategy = new RookMoveStrategy(); + Route route = new Route(Position.of(4, 4), Position.of(1, 4), List.of(Position.of(3, 4), Position.of(2, 4))); + + boolean canMove = moveStrategy.canMove( + route, + List.of(Piece.of(TeamColor.CHO, PieceType.CANNON)), + Optional.empty(), + TeamColor.CHO + ); + + assertThat(canMove).isFalse(); + } + + @Test + public void 차는_도착지에_같은팀_기물이_있으면_이동할수_없다() { + MoveStrategy moveStrategy = new RookMoveStrategy(); + Route route = new Route(Position.of(4, 4), Position.of(1, 4), List.of(Position.of(3, 4), Position.of(2, 4))); + + boolean canMove = moveStrategy.canMove( + route, + List.of(), + Optional.of(Piece.of(TeamColor.CHO, PieceType.GUARD)), + TeamColor.CHO + ); + + assertThat(canMove).isFalse(); + } + } +} + +