diff --git a/README.md b/README.md index 9003d49411..1ed5527795 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,12 @@ -# 1단계 - 보드 초기화 +# 실행 방법 + +`docker-compose up -d`로 db 세팅 후 프로젝트 실행 + +--- + +# Cycle 1 + +## 1단계 - 보드 초기화 ``` 한나라의 상차림을 선택해주세요. @@ -35,7 +43,7 @@  +-------------------+ ``` -## 기능 정리 +### 기능 정리 - 상차림 순서 관리 - 한나라부터 상차림 입력을 받는다. @@ -56,7 +64,7 @@ - 나라 정보 - 한, 초 -## 구현 순서 +### 구현 순서 * 초나라/한나라 상차림 입력 * 나라 별 유효한 상차림을 입력받는다. @@ -70,11 +78,11 @@ * 장기판 위치 범위를 초과하면 예외가 발생해야한다. * 장기판이 필요하다. -# 2단계 - 기물 이동 +## 2단계 - 기물 이동 - 2단계에서는 기물 이동 구현이 목적이니까 종료는 `Ctrl+C`로 강제 종료한다. -## 잘못된 기물 선택 시나리오 +### 잘못된 기물 선택 시나리오 1. 다른 나라의 기물을 선택했을 때 1. 예외: 다른나라의 기물 선택 @@ -83,7 +91,7 @@ 1. 예외: 기물이 없는 곳을 선택 2. `[ERROR] 현재 위치에 존재하는 기물이 없습니다.` -## 기물 이동 규칙 +### 기물 이동 규칙 - 궁성 영역 - 고려하지 않는다. (사이클 2) - 기물 이동 규칙 공통 @@ -104,7 +112,7 @@ - 한 칸이라서 경로에 기물이 있을 리가 없다. - `사, 장`: 궁성 영역이므로 구현하지 않는다. -## 입출력 예시 +### 입출력 예시 ``` 1단계 ... @@ -129,7 +137,6 @@ [초나라] 기물 卒의 다음 위치를 선택해주세요. (쉼표 기준으로 분리) 위치: 6,7 -    0 1 2 3 4 5 6 7 8  +-------------------+ 0| 車 馬 象 士 * 士 象 馬 車 | @@ -149,11 +156,9 @@ [ERROR] 현재 위치에 존재하는 기물이 없습니다. - [한나라] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리) 기물: 3,0 - [한나라] 기물 兵의 다음 위치를 선택해주세요. (쉼표 기준으로 분리) 위치: 3,1 @@ -172,7 +177,7 @@  +-------------------+ ``` -## 구현 순서 +### 구현 순서 1. 기물 이동 기능 추가 * 나라 별 입력 @@ -183,4 +188,256 @@ * 기물의 이동할 위치를 입력 받는다. * 잘못된 범위라면 예외를 발생한다. * 해당 위치에 아군의 기물이 존재한다면 예외가 발생한다. - * 기물 별로 이동할 수 없는 목적지라면 예외가 발생한다. \ No newline at end of file + * 기물 별로 이동할 수 없는 목적지라면 예외가 발생한다. + +--- + +# Cycle2 + +## 1단계 - 기물의 확장 + +1. 궁성 영역을 정의한다. + * 한나라 궁성: (0,3) ~ (2,5) 내 9개 위치 + * 초나라 궁성: (7,3) ~ (9,5) 내 9개 위치 + * 궁성 내 대각선 이동 가능 교차점을 정의한다. + * 한나라: (0,3), (0,5), (1,4), (2,3), (2,5) + * 초나라: (7,3), (7,5), (8,4), (9,3), (9,5) +2. 장(將/楚), 사(士)의 이동을 구현한다. + * 출발 지점이 궁성 영역이 아니라면 예외가 발생한다. + * 도착 지점이 궁성 영역이 아니라면 예외가 발생한다. + * 직선(상하좌우) 한 칸 이동이 가능하다. + * 대각선 한 칸 이동이 가능하다. + * 대각선 이동은 궁성 내 대각선 교차점에서만 출발할 수 있다. +3. 차(車)의 궁성 내 이동을 추가 구현한다. + * 궁성 내에서 대각선 이동이 가능하다. + * 대각선 이동은 궁성 내 대각선 교차점에서만 출발할 수 있다. + * 대각선 이동 경로 상에 기물이 있으면 예외가 발생한다. +4. 포(包)의 궁성 내 이동을 추가 구현한다. + * 궁성 내에서 대각선 이동이 가능하다. + * 대각선 이동은 궁성 내 대각선 교차점에서만 출발할 수 있다. + * 대각선 이동 경로 상에 정확히 하나의 기물을 뛰어넘어야 한다. + * 포는 뛰어넘을 수 없다. +5. 졸(卒)/병(兵)의 궁성 내 이동을 추가 구현한다. + * 궁성 내에서 대각선 한 칸 이동이 가능하다. + * 대각선 이동은 궁성 내 대각선 교차점에서만 출발할 수 있다. + * 전진 방향 대각선만 이동 가능하다. +6. 게임 종료 + * 왕(장)이 잡히면 게임이 종료된다. + * 한나라 왕이 잡히면 초나라가 승리한다. + * 초나라 왕이 잡히면 한나라가 승리한다. + * 게임 종료 시 승리한 팀을 출력한다. +7. 점수 계산 + * 왕이 잡히지 않은 경우 점수로 승패를 결정한다. (빅장) + * 점수가 높은 팀이 승리한다. + * 1.5점 가산으로 인해 무승부는 발생하지 않는다. + * 초나라는 후수이므로 1.5점의 추가 점수를 받는다. + +### 예시 출력 1) 빅장 + +``` +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 馬 象 士 * 士 象 馬 車 | +1| * * * * 漢 * * * * | +2| * 包 * * * * * 包 * | +3| 兵 * 兵 兵 * * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 卒 * * 卒 卒 * | +7| * 包 * * * * * 包 * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 馬 車 | + +-------------------+ + +[초나라] 빅장입니다! 종료하시겠습니까? (Y, N) +N + +[초나라] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리) +기물: 6,3 + +[초나라] 기물 卒의 다음 위치를 선택해주세요. (쉼표 기준으로 분리) +위치: 5,3 + +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 馬 象 士 * 士 象 馬 車 | +1| * * * * 漢 * * * * | +2| * 包 * * * * * 包 * | +3| 兵 * 兵 兵 * * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * 卒 * * * * * | +6| 卒 * 卒 * * * 卒 卒 * | +7| * 包 * * * * * 包 * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 馬 車 | + +-------------------+ + +[한나라] 빅장입니다! 종료하시겠습니까? (Y, N) +Y + +한나라 승 +초나라: 72점 +한나라: 73.5점 +``` + +### 예시 출력 2) 왕 죽음 + +``` +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 * 象 士 * 士 象 馬 車 | +1| * * * * 漢 * * * * | +2| * 包 馬 * * * * 包 * | +3| 兵 * 兵 兵 * * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 * 卒 * 卒 * 卒 | +7| * 包 * * 包 * 馬 * * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 * 車 | + +-------------------+ + +[초나라] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리) +기물: 7,4 + +[초나라] 기물 包의 다음 위치를 선택해주세요. (쉼표 기준으로 분리) +위치: 1,4 + +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 * 象 士 * 士 象 馬 車 | +1| * * * * 包 * * * * | +2| * 包 馬 * * * * 包 * | +3| 兵 * 兵 兵 * * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 * 卒 * 卒 * 卒 | +7| * 包 * * * * 馬 * * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 * 車 | + +-------------------+ + +초나라 승 +``` + +## 2단계 - DB 적용 + +1. 이전에 하던 게임을 다시 시작할 수 있어야 한다. + * 장기를 저장하고 그대로 꺼내온다. +2. 장기 게임방을 만들고 장기 게임방에 입장할 수 있는 기능을 추가한다. (선택) + * 추후 작성 + +### 잘못된 게임 메뉴 선택 + +```text +> 장기 게임 메뉴 + +1. 게임 이어서 진행하기 +2. 새로 시작하기 +3. 종료하기 + +메뉴를 선택해주세요: +4 + +[ERROR] 잘못된 입력입니다. +``` + +### 종료된 게임 선택 + +```text + +> 장기 게임 메뉴 + +1. 게임 이어서 진행하기 +2. 새로 시작하기 +3. 종료하기 + +메뉴를 선택해주세요: +1 + +> 이어서 진행할 게임을 선택해주세요. +1번 게임: 종료 상태 +2번 게임: 진행 상태 + +번호를 입력해주세요: +1 + +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 * 象 士 * 士 象 馬 車 | +1| * * * * 包 * * * * | +2| * 包 馬 * * * * 包 * | +3| 兵 * 兵 兵 * * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 * 卒 * 卒 * 卒 | +7| * 包 * * * * 馬 * * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 * 車 | + +-------------------+ + +초나라 승 +``` + +### 존재하지 않는 게임 선택 + +```text + +> 장기 게임 메뉴 + +1. 게임 이어서 진행하기 +2. 새로 시작하기 +3. 종료하기 + +메뉴를 선택해주세요: +1 + +> 이어서 진행할 게임을 선택해주세요. +1번 게임: 종료 상태 +2번 게임: 진행 상태 + +번호를 입력해주세요: +3 + +[ERROR] 1번 장기 게임이 존재하지 않습니다. +``` + +### 이전 게임 다시 시작 + +```text + +> 장기 게임 메뉴 + +1. 게임 이어서 진행하기 +2. 새로 시작하기 +3. 종료하기 + +메뉴를 선택해주세요: +1 + +> 이어서 진행할 게임을 선택해주세요. +1번 게임: 종료 상태 +2번 게임: 진행 상태 + +번호를 입력해주세요: +2 + +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 * 象 士 * 士 象 馬 車 | +1| * * * * 漢 * * * * | +2| * 包 馬 * * * * 包 * | +3| 兵 * 兵 兵 * * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 * 卒 * 卒 * 卒 | +7| * 包 * * 包 * 馬 * * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 * 車 | + +-------------------+ + +[초나라] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리) + +... + +``` \ No newline at end of file diff --git a/build.gradle b/build.gradle index ce846f70cc..2ad58f2118 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,8 @@ repositories { } dependencies { + implementation 'com.mysql:mysql-connector-j:8.4.0' + testImplementation platform('org.junit:junit-bom:5.11.4') testImplementation platform('org.assertj:assertj-bom:3.27.3') testImplementation('org.junit.jupiter:junit-jupiter') diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..7696350216 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + db: + image: mysql:8.4 + container_name: janggi-db + restart: always + environment: + MYSQL_DATABASE: janggi_db + MYSQL_USER: janggi + MYSQL_PASSWORD: janggi1234 + MYSQL_ROOT_PASSWORD: root_password + ports: + - "3309:3306" + volumes: + - ./init:/docker-entrypoint-initdb.d \ No newline at end of file diff --git a/init/v1_ddl.sql b/init/v1_ddl.sql new file mode 100644 index 0000000000..7c6e01fcc6 --- /dev/null +++ b/init/v1_ddl.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS janggi_game ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + status VARCHAR(20) NOT NULL COMMENT '게임 상태', + turn VARCHAR(10) NOT NULL COMMENT '현재 게임 턴', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS piece ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + game_id BIGINT NOT NULL, + `row` INT NOT NULL COMMENT '행 좌표 (0~9)', + `col` INT NOT NULL COMMENT '열 좌표 (0~8)', + `type` VARCHAR(20) NOT NULL COMMENT '기물 종류)', + `team` VARCHAR(20) NOT NULL COMMENT '진영', + FOREIGN KEY (game_id) REFERENCES janggi_game(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE INDEX idx_game_id ON piece(game_id); \ No newline at end of file diff --git a/src/main/java/Application.java b/src/main/java/Application.java index c4e80ce0a4..12142977a6 100644 --- a/src/main/java/Application.java +++ b/src/main/java/Application.java @@ -1,10 +1,25 @@ -import controller.JanggiController; -import view.InputView; -import view.OutputView; +import application.JanggiQueryService; +import application.JanggiService; +import application.JanggiServiceImpl; +import application.JanggiTxService; +import javax.sql.DataSource; +import repository.JanggiRepository; +import repository.JdbcJanggiRepository; +import repository.db.DataSourceManager; +import repository.db.TransactionTemplate; +import ui.FrontController; public class Application { + public static void main(String[] args) { - JanggiController janggiController = new JanggiController(new InputView(), new OutputView()); - janggiController.run(); + DataSource dataSource = DataSourceManager.getDataSource(); + TransactionTemplate transactionTemplate = new TransactionTemplate(dataSource); + JanggiRepository janggiRepository = new JdbcJanggiRepository(dataSource); + + JanggiServiceImpl janggiServiceImpl = new JanggiServiceImpl(janggiRepository); + JanggiService janggiService = new JanggiTxService(transactionTemplate, janggiServiceImpl); + JanggiQueryService janggiQueryService = new JanggiQueryService(janggiRepository); + + new FrontController(janggiService, janggiQueryService).run(); } } diff --git a/src/main/java/application/JanggiQueryService.java b/src/main/java/application/JanggiQueryService.java new file mode 100644 index 0000000000..a25ab839f2 --- /dev/null +++ b/src/main/java/application/JanggiQueryService.java @@ -0,0 +1,52 @@ +package application; + +import java.util.Map; +import model.GameStatus; +import model.JanggiGame; +import model.Team; +import model.coordinate.Position; +import model.piece.Piece; +import repository.JanggiRepository; + +public class JanggiQueryService { + + private final JanggiRepository janggiRepository; + + public JanggiQueryService(JanggiRepository janggiRepository) { + this.janggiRepository = janggiRepository; + } + + public JanggiResultDto getGameResult(Long janggiId) { + JanggiGame janggiGame = getGame(janggiId); + return JanggiResultDto.from(janggiGame); + } + + public Map getBoardResponse(Long janggiId) { + return getGame(janggiId).getBoard(); + } + + public boolean canPlaying(Long janggiId) { + return getGame(janggiId).canPlaying(); + } + + public Team getTurn(Long janggiId) { + return getGame(janggiId).turn(); + } + + public Piece selectPiece(Long janggiId, Position current) { + return getGame(janggiId).selectPiece(current); + } + + public boolean isBigJang(Long janggiId) { + return getGame(janggiId).isBigJang(); + } + + public Map collectGameStatus() { + return janggiRepository.collectGameStatus(); + } + + private JanggiGame getGame(Long janggiId) { + return janggiRepository.findById(janggiId) + .orElseThrow(() -> new IllegalArgumentException(janggiId + "번 장기 게임이 존재하지 않습니다.")); + } +} diff --git a/src/main/java/application/JanggiResultDto.java b/src/main/java/application/JanggiResultDto.java new file mode 100644 index 0000000000..ab69d5482a --- /dev/null +++ b/src/main/java/application/JanggiResultDto.java @@ -0,0 +1,20 @@ +package application; + +import java.util.Map; +import model.JanggiGame; +import model.Team; + +public record JanggiResultDto( + Team winner, + boolean bigJangDone, + Map finalScore +) { + + public static JanggiResultDto from(JanggiGame janggiGame) { + return new JanggiResultDto( + janggiGame.resolveWinner(), + janggiGame.isBigJangDone(), + janggiGame.calculateFinalScore() + ); + } +} diff --git a/src/main/java/application/JanggiService.java b/src/main/java/application/JanggiService.java new file mode 100644 index 0000000000..f8a73cb542 --- /dev/null +++ b/src/main/java/application/JanggiService.java @@ -0,0 +1,13 @@ +package application; + +import model.board.TeamFormation; + +public interface JanggiService { + + Long createJanggiGame(TeamFormation hanFormation, TeamFormation choFormation); + + void finishByBigJang(Long janggiId); + + void movePiece(Long janggiId, MoveCommand command); + +} diff --git a/src/main/java/application/JanggiServiceImpl.java b/src/main/java/application/JanggiServiceImpl.java new file mode 100644 index 0000000000..880a7adf32 --- /dev/null +++ b/src/main/java/application/JanggiServiceImpl.java @@ -0,0 +1,42 @@ +package application; + +import model.JanggiGame; +import model.board.Board; +import model.board.BoardFactory; +import model.board.TeamFormation; +import repository.JanggiRepository; + +public class JanggiServiceImpl implements JanggiService { + + private final JanggiRepository janggiRepository; + + public JanggiServiceImpl(JanggiRepository janggiRepository) { + this.janggiRepository = janggiRepository; + } + + public Long createJanggiGame(TeamFormation hanFormation, TeamFormation choFormation) { + Board board = BoardFactory.generateDefaultPieces(); + board.arrangePieces(hanFormation.generate()); + board.arrangePieces(choFormation.generate()); + + JanggiGame janggiGame = new JanggiGame(board); + return janggiRepository.save(janggiGame); + } + + public void finishByBigJang(Long janggiId) { + JanggiGame janggiGame = getGame(janggiId); + janggiGame.finishByBigJang(); + janggiRepository.update(janggiId, janggiGame); + } + + public void movePiece(Long janggiId, MoveCommand command) { + JanggiGame janggiGame = getGame(janggiId); + janggiGame.movePiece(command.current(), command.next()); + janggiRepository.update(janggiId, janggiGame); + } + + private JanggiGame getGame(Long janggiId) { + return janggiRepository.findById(janggiId) + .orElseThrow(() -> new IllegalArgumentException(janggiId + "번 장기 게임이 존재하지 않습니다.")); + } +} diff --git a/src/main/java/application/JanggiTxService.java b/src/main/java/application/JanggiTxService.java new file mode 100644 index 0000000000..e162755735 --- /dev/null +++ b/src/main/java/application/JanggiTxService.java @@ -0,0 +1,30 @@ +package application; + +import model.board.TeamFormation; +import repository.db.TransactionTemplate; + +public class JanggiTxService implements JanggiService { + + private final TransactionTemplate transactionTemplate; + private final JanggiService janggiService; + + public JanggiTxService(TransactionTemplate transactionTemplate, JanggiService janggiService) { + this.transactionTemplate = transactionTemplate; + this.janggiService = janggiService; + } + + @Override + public Long createJanggiGame(TeamFormation hanFormation, TeamFormation choFormation) { + return transactionTemplate.execute(() -> janggiService.createJanggiGame(hanFormation, choFormation)); + } + + @Override + public void finishByBigJang(Long janggiId) { + transactionTemplate.execute(() -> janggiService.finishByBigJang(janggiId)); + } + + @Override + public void movePiece(Long janggiId, MoveCommand command) { + transactionTemplate.execute(() -> janggiService.movePiece(janggiId, command)); + } +} diff --git a/src/main/java/application/MoveCommand.java b/src/main/java/application/MoveCommand.java new file mode 100644 index 0000000000..178639cafc --- /dev/null +++ b/src/main/java/application/MoveCommand.java @@ -0,0 +1,6 @@ +package application; + +import model.coordinate.Position; + +public record MoveCommand(Position current, Position next) { +} diff --git a/src/main/java/controller/JanggiController.java b/src/main/java/controller/JanggiController.java deleted file mode 100644 index 12f5c8d293..0000000000 --- a/src/main/java/controller/JanggiController.java +++ /dev/null @@ -1,61 +0,0 @@ -package controller; - -import static controller.Retrier.retry; -import static model.Team.CHO; -import static model.Team.HAN; - -import java.util.function.Consumer; -import model.JanggiGame; -import model.Team; -import model.board.Board; -import model.board.BoardFactory; -import model.board.JanggiFormation; -import model.board.Position; -import model.piece.Piece; -import view.InputView; -import view.OutputView; - -public class JanggiController { - private final InputView inputView; - private final OutputView outputView; - - public JanggiController(InputView inputView, OutputView outputView) { - this.inputView = inputView; - this.outputView = outputView; - } - - public void run() { - Board board = createBoardByFormation(); - outputView.displayBoard(board.board()); - - JanggiGame janggiGame = new JanggiGame(board); - while (true) { - retry(() -> playByTurn(janggiGame), processError()); - outputView.displayBoard(board.board()); - } - } - - private Board createBoardByFormation() { - JanggiFormation hanFormation = retry(() -> inputView.readFormationByTeam(HAN), processError()); - JanggiFormation choFormation = retry(() -> inputView.readFormationByTeam(CHO), processError()); - - Board board = BoardFactory.generateDefaultPieces(); - board.arrangePieces(hanFormation.generateByTeam(HAN)); - board.arrangePieces(choFormation.generateByTeam(CHO)); - return board; - } - - private void playByTurn(JanggiGame janggiGame) { - Team currentTurn = janggiGame.getTurn(); - - Position current = inputView.readPiecePositionForMove(currentTurn); - Piece piece = janggiGame.selectPiece(current); - - Position next = inputView.readPiecePositionForArrange(currentTurn, piece); - janggiGame.movePiece(current, next); - } - - private Consumer processError() { - return (e) -> outputView.displayError(e.getMessage()); - } -} diff --git a/src/main/java/model/GameStatus.java b/src/main/java/model/GameStatus.java new file mode 100644 index 0000000000..ab42d80536 --- /dev/null +++ b/src/main/java/model/GameStatus.java @@ -0,0 +1,8 @@ +package model; + +public enum GameStatus { + PLAYING, + BIG_JANG, + BIG_JANG_DONE, + DONE +} diff --git a/src/main/java/model/JanggiGame.java b/src/main/java/model/JanggiGame.java index f68211fdb6..04e28fc7a1 100644 --- a/src/main/java/model/JanggiGame.java +++ b/src/main/java/model/JanggiGame.java @@ -1,39 +1,86 @@ package model; import java.util.List; +import java.util.Map; import model.board.Board; -import model.board.Position; +import model.coordinate.Position; import model.piece.Piece; +import model.state.BigJangDone; +import model.state.Running; public class JanggiGame { - private final Board board; - private Team turn; + private JanggiState state; public JanggiGame(Board board) { + this(board, new Running(Team.startTurn())); + } + + public JanggiGame(Board board, JanggiState state) { this.board = board; - this.turn = Team.CHO; + this.state = state; } public void movePiece(Position current, Position next) { Piece piece = selectPiece(current); - List path = piece.extractPath(current, next); + List path = piece.pathTo(current, next); List pieces = board.extractPiecesByPath(path); - piece.validatePathCondition(pieces); - board.move(current, next); - this.turn = turn.next(); + board.move(current, next); + this.state = state.next(board); } public Piece selectPiece(Position position) { + state.validateGamePlay(); Piece piece = board.pickPiece(position); - turn.validateAlly(piece); + validateAlly(piece); return piece; } - public Team getTurn() { - return turn; + public Team resolveWinner() { + return state.resolveWinner(board); + } + + public Map calculateFinalScore() { + return JanggiReferee.collectScores(board); + } + + public void finishByBigJang() { + if (state.status() != GameStatus.BIG_JANG) { + throw new IllegalStateException("현재 빅장 상태가 아닙니다."); + } + this.state = new BigJangDone(state.turn()); + } + + public boolean canPlaying() { + return state.canPlaying(); + } + + public boolean isBigJang() { + return state.status() == GameStatus.BIG_JANG; + } + + public boolean isBigJangDone() { + return state.status() == GameStatus.BIG_JANG_DONE; + } + + private void validateAlly(Piece piece) { + if (!piece.isSameTeam(turn())) { + throw new IllegalArgumentException(turn().getName() + "의 기물이 아닙니다."); + } + } + + public Team turn() { + return state.turn(); + } + + public GameStatus status() { + return state.status(); + } + + public Map getBoard() { + return board.board(); } } diff --git a/src/main/java/model/JanggiReferee.java b/src/main/java/model/JanggiReferee.java new file mode 100644 index 0000000000..4f3df01ce1 --- /dev/null +++ b/src/main/java/model/JanggiReferee.java @@ -0,0 +1,33 @@ +package model; + +import java.util.Map; +import model.board.Board; + +public class JanggiReferee { + + private static final Team START_TURN = Team.startTurn(); + private static final Team AFTER_TURN = Team.afterTurn(); + + private static final double AFTER_TURN_BONUS_SCORE = 1.5; + + private JanggiReferee() { + } + + public static Map collectScores(Board board) { + return Map.of( + START_TURN, board.calculateBaseScore(START_TURN), + AFTER_TURN, board.calculateBaseScore(AFTER_TURN) + AFTER_TURN_BONUS_SCORE + ); + } + + public static Team judgeWinner(Board board) { + Map scores = collectScores(board); + double startTurnScore = scores.get(START_TURN); + double afterTurnScore = scores.get(AFTER_TURN); + + if (startTurnScore > afterTurnScore) { + return START_TURN; + } + return AFTER_TURN; + } +} diff --git a/src/main/java/model/JanggiState.java b/src/main/java/model/JanggiState.java new file mode 100644 index 0000000000..4ead04da83 --- /dev/null +++ b/src/main/java/model/JanggiState.java @@ -0,0 +1,61 @@ +package model; + +import java.util.List; +import model.board.Board; +import model.coordinate.Direction; +import model.coordinate.Displacement; +import model.coordinate.Position; +import model.piece.Piece; +import model.state.BigJang; +import model.state.Finished; +import model.state.Running; + +public abstract class JanggiState { + private final Team turn; + + protected JanggiState(Team turn) { + this.turn = turn; + } + + public void validateGamePlay() { + if (!canPlaying()) { + throw new IllegalStateException("게임이 종료되었으므로 장기 게임을 진행할 수 없습니다."); + } + } + + public JanggiState next(Board board) { + Team nextTurn = this.turn.opposite(); + + if (!board.isAliveGeneral(nextTurn)) { + return new Finished(this.turn); + } + if (checkBigJang(board, nextTurn)) { + return new BigJang(nextTurn); + } + return new Running(nextTurn); + } + + private boolean checkBigJang(Board board, Team nextTurn) { + Position allyGeneralPosition = board.findGeneralPositionByTeam(nextTurn); + Position enemyGeneralPosition = board.findGeneralPositionByTeam(nextTurn.opposite()); + Displacement displacement = enemyGeneralPosition.toDisplacement(allyGeneralPosition); + if (displacement.isNotStraight()) { + return false; + } + + Direction direction = displacement.extractCardinal(); + List path = direction.pathTo(allyGeneralPosition, enemyGeneralPosition); + List pieces = board.extractPiecesByPath(path); + return pieces.isEmpty(); + } + + public abstract Team resolveWinner(Board board); + + public abstract boolean canPlaying(); + + public abstract GameStatus status(); + + public Team turn() { + return turn; + } +} \ No newline at end of file diff --git a/src/main/java/model/Team.java b/src/main/java/model/Team.java index 68f566eade..4b66d75639 100644 --- a/src/main/java/model/Team.java +++ b/src/main/java/model/Team.java @@ -1,9 +1,9 @@ package model; -import model.piece.Piece; - public enum Team { - HAN("한나라"), CHO("초나라"); + + HAN("한나라"), + CHO("초나라"); private final String name; @@ -11,17 +11,19 @@ public enum Team { this.name = name; } - public void validateAlly(Piece piece) { - if (piece.isOtherTeam(this)) { - throw new IllegalArgumentException(this.name + "의 기물이 아닙니다."); - } + public static Team startTurn() { + return CHO; + } + + public static Team afterTurn() { + return startTurn().opposite(); } public boolean isHan() { return this == HAN; } - public Team next() { + public Team opposite() { if (isHan()) { return CHO; } diff --git a/src/main/java/model/board/Board.java b/src/main/java/model/board/Board.java index 8abf94644a..7e552540c2 100644 --- a/src/main/java/model/board/Board.java +++ b/src/main/java/model/board/Board.java @@ -4,7 +4,10 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import model.Team; +import model.coordinate.Position; import model.piece.Piece; +import model.piece.PieceType; public class Board { @@ -30,6 +33,12 @@ public Piece pickPiece(Position position) { .orElseThrow(() -> new IllegalArgumentException("해당 위치에 존재하는 장기말이 없습니다.")); } + public boolean isAliveGeneral(Team team) { + return board.values() + .stream() + .anyMatch(piece -> isTargetGeneral(piece, team)); + } + public void arrangePieces(Map pieces) { board.putAll(pieces); } @@ -41,15 +50,36 @@ public List extractPiecesByPath(List path) { .toList(); } - private Optional findByPosition(Position position) { - return Optional.ofNullable(board.get(position)); + public Position findGeneralPositionByTeam(Team team) { + return board.entrySet() + .stream() + .filter(data -> isTargetGeneral(data.getValue(), team)) + .map(Map.Entry::getKey) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(team.getName() + "의 왕이 없습니다.")); } - public Map board() { - return Map.copyOf(board); + public double calculateBaseScore(Team team) { + return board.values() + .stream() + .filter(piece -> piece.isSameTeam(team)) + .mapToDouble(Piece::score) + .sum(); + } + + private boolean isTargetGeneral(Piece piece, Team team) { + return piece.isSameType(PieceType.GENERAL) && piece.isSameTeam(team); + } + + private Optional findByPosition(Position position) { + return Optional.ofNullable(board.get(position)); } private boolean hasPieceAt(Position position) { return board.containsKey(position); } + + public Map board() { + return Map.copyOf(board); + } } diff --git a/src/main/java/model/board/BoardFactory.java b/src/main/java/model/board/BoardFactory.java index b89731725c..e43cb40af9 100644 --- a/src/main/java/model/board/BoardFactory.java +++ b/src/main/java/model/board/BoardFactory.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.Map; +import model.coordinate.Position; import model.piece.Cannon; import model.piece.Chariot; import model.piece.General; diff --git a/src/main/java/model/board/JanggiFormation.java b/src/main/java/model/board/FormationType.java similarity index 94% rename from src/main/java/model/board/JanggiFormation.java rename to src/main/java/model/board/FormationType.java index b3bf475b70..1949ab26b6 100644 --- a/src/main/java/model/board/JanggiFormation.java +++ b/src/main/java/model/board/FormationType.java @@ -4,11 +4,12 @@ import java.util.List; import java.util.Map; import model.Team; +import model.coordinate.Position; import model.piece.Elephant; import model.piece.Horse; import model.piece.Piece; -public enum JanggiFormation { +public enum FormationType { SANG_MA_SANG_MA(team -> List.of(new Elephant(team), new Horse(team), new Elephant(team), new Horse(team))), MA_SANG_MA_SANG(team -> List.of(new Horse(team), new Elephant(team), new Horse(team), new Elephant(team))), MA_SANG_SANG_MA(team -> List.of(new Horse(team), new Elephant(team), new Elephant(team), new Horse(team))), @@ -26,7 +27,7 @@ public enum JanggiFormation { private final FormationStrategy strategy; - JanggiFormation(FormationStrategy strategy) { + FormationType(FormationStrategy strategy) { this.strategy = strategy; } diff --git a/src/main/java/model/board/TeamFormation.java b/src/main/java/model/board/TeamFormation.java new file mode 100644 index 0000000000..6650ab3c36 --- /dev/null +++ b/src/main/java/model/board/TeamFormation.java @@ -0,0 +1,13 @@ +package model.board; + +import java.util.Map; +import model.Team; +import model.coordinate.Position; +import model.piece.Piece; + +public record TeamFormation(Team team, FormationType type) { + + public Map generate() { + return type.generateByTeam(team); + } +} \ No newline at end of file diff --git a/src/main/java/model/movement/Direction.java b/src/main/java/model/coordinate/Direction.java similarity index 72% rename from src/main/java/model/movement/Direction.java rename to src/main/java/model/coordinate/Direction.java index 524c07c3f6..10eb9419d9 100644 --- a/src/main/java/model/movement/Direction.java +++ b/src/main/java/model/coordinate/Direction.java @@ -1,7 +1,8 @@ -package model.movement; +package model.coordinate; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; -import model.board.Position; public enum Direction { EAST(0, 1), @@ -32,15 +33,17 @@ private boolean isSameDirection(int rowDiff, int colDiff) { return row == Integer.signum(rowDiff) && col == Integer.signum(colDiff); } - public Position move(Position target) { - return target.resolveNext(row, col); - } - - public int row() { - return row; + public List pathTo(Position start, Position end) { + List path = new ArrayList<>(); + Position step = move(start); + while (!step.equals(end)) { + path.add(step); + step = move(step); + } + return path; } - public int col() { - return col; + public Position move(Position target) { + return target.resolveNext(row, col); } } diff --git a/src/main/java/model/movement/Displacement.java b/src/main/java/model/coordinate/Displacement.java similarity index 63% rename from src/main/java/model/movement/Displacement.java rename to src/main/java/model/coordinate/Displacement.java index cf8d3ae4f1..8011457f4c 100644 --- a/src/main/java/model/movement/Displacement.java +++ b/src/main/java/model/coordinate/Displacement.java @@ -1,4 +1,4 @@ -package model.movement; +package model.coordinate; public record Displacement(int rowDiff, int colDiff) { @@ -13,14 +13,6 @@ public Direction extractDiagonal() { return Direction.of(rowDiff, colDiff); } - public int absRowDiff() { - return Math.abs(rowDiff); - } - - public int absColDiff() { - return Math.abs(colDiff); - } - public boolean isNotStraight() { return colDiff != 0 && rowDiff != 0; } @@ -30,11 +22,29 @@ public boolean isNotStepCombination(int longStep, int shortStep) { (absRowDiff() != shortStep || absColDiff() != longStep); } - public boolean isForwardBy(int forwardCount) { - return rowDiff == forwardCount && colDiff == 0; + public boolean isForwardBy(int forwardDirection) { + return rowDiff == forwardDirection && colDiff == 0; + } + + public boolean isSameForwardDirection(int forwardDirection) { + return rowDiff == forwardDirection; } public boolean isSideOneStep() { return rowDiff == 0 && absColDiff() == 1; } + + public boolean isOneStepInRange() { + int row = absRowDiff(); + int col = absColDiff(); + return (row <= 1 && col <= 1) && (row + col > 0); + } + + private int absRowDiff() { + return Math.abs(rowDiff); + } + + private int absColDiff() { + return Math.abs(colDiff); + } } diff --git a/src/main/java/model/coordinate/Palace.java b/src/main/java/model/coordinate/Palace.java new file mode 100644 index 0000000000..1574c94597 --- /dev/null +++ b/src/main/java/model/coordinate/Palace.java @@ -0,0 +1,48 @@ +package model.coordinate; + +import java.util.Set; +import model.Team; + +public class Palace { + + private static final Set HAN_PALACE = Set.of( + new Position(0, 3), new Position(0, 4), new Position(0, 5), + new Position(1, 3), new Position(1, 4), new Position(1, 5), + new Position(2, 3), new Position(2, 4), new Position(2, 5) + ); + + private static final Set CHO_PALACE = Set.of( + new Position(7, 3), new Position(7, 4), new Position(7, 5), + new Position(8, 3), new Position(8, 4), new Position(8, 5), + new Position(9, 3), new Position(9, 4), new Position(9, 5) + ); + + private static final Set HAN_DIAGONAL_POINTS = Set.of( + new Position(0, 3), new Position(0, 5), + new Position(1, 4), + new Position(2, 3), new Position(2, 5) + ); + + private static final Set CHO_DIAGONAL_POINTS = Set.of( + new Position(7, 3), new Position(7, 5), + new Position(8, 4), + new Position(9, 3), new Position(9, 5) + ); + + private Palace() { + } + + public static boolean contains(Team team, Position position) { + if (team.isHan()) { + return HAN_PALACE.contains(position); + } + return CHO_PALACE.contains(position); + } + + public static boolean isDiagonalPoint(Team team, Position position) { + if (team.isHan()) { + return HAN_DIAGONAL_POINTS.contains(position); + } + return CHO_DIAGONAL_POINTS.contains(position); + } +} \ No newline at end of file diff --git a/src/main/java/model/board/Position.java b/src/main/java/model/coordinate/Position.java similarity index 63% rename from src/main/java/model/board/Position.java rename to src/main/java/model/coordinate/Position.java index b2f1da040f..daf2469eb3 100644 --- a/src/main/java/model/board/Position.java +++ b/src/main/java/model/coordinate/Position.java @@ -1,10 +1,8 @@ -package model.board; +package model.coordinate; import static model.board.Board.BOARD_COL; import static model.board.Board.BOARD_ROW; -import model.movement.Displacement; - public record Position(int row, int col) { public Position { @@ -17,16 +15,8 @@ public record Position(int row, int col) { } } - public Displacement minus(Position other) { - return new Displacement(calculateRowDiff(other), calculateColDiff(other)); - } - - public int calculateRowDiff(Position other) { - return this.row() - other.row(); - } - - public int calculateColDiff(Position other) { - return this.col - other.col(); + public Displacement toDisplacement(Position other) { + return new Displacement(this.row() - other.row(), this.col - other.col()); } public Position resolveNext(int rowDistance, int colDistance) { diff --git a/src/main/java/model/movement/LinearStrategy.java b/src/main/java/model/movement/LinearStrategy.java deleted file mode 100644 index 26668460e3..0000000000 --- a/src/main/java/model/movement/LinearStrategy.java +++ /dev/null @@ -1,26 +0,0 @@ -package model.movement; - -import java.util.ArrayList; -import java.util.List; -import model.board.Position; - -public class LinearStrategy implements MoveStrategy { - - private static Direction resolveCardinal(Position start, Position end) { - Displacement displacement = end.minus(start); - return displacement.extractCardinal(); - } - - @Override - public List extractPath(Position start, Position end) { - Direction direction = resolveCardinal(start, end); - - List path = new ArrayList<>(); - Position step = direction.move(start); - while (!step.equals(end)) { - path.add(step); - step = direction.move(step); - } - return path; - } -} \ No newline at end of file diff --git a/src/main/java/model/movement/MoveStrategy.java b/src/main/java/model/movement/MoveStrategy.java deleted file mode 100644 index ea10f61c19..0000000000 --- a/src/main/java/model/movement/MoveStrategy.java +++ /dev/null @@ -1,9 +0,0 @@ -package model.movement; - -import java.util.List; -import model.board.Position; - -public interface MoveStrategy { - - List extractPath(Position start, Position end); -} diff --git a/src/main/java/model/movement/OneStepStrategy.java b/src/main/java/model/movement/OneStepStrategy.java deleted file mode 100644 index 5e2648cac3..0000000000 --- a/src/main/java/model/movement/OneStepStrategy.java +++ /dev/null @@ -1,16 +0,0 @@ -package model.movement; - -import java.util.List; -import model.board.Position; - -public class OneStepStrategy implements MoveStrategy { - - @Override - public List extractPath(Position start, Position end) { - Displacement displacement = end.minus(start); - Direction cardinal = displacement.extractCardinal(); - - Position step = cardinal.move(start); - return List.of(step); - } -} \ No newline at end of file diff --git a/src/main/java/model/movement/SteppingStrategy.java b/src/main/java/model/movement/SteppingStrategy.java deleted file mode 100644 index 6448585dff..0000000000 --- a/src/main/java/model/movement/SteppingStrategy.java +++ /dev/null @@ -1,18 +0,0 @@ -package model.movement; - -import java.util.List; -import model.board.Position; - -public class SteppingStrategy implements MoveStrategy { - - @Override - public List extractPath(Position start, Position end) { - Displacement displacement = end.minus(start); - Direction cardinal = displacement.extractCardinal(); - Direction diagonal = displacement.extractDiagonal(); - - Position oneStep = cardinal.move(start); - Position stepping = diagonal.move(oneStep); - return List.of(oneStep, stepping); - } -} \ No newline at end of file diff --git a/src/main/java/model/piece/Cannon.java b/src/main/java/model/piece/Cannon.java index 47f2b26260..fe934c18b8 100644 --- a/src/main/java/model/piece/Cannon.java +++ b/src/main/java/model/piece/Cannon.java @@ -2,10 +2,8 @@ import java.util.List; import model.Team; -import model.board.Position; -import model.movement.Displacement; -public class Cannon extends Piece { +public class Cannon extends LinearMovePiece { private static final int CANNON_HURDLE_COUNT = 1; @@ -19,7 +17,7 @@ public void validatePathCondition(List pieces) { throw new IllegalArgumentException("포는 정확히 하나의 기물을 뛰어넘어야 합니다."); } - boolean hasCannonAsHurdle = pieces.stream().anyMatch(Piece::isCannon); + boolean hasCannonAsHurdle = pieces.stream().anyMatch(piece -> piece.isSameType(this)); if (hasCannonAsHurdle) { throw new IllegalArgumentException("포는 포를 다리로 쓸 수 없습니다."); } @@ -28,16 +26,8 @@ public void validatePathCondition(List pieces) { @Override public void validateTarget(Piece otherPiece) { super.validateTarget(otherPiece); - if (otherPiece.isCannon()) { + if (otherPiece.isSameType(this)) { throw new IllegalArgumentException("포는 포를 잡을 수 없습니다."); } } - - @Override - protected void validateMove(Position current, Position next) { - Displacement displacement = next.minus(current); - if (displacement.isNotStraight()) { - throw new IllegalArgumentException("포가 이동할 수 없는 위치입니다."); - } - } } diff --git a/src/main/java/model/piece/Chariot.java b/src/main/java/model/piece/Chariot.java index 619f83ae32..a8ee1b910b 100644 --- a/src/main/java/model/piece/Chariot.java +++ b/src/main/java/model/piece/Chariot.java @@ -1,20 +1,10 @@ package model.piece; import model.Team; -import model.board.Position; -import model.movement.Displacement; -public class Chariot extends Piece { +public class Chariot extends LinearMovePiece { public Chariot(Team team) { super(team, PieceType.CHARIOT); } - - @Override - protected void validateMove(Position current, Position next) { - Displacement displacement = next.minus(current); - if (displacement.isNotStraight()) { - throw new IllegalArgumentException("차가 이동할 수 없는 위치입니다."); - } - } } diff --git a/src/main/java/model/piece/Elephant.java b/src/main/java/model/piece/Elephant.java index 15da27c1e3..db31288db2 100644 --- a/src/main/java/model/piece/Elephant.java +++ b/src/main/java/model/piece/Elephant.java @@ -1,8 +1,10 @@ package model.piece; +import java.util.List; import model.Team; -import model.board.Position; -import model.movement.Displacement; +import model.coordinate.Direction; +import model.coordinate.Displacement; +import model.coordinate.Position; public class Elephant extends Piece { @@ -15,9 +17,20 @@ public Elephant(Team team) { @Override protected void validateMove(Position current, Position next) { - Displacement displacement = next.minus(current); + Displacement displacement = next.toDisplacement(current); if (displacement.isNotStepCombination(ELEPHANT_LONG_STEP, ELEPHANT_SHORT_STEP)) { - throw new IllegalArgumentException("상이 이동할 수 없는 위치입니다."); + throw new IllegalArgumentException("현재 기물이 이동할 수 없는 위치입니다."); } } + + @Override + protected List extractPath(Position start, Position end) { + Displacement displacement = end.toDisplacement(start); + Direction cardinal = displacement.extractCardinal(); + Direction diagonal = displacement.extractDiagonal(); + + Position oneStep = cardinal.move(start); + Position stepping = diagonal.move(oneStep); + return List.of(oneStep, stepping); + } } diff --git a/src/main/java/model/piece/General.java b/src/main/java/model/piece/General.java index c0db5579e8..23c64ce504 100644 --- a/src/main/java/model/piece/General.java +++ b/src/main/java/model/piece/General.java @@ -1,16 +1,10 @@ package model.piece; import model.Team; -import model.board.Position; -public class General extends Piece { +public class General extends PalacePiece { public General(Team team) { super(team, PieceType.GENERAL); } - - @Override - protected void validateMove(Position current, Position next) { - throw new IllegalArgumentException("1단계 궁성 영역 미구현"); - } } diff --git a/src/main/java/model/piece/Guard.java b/src/main/java/model/piece/Guard.java index 9c635738f1..ec01f36d01 100644 --- a/src/main/java/model/piece/Guard.java +++ b/src/main/java/model/piece/Guard.java @@ -1,16 +1,10 @@ package model.piece; import model.Team; -import model.board.Position; -public class Guard extends Piece { +public class Guard extends PalacePiece { public Guard(Team team) { super(team, PieceType.GUARD); } - - @Override - protected void validateMove(Position current, Position next) { - throw new IllegalArgumentException("1단계 궁성 영역 미구현"); - } } diff --git a/src/main/java/model/piece/Horse.java b/src/main/java/model/piece/Horse.java index cd34d14434..653f8207bd 100644 --- a/src/main/java/model/piece/Horse.java +++ b/src/main/java/model/piece/Horse.java @@ -1,8 +1,10 @@ package model.piece; +import java.util.List; import model.Team; -import model.board.Position; -import model.movement.Displacement; +import model.coordinate.Direction; +import model.coordinate.Displacement; +import model.coordinate.Position; public class Horse extends Piece { @@ -15,9 +17,18 @@ public Horse(Team team) { @Override protected void validateMove(Position current, Position next) { - Displacement displacement = next.minus(current); + Displacement displacement = next.toDisplacement(current); if (displacement.isNotStepCombination(HORSE_LONG_STEP, HORSE_SHORT_STEP)) { - throw new IllegalArgumentException("마가 이동할 수 없는 위치입니다."); + throw new IllegalArgumentException("현재 기물이 이동할 수 없는 위치입니다."); } } + + @Override + protected List extractPath(Position start, Position end) { + Displacement displacement = end.toDisplacement(start); + Direction cardinal = displacement.extractCardinal(); + + Position step = cardinal.move(start); + return List.of(step); + } } diff --git a/src/main/java/model/piece/LinearMovePiece.java b/src/main/java/model/piece/LinearMovePiece.java new file mode 100644 index 0000000000..3f193ce097 --- /dev/null +++ b/src/main/java/model/piece/LinearMovePiece.java @@ -0,0 +1,52 @@ +package model.piece; + +import java.util.List; +import model.Team; +import model.coordinate.Direction; +import model.coordinate.Displacement; +import model.coordinate.Palace; +import model.coordinate.Position; + +public abstract class LinearMovePiece extends Piece { + protected LinearMovePiece(Team team, PieceType type) { + super(team, type); + } + + @Override + protected void validateMove(Position current, Position next) { + Displacement displacement = next.toDisplacement(current); + if (displacement.isNotStraight()) { + validatePalaceDiagonal(current, next); + } + } + + @Override + protected List extractPath(Position current, Position next) { + if (Palace.isDiagonalPoint(team(), current)) { + return extractDiagonalPath(current, next); + } + return extractLinearPath(current, next); + } + + private void validatePalaceDiagonal(Position current, Position next) { + if (isNotInPalaceDiagonal(current, next)) { + throw new IllegalArgumentException("대각선 이동은 궁성 교차점에서만 가능합니다."); + } + } + + private boolean isNotInPalaceDiagonal(Position current, Position next) { + return !Palace.isDiagonalPoint(team(), current) || !Palace.isDiagonalPoint(team(), next); + } + + private List extractLinearPath(Position start, Position end) { + Displacement displacement = end.toDisplacement(start); + Direction direction = displacement.extractCardinal(); + return direction.pathTo(start, end); + } + + private List extractDiagonalPath(Position start, Position end) { + Displacement displacement = end.toDisplacement(start); + Direction direction = displacement.extractDiagonal(); + return direction.pathTo(start, end); + } +} diff --git a/src/main/java/model/piece/PalacePiece.java b/src/main/java/model/piece/PalacePiece.java new file mode 100644 index 0000000000..f7e1b277bd --- /dev/null +++ b/src/main/java/model/piece/PalacePiece.java @@ -0,0 +1,42 @@ +package model.piece; + +import model.Team; +import model.coordinate.Displacement; +import model.coordinate.Palace; +import model.coordinate.Position; + +public abstract class PalacePiece extends Piece { + protected PalacePiece(Team team, PieceType type) { + super(team, type); + } + + @Override + protected void validateMove(Position current, Position next) { + Displacement displacement = next.toDisplacement(current); + validatePalacePosition(current, next); + validateOneStep(displacement); + validateDiagonal(current, displacement); + } + + private void validatePalacePosition(Position current, Position next) { + if (isNotInPalace(current, next)) { + throw new IllegalArgumentException("궁성 영역 내부에서만 이동할 수 있습니다."); + } + } + + private boolean isNotInPalace(Position current, Position next) { + return !Palace.contains(team(), current) || !Palace.contains(team(), next); + } + + private void validateOneStep(Displacement displacement) { + if (!displacement.isOneStepInRange()) { + throw new IllegalArgumentException("궁성 내부에서 이동할 수 없는 위치입니다."); + } + } + + private void validateDiagonal(Position current, Displacement displacement) { + if (displacement.isNotStraight() && !Palace.isDiagonalPoint(team(), current)) { + throw new IllegalArgumentException("궁성 대각선 이동은 특정 지점에서만 가능합니다."); + } + } +} \ No newline at end of file diff --git a/src/main/java/model/piece/Piece.java b/src/main/java/model/piece/Piece.java index f6cea5ff83..79714fcae8 100644 --- a/src/main/java/model/piece/Piece.java +++ b/src/main/java/model/piece/Piece.java @@ -2,10 +2,9 @@ import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; public abstract class Piece { - private final Team team; private final PieceType type; @@ -14,13 +13,20 @@ protected Piece(Team team, PieceType type) { this.type = type; } - public List extractPath(Position current, Position next) { + private static void validatePosition(Position current, Position next) { + if (current.equals(next)) { + throw new IllegalArgumentException("기물은 제자리로 이동할 수 없습니다."); + } + } + + public List pathTo(Position current, Position next) { + validatePosition(current, next); validateMove(current, next); - return type.extractPath(current, next); + return extractPath(current, next); } - public boolean isOtherTeam(Team team) { - return this.team != team; + public boolean isSameTeam(Team team) { + return team() == team; } public void validatePathCondition(List pieces) { @@ -30,26 +36,34 @@ public void validatePathCondition(List pieces) { } public void validateTarget(Piece otherPiece) { - if (getTeam() == otherPiece.team) { + if (team() == otherPiece.team()) { throw new IllegalArgumentException("아군이 있는 위치로 이동할 수 없습니다."); } } + public boolean isSameType(PieceType type) { + return type() == type; + } + + public double score() { + return type().getScore(); + } + protected abstract void validateMove(Position current, Position next); - protected boolean isCho() { - return !team.isHan(); + protected boolean isSameType(Piece piece) { + return isSameType(piece.type()); } - protected boolean isCannon() { - return getType() == PieceType.CANNON; + protected List extractPath(Position current, Position next) { + return List.of(); } - public Team getTeam() { + public Team team() { return team; } - public PieceType getType() { + public PieceType type() { return type; } } diff --git a/src/main/java/model/piece/PieceType.java b/src/main/java/model/piece/PieceType.java index c2afe70b14..4dede4f787 100644 --- a/src/main/java/model/piece/PieceType.java +++ b/src/main/java/model/piece/PieceType.java @@ -1,34 +1,21 @@ package model.piece; -import java.util.List; -import model.board.Position; -import model.movement.LinearStrategy; -import model.movement.MoveStrategy; -import model.movement.OneStepStrategy; -import model.movement.SteppingStrategy; - public enum PieceType { + GENERAL(0), + CHARIOT(13), + CANNON(7), + HORSE(5), + ELEPHANT(3), + GUARD(3), + SOLDIER(2); - CANNON(new LinearStrategy()), - CHARIOT(new LinearStrategy()), - ELEPHANT(new SteppingStrategy()), - GENERAL((start, end) -> { - throw new IllegalArgumentException("1단계 궁성 영역 미구현"); - }), - GUARD((start, end) -> { - throw new IllegalArgumentException("1단계 궁성 영역 미구현"); - }), - HORSE(new OneStepStrategy()), - SOLDIER((start, end) -> List.of()), - ; - - private final MoveStrategy moveStrategy; + private final double score; - PieceType(MoveStrategy moveStrategy) { - this.moveStrategy = moveStrategy; + PieceType(double score) { + this.score = score; } - public List extractPath(Position current, Position next) { - return moveStrategy.extractPath(current, next); + public double getScore() { + return score; } } diff --git a/src/main/java/model/piece/Soldier.java b/src/main/java/model/piece/Soldier.java index f5e21fc7c7..ffe575655a 100644 --- a/src/main/java/model/piece/Soldier.java +++ b/src/main/java/model/piece/Soldier.java @@ -1,32 +1,61 @@ package model.piece; import model.Team; -import model.board.Position; -import model.movement.Displacement; +import model.coordinate.Displacement; +import model.coordinate.Palace; +import model.coordinate.Position; public class Soldier extends Piece { - private static final int SOLDIER_FORWARD_STEP = 1; + private final int forwardDirection; public Soldier(Team team) { super(team, PieceType.SOLDIER); + this.forwardDirection = resolveForwardDirection(team); + } + + private static int resolveForwardDirection(Team team) { + if (team.isHan()) { + return 1; + } + return -1; } @Override protected void validateMove(Position current, Position next) { - Displacement displacement = next.minus(current); - int forwardCount = resolveForwardCount(); + Displacement displacement = next.toDisplacement(current); + if (displacement.isNotStraight()) { + validateDiagonalOneStep(displacement); + validateEnemyPalaceDiagonal(current, next); + return; + } + validateForwardOneStep(displacement); + } - if (!(displacement.isForwardBy(forwardCount) || displacement.isSideOneStep())) { - throw new IllegalArgumentException("졸이 이동할 수 없는 위치입니다."); + private void validateEnemyPalaceDiagonal(Position current, Position next) { + if (isNotInEnemyPalaceDiagonal(current, next)) { + throw new IllegalArgumentException("졸은 궁성 교차점에서만 대각선 이동 가능합니다."); } } - private int resolveForwardCount() { - if (isCho()) { - return -SOLDIER_FORWARD_STEP; + private boolean isNotInEnemyPalaceDiagonal(Position current, Position next) { + Team enemy = team().opposite(); + return !Palace.isDiagonalPoint(enemy, current) || !Palace.isDiagonalPoint(enemy, next); + } + + private void validateForwardOneStep(Displacement displacement) { + if (!(displacement.isForwardBy(forwardDirection) || displacement.isSideOneStep())) { + throw new IllegalArgumentException("졸은 전진 또는 좌우로만 이동할 수 있습니다."); } - return SOLDIER_FORWARD_STEP; } + private void validateDiagonalOneStep(Displacement displacement) { + if (!displacement.isOneStepInRange()) { + throw new IllegalArgumentException("졸은 한 칸만 이동할 수 있습니다."); + } + + if (!displacement.isSameForwardDirection(forwardDirection)) { + throw new IllegalArgumentException("졸은 전진 대각선만 이동 가능합니다."); + } + } } diff --git a/src/main/java/model/state/BigJang.java b/src/main/java/model/state/BigJang.java new file mode 100644 index 0000000000..b439c1556e --- /dev/null +++ b/src/main/java/model/state/BigJang.java @@ -0,0 +1,28 @@ +package model.state; + +import model.GameStatus; +import model.JanggiState; +import model.Team; +import model.board.Board; + +public class BigJang extends JanggiState { + + public BigJang(Team turn) { + super(turn); + } + + @Override + public Team resolveWinner(Board board) { + throw new IllegalStateException("빅장 합의가 필요합니다."); + } + + @Override + public boolean canPlaying() { + return true; + } + + @Override + public GameStatus status() { + return GameStatus.BIG_JANG; + } +} \ No newline at end of file diff --git a/src/main/java/model/state/BigJangDone.java b/src/main/java/model/state/BigJangDone.java new file mode 100644 index 0000000000..faddc586e6 --- /dev/null +++ b/src/main/java/model/state/BigJangDone.java @@ -0,0 +1,29 @@ +package model.state; + +import model.GameStatus; +import model.JanggiReferee; +import model.JanggiState; +import model.Team; +import model.board.Board; + +public class BigJangDone extends JanggiState { + + public BigJangDone(Team turn) { + super(turn); + } + + @Override + public Team resolveWinner(Board board) { + return JanggiReferee.judgeWinner(board); + } + + @Override + public boolean canPlaying() { + return false; + } + + @Override + public GameStatus status() { + return GameStatus.BIG_JANG_DONE; + } +} \ No newline at end of file diff --git a/src/main/java/model/state/Finished.java b/src/main/java/model/state/Finished.java new file mode 100644 index 0000000000..0c5295b9ee --- /dev/null +++ b/src/main/java/model/state/Finished.java @@ -0,0 +1,28 @@ +package model.state; + +import model.GameStatus; +import model.JanggiState; +import model.Team; +import model.board.Board; + +public class Finished extends JanggiState { + + public Finished(Team winner) { + super(winner); + } + + @Override + public Team resolveWinner(Board board) { + return turn(); + } + + @Override + public boolean canPlaying() { + return false; + } + + @Override + public GameStatus status() { + return GameStatus.DONE; + } +} \ No newline at end of file diff --git a/src/main/java/model/state/Running.java b/src/main/java/model/state/Running.java new file mode 100644 index 0000000000..4f939a711f --- /dev/null +++ b/src/main/java/model/state/Running.java @@ -0,0 +1,28 @@ +package model.state; + +import model.GameStatus; +import model.JanggiState; +import model.Team; +import model.board.Board; + +public class Running extends JanggiState { + + public Running(Team turn) { + super(turn); + } + + @Override + public Team resolveWinner(Board board) { + throw new IllegalStateException("아직 게임이 진행중입니다."); + } + + @Override + public boolean canPlaying() { + return true; + } + + @Override + public GameStatus status() { + return GameStatus.PLAYING; + } +} \ No newline at end of file diff --git a/src/main/java/repository/InMemoryJanggiRepository.java b/src/main/java/repository/InMemoryJanggiRepository.java new file mode 100644 index 0000000000..34173c2fe6 --- /dev/null +++ b/src/main/java/repository/InMemoryJanggiRepository.java @@ -0,0 +1,37 @@ +package repository; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import model.GameStatus; +import model.JanggiGame; + +public class InMemoryJanggiRepository implements JanggiRepository { + + private final Map storage = new ConcurrentHashMap<>(); + private Long id = 0L; + + @Override + public Long save(JanggiGame janggiGame) { + storage.put(++id, janggiGame); + return id; + } + + @Override + public Optional findById(Long janggiId) { + return Optional.ofNullable(storage.get(janggiId)); + } + + @Override + public void update(Long janggiId, JanggiGame janggiGame) { + storage.put(janggiId, janggiGame); + } + + @Override + public Map collectGameStatus() { + return storage.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().status())); + } +} diff --git a/src/main/java/repository/JanggiRepository.java b/src/main/java/repository/JanggiRepository.java new file mode 100644 index 0000000000..73ca32f057 --- /dev/null +++ b/src/main/java/repository/JanggiRepository.java @@ -0,0 +1,17 @@ +package repository; + +import java.util.Map; +import java.util.Optional; +import model.GameStatus; +import model.JanggiGame; + +public interface JanggiRepository { + + Long save(JanggiGame janggiGame); + + Optional findById(Long janggiId); + + void update(Long janggiId, JanggiGame janggiGame); + + Map collectGameStatus(); +} diff --git a/src/main/java/repository/JdbcJanggiRepository.java b/src/main/java/repository/JdbcJanggiRepository.java new file mode 100644 index 0000000000..675e5a858b --- /dev/null +++ b/src/main/java/repository/JdbcJanggiRepository.java @@ -0,0 +1,92 @@ +package repository; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.sql.DataSource; +import model.GameStatus; +import model.JanggiGame; +import model.JanggiState; +import model.board.Board; +import model.coordinate.Position; +import model.piece.Piece; +import repository.mapper.JanggiMapper; + +public class JdbcJanggiRepository extends JdbcTemplate implements JanggiRepository { + + public JdbcJanggiRepository(DataSource dataSource) { + super(dataSource); + } + + @Override + public Long save(JanggiGame janggiGame) { + String sql = "INSERT INTO janggi_game (status, turn) VALUES (?, ?)"; + Long gameId = executeInsert(sql, janggiGame.status().name(), janggiGame.turn().name()); + + savePieces(gameId, janggiGame.getBoard()); + return gameId; + } + + @Override + public Optional findById(Long janggiId) { + String sql = "SELECT * FROM janggi_game WHERE id = ?"; + return executeQuery(sql, rs -> { + if (!rs.next()) { + return Optional.empty(); + } + + Board board = fetchBoard(janggiId); + JanggiState state = JanggiMapper.createState(rs.getString("status"), rs.getString("turn")); + return Optional.of(new JanggiGame(board, state)); + }, janggiId); + } + + @Override + public void update(Long janggiId, JanggiGame janggiGame) { + String sql = "UPDATE janggi_game SET status = ?, turn = ? WHERE id = ?"; + executeUpdate(sql, janggiGame.status().name(), janggiGame.turn().name(), janggiId); + + executeUpdate("DELETE FROM piece WHERE game_id = ?", janggiId); + savePieces(janggiId, janggiGame.getBoard()); + } + + @Override + public Map collectGameStatus() { + String sql = "SELECT id, status FROM janggi_game"; + return executeQuery(sql, rs -> { + Map statusMap = new HashMap<>(); + while (rs.next()) { + statusMap.put(rs.getLong("id"), GameStatus.valueOf(rs.getString("status"))); + } + return statusMap; + }); + } + + private void savePieces(Long gameId, Map board) { + String sql = "INSERT INTO piece (game_id, `row`, col, `type`, team) VALUES (?, ?, ?, ?, ?)"; + + List parameters = board.entrySet().stream() + .map(entry -> { + Position pos = entry.getKey(); + Piece piece = entry.getValue(); + return new Object[]{gameId, pos.row(), pos.col(), piece.type().name(), piece.team().name()}; + }) + .toList(); + + executeBatch(sql, parameters); + } + + private Board fetchBoard(Long gameId) { + String sql = "SELECT * FROM piece WHERE game_id = ?"; + return executeQuery(sql, rs -> { + Map pieces = new HashMap<>(); + while (rs.next()) { + Piece piece = JanggiMapper.createPiece(rs.getString("type"), rs.getString("team")); + Position position = new Position(rs.getInt("row"), rs.getInt("col")); + pieces.put(position, piece); + } + return new Board(pieces); + }, gameId); + } +} \ No newline at end of file diff --git a/src/main/java/repository/JdbcTemplate.java b/src/main/java/repository/JdbcTemplate.java new file mode 100644 index 0000000000..2b1354dd57 --- /dev/null +++ b/src/main/java/repository/JdbcTemplate.java @@ -0,0 +1,100 @@ +package repository; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import javax.sql.DataSource; +import repository.db.ConnectionManager; + +public abstract class JdbcTemplate { + + private final DataSource dataSource; + + protected JdbcTemplate(DataSource dataSource) { + this.dataSource = dataSource; + } + + public Long executeInsert(String sql, Object... parameters) { + return execute(conn -> { + try (PreparedStatement statement = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + setParameters(statement, parameters); + statement.executeUpdate(); + return extractGeneratedKey(statement); + } + }); + } + + public void executeUpdate(String sql, Object... parameters) { + execute(conn -> { + try (PreparedStatement statement = conn.prepareStatement(sql)) { + setParameters(statement, parameters); + statement.executeUpdate(); + return null; + } + }); + } + + public T executeQuery(String sql, RowMapper mapper, Object... parameters) { + return execute(conn -> { + try (PreparedStatement statement = conn.prepareStatement(sql)) { + setParameters(statement, parameters); + try (ResultSet rs = statement.executeQuery()) { + return mapper.map(rs); + } + } + }); + } + + public void executeBatch(String sql, List parameters) { + execute(conn -> { + try (PreparedStatement statement = conn.prepareStatement(sql)) { + for (Object[] params : parameters) { + setParameters(statement, params); + statement.addBatch(); + } + statement.executeBatch(); + return null; + } + }); + } + + private T execute(PreparedStatementExecutor executor) { + Connection conn = null; + try { + conn = ConnectionManager.getConnection(dataSource); + return executor.execute(conn); + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + ConnectionManager.releaseConnection(conn); + } + } + + private long extractGeneratedKey(PreparedStatement statement) throws SQLException { + try (ResultSet rs = statement.getGeneratedKeys()) { + if (rs.next()) { + return rs.getLong(1); + } + throw new SQLException("[ERROR] ID를 생성할 수 없습니다."); + } + } + + private void setParameters(PreparedStatement statement, Object... parameters) throws SQLException { + for (int i = 0; i < parameters.length; i++) { + statement.setObject(i + 1, parameters[i]); + } + } + + @FunctionalInterface + public interface RowMapper { + T map(ResultSet rs) throws SQLException; + } + + @FunctionalInterface + private interface PreparedStatementExecutor { + T execute(Connection conn) throws SQLException; + } +} \ No newline at end of file diff --git a/src/main/java/repository/db/ConnectionManager.java b/src/main/java/repository/db/ConnectionManager.java new file mode 100644 index 0000000000..9541f41d1f --- /dev/null +++ b/src/main/java/repository/db/ConnectionManager.java @@ -0,0 +1,52 @@ +package repository.db; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Optional; +import javax.sql.DataSource; + +public class ConnectionManager { + + private static final ThreadLocal THREAD_LOCAL_CONNECTION = new ThreadLocal<>(); + + private ConnectionManager() { + } + + public static void set(Connection connection) { + THREAD_LOCAL_CONNECTION.set(connection); + } + + public static Optional get() { + return Optional.ofNullable(THREAD_LOCAL_CONNECTION.get()); + } + + public static boolean isPresent() { + return get().isPresent(); + } + + public static void remove() { + THREAD_LOCAL_CONNECTION.remove(); + } + + public static Connection getConnection(DataSource dataSource) { + return get().orElseGet(() -> { + try { + return dataSource.getConnection(); + } catch (SQLException e) { + throw new RuntimeException("[ERROR] 커넥션 획득 실패", e); + } + }); + } + + public static void releaseConnection(Connection conn) { + if (isPresent() || conn == null) { + return; + } + + try { + conn.close(); + } catch (SQLException e) { + throw new RuntimeException("[ERROR] 커넥션 반납 실패", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/repository/db/DataSourceManager.java b/src/main/java/repository/db/DataSourceManager.java new file mode 100644 index 0000000000..e66596d003 --- /dev/null +++ b/src/main/java/repository/db/DataSourceManager.java @@ -0,0 +1,22 @@ +package repository.db; + +import com.mysql.cj.jdbc.MysqlDataSource; +import javax.sql.DataSource; + +public class DataSourceManager { + + private static final String URL = "jdbc:mysql://localhost:3309/janggi_db?serverTimezone=UTC&useSSL=false&rewriteBatchedStatements=true"; + private static final String USER = "janggi"; + private static final String PASSWORD = "janggi1234"; + + private DataSourceManager() { + } + + public static DataSource getDataSource() { + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setURL(URL); + dataSource.setUser(USER); + dataSource.setPassword(PASSWORD); + return dataSource; + } +} diff --git a/src/main/java/repository/db/TransactionTemplate.java b/src/main/java/repository/db/TransactionTemplate.java new file mode 100644 index 0000000000..bb3c7666b3 --- /dev/null +++ b/src/main/java/repository/db/TransactionTemplate.java @@ -0,0 +1,57 @@ +package repository.db; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; + +public class TransactionTemplate { + private final DataSource dataSource; + + public TransactionTemplate(DataSource dataSource) { + this.dataSource = dataSource; + } + + public T execute(TransactionCallback callback) { + try (Connection conn = dataSource.getConnection()) { + conn.setAutoCommit(false); + ConnectionManager.set(conn); + + try { + T result = callback.execute(); + conn.commit(); + return result; + } catch (Exception e) { + conn.rollback(); + throw convertToRuntimeException(e); + } finally { + ConnectionManager.remove(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public void execute(TransactionRunnable runnable) { + execute(() -> { + runnable.execute(); + return null; + }); + } + + private RuntimeException convertToRuntimeException(Exception e) { + if (e instanceof RuntimeException) { + return (RuntimeException) e; + } + return new RuntimeException(e); + } + + @FunctionalInterface + public interface TransactionCallback { + T execute() throws Exception; + } + + @FunctionalInterface + public interface TransactionRunnable { + void execute() throws Exception; + } +} \ No newline at end of file diff --git a/src/main/java/repository/mapper/JanggiMapper.java b/src/main/java/repository/mapper/JanggiMapper.java new file mode 100644 index 0000000000..a1719befbf --- /dev/null +++ b/src/main/java/repository/mapper/JanggiMapper.java @@ -0,0 +1,47 @@ +package repository.mapper; + +import model.GameStatus; +import model.JanggiState; +import model.Team; +import model.piece.Cannon; +import model.piece.Chariot; +import model.piece.Elephant; +import model.piece.General; +import model.piece.Guard; +import model.piece.Horse; +import model.piece.Piece; +import model.piece.PieceType; +import model.piece.Soldier; +import model.state.BigJang; +import model.state.BigJangDone; +import model.state.Finished; +import model.state.Running; + +public class JanggiMapper { + + private JanggiMapper() { + } + + public static Piece createPiece(String type, String teamName) { + Team team = Team.valueOf(teamName); + return switch (PieceType.valueOf(type)) { + case CHARIOT -> new Chariot(team); + case CANNON -> new Cannon(team); + case HORSE -> new Horse(team); + case ELEPHANT -> new Elephant(team); + case SOLDIER -> new Soldier(team); + case GUARD -> new Guard(team); + case GENERAL -> new General(team); + }; + } + + public static JanggiState createState(String statusName, String turnName) { + Team turn = Team.valueOf(turnName); + return switch (GameStatus.valueOf(statusName)) { + case GameStatus.PLAYING -> new Running(turn); + case GameStatus.BIG_JANG -> new BigJang(turn); + case GameStatus.DONE -> new Finished(turn); + case GameStatus.BIG_JANG_DONE -> new BigJangDone(turn); + }; + } +} diff --git a/src/main/java/ui/ContinueController.java b/src/main/java/ui/ContinueController.java new file mode 100644 index 0000000000..1abc8127d5 --- /dev/null +++ b/src/main/java/ui/ContinueController.java @@ -0,0 +1,38 @@ +package ui; + +import static ui.Retrier.retry; + +import application.JanggiQueryService; +import application.JanggiService; +import java.util.Map; +import model.GameStatus; +import ui.view.InputView; +import ui.view.OutputView; + +public class ContinueController extends JanggiController { + + protected ContinueController(JanggiService janggiService, JanggiQueryService janggiQueryService) { + super(janggiService, janggiQueryService); + } + + @Override + public void run() { + Map gameStatusById = janggiQueryService.collectGameStatus(); + if (gameStatusById.isEmpty()) { + OutputView.displayNoGame(); + return; + } + + process(gameStatusById); + } + + private void process(Map gameStatusById) { + Long gameId = retry(() -> InputView.readPlayGameId(gameStatusById), OutputView::displayError); + + try { + playGame(gameId); + } catch (IllegalArgumentException e) { + OutputView.displayError(e.getMessage()); + } + } +} diff --git a/src/main/java/ui/FrontController.java b/src/main/java/ui/FrontController.java new file mode 100644 index 0000000000..f186f9636e --- /dev/null +++ b/src/main/java/ui/FrontController.java @@ -0,0 +1,31 @@ +package ui; + +import static ui.Retrier.retry; + +import application.JanggiQueryService; +import application.JanggiService; +import java.util.Map; +import java.util.Optional; +import ui.view.InputView; +import ui.view.OutputView; + +public class FrontController { + + private final Map controllers; + + public FrontController(JanggiService janggiService, JanggiQueryService janggiQueryService) { + controllers = Map.of( + GameMenu.CONTINUE, new ContinueController(janggiService, janggiQueryService), + GameMenu.NEW_GAME, new NewGameController(janggiService, janggiQueryService) + ); + } + + public void run() { + GameMenu gameMenu; + do { + gameMenu = retry(InputView::readGameMenu, OutputView::displayError); + Optional.ofNullable(controllers.get(gameMenu)) + .ifPresent(JanggiController::run); + } while (gameMenu != GameMenu.END); + } +} diff --git a/src/main/java/ui/GameMenu.java b/src/main/java/ui/GameMenu.java new file mode 100644 index 0000000000..ecd25e9dbf --- /dev/null +++ b/src/main/java/ui/GameMenu.java @@ -0,0 +1,33 @@ +package ui; + +import java.util.stream.Stream; + +public enum GameMenu { + + CONTINUE(1, "게임 이어서 진행하기"), + NEW_GAME(2, "새로 시작하기"), + END(3, "종료하기"); + + private final int value; + private final String description; + + GameMenu(int value, String description) { + this.value = value; + this.description = description; + } + + public static GameMenu select(int number) { + return Stream.of(values()) + .filter(v -> v.value == number) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("잘못된 입력입니다.")); + } + + public int getValue() { + return value; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/ui/InputCommand.java b/src/main/java/ui/InputCommand.java new file mode 100644 index 0000000000..1c025932b6 --- /dev/null +++ b/src/main/java/ui/InputCommand.java @@ -0,0 +1,13 @@ +package ui; + +public enum InputCommand { + Y, N; + + public static InputCommand parse(String input) { + try { + return InputCommand.valueOf(input.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("잘못된 입력입니다."); + } + } +} diff --git a/src/main/java/ui/JanggiController.java b/src/main/java/ui/JanggiController.java new file mode 100644 index 0000000000..673ee91e92 --- /dev/null +++ b/src/main/java/ui/JanggiController.java @@ -0,0 +1,74 @@ +package ui; + +import static ui.Retrier.retry; + +import application.JanggiQueryService; +import application.JanggiResultDto; +import application.JanggiService; +import application.MoveCommand; +import model.Team; +import model.coordinate.Position; +import model.piece.Piece; +import ui.view.InputView; +import ui.view.OutputView; + +public abstract class JanggiController { + + protected final JanggiService janggiService; + protected final JanggiQueryService janggiQueryService; + + protected JanggiController(JanggiService janggiService, JanggiQueryService janggiQueryService) { + this.janggiService = janggiService; + this.janggiQueryService = janggiQueryService; + } + + public abstract void run(); + + protected void playGame(Long janggiId) { + OutputView.displayBoard(janggiQueryService.getBoardResponse(janggiId)); + + while (janggiQueryService.canPlaying(janggiId)) { + retry(() -> checkBigJang(janggiId), OutputView::displayError); + play(janggiId); + } + + printResult(janggiId); + } + + private void play(Long janggiId) { + if (janggiQueryService.canPlaying(janggiId)) { + retry(() -> playByTurn(janggiId), OutputView::displayError); + } + OutputView.displayBoard(janggiQueryService.getBoardResponse(janggiId)); + } + + private void playByTurn(Long janggiId) { + Team currentTurn = janggiQueryService.getTurn(janggiId); + + Position current = InputView.readPiecePositionForMove(currentTurn); + Piece piece = janggiQueryService.selectPiece(janggiId, current); + + Position next = InputView.readPiecePositionForArrange(currentTurn, piece); + janggiService.movePiece(janggiId, new MoveCommand(current, next)); + } + + private void checkBigJang(Long janggiId) { + if (!janggiQueryService.isBigJang(janggiId)) { + return; + } + + boolean bigJang = InputView.readBigJangStatus(janggiQueryService.getTurn(janggiId)); + if (bigJang) { + janggiService.finishByBigJang(janggiId); + } + } + + private void printResult(Long janggiId) { + JanggiResultDto gameResult = janggiQueryService.getGameResult(janggiId); + OutputView.displayWinner(gameResult.winner()); + + if (gameResult.bigJangDone()) { + OutputView.displayScore(gameResult.finalScore()); + } + } +} \ No newline at end of file diff --git a/src/main/java/ui/NewGameController.java b/src/main/java/ui/NewGameController.java new file mode 100644 index 0000000000..91ffa7a3e0 --- /dev/null +++ b/src/main/java/ui/NewGameController.java @@ -0,0 +1,26 @@ +package ui; + +import static model.Team.CHO; +import static model.Team.HAN; +import static ui.Retrier.retry; + +import application.JanggiQueryService; +import application.JanggiService; +import model.board.TeamFormation; +import ui.view.InputView; +import ui.view.OutputView; + +public class NewGameController extends JanggiController { + + protected NewGameController(JanggiService janggiService, JanggiQueryService janggiQueryService) { + super(janggiService, janggiQueryService); + } + + @Override + public void run() { + TeamFormation hanFormation = retry(() -> InputView.readFormationByTeam(HAN), OutputView::displayError); + TeamFormation choFormation = retry(() -> InputView.readFormationByTeam(CHO), OutputView::displayError); + Long janggiId = janggiService.createJanggiGame(hanFormation, choFormation); + playGame(janggiId); + } +} diff --git a/src/main/java/controller/Retrier.java b/src/main/java/ui/Retrier.java similarity index 62% rename from src/main/java/controller/Retrier.java rename to src/main/java/ui/Retrier.java index 76e3f8e2c5..8493499631 100644 --- a/src/main/java/controller/Retrier.java +++ b/src/main/java/ui/Retrier.java @@ -1,4 +1,4 @@ -package controller; +package ui; import java.util.function.Consumer; import java.util.function.Supplier; @@ -7,24 +7,24 @@ public final class Retrier { private Retrier() { } - public static T retry(Supplier task, Consumer consumer) { + public static T retry(Supplier task, Consumer consumer) { while (true) { try { return task.get(); } catch (IllegalArgumentException e) { - consumer.accept(e); + consumer.accept(e.getMessage()); } } } - public static void retry(Runnable task, Consumer consumer) { + public static void retry(Runnable task, Consumer consumer) { while (true) { try { task.run(); return; } catch (IllegalArgumentException e) { - consumer.accept(e); + consumer.accept(e.getMessage()); } } } diff --git a/src/main/java/view/formater/BoardFormatter.java b/src/main/java/ui/formater/BoardFormatter.java similarity index 85% rename from src/main/java/view/formater/BoardFormatter.java rename to src/main/java/ui/formater/BoardFormatter.java index a01a712350..555ab569f1 100644 --- a/src/main/java/view/formater/BoardFormatter.java +++ b/src/main/java/ui/formater/BoardFormatter.java @@ -1,6 +1,6 @@ -package view.formater; +package ui.formater; -import static view.mapper.ViewMapper.SYMBOL_MAP; +import static ui.mapper.ViewMapper.SYMBOL_MAP; import model.Team; import model.piece.Piece; @@ -32,8 +32,8 @@ public static String formatSymbol(Piece piece) { if (piece == null) { return EMPTY; } - String color = extractColor(piece.getTeam()); - String symbol = SYMBOL_MAP.get(piece.getType()).get(piece.getTeam()); + String color = extractColor(piece.team()); + String symbol = SYMBOL_MAP.get(piece.type()).get(piece.team()); return color + symbol + RESET; } diff --git a/src/main/java/view/mapper/ViewMapper.java b/src/main/java/ui/mapper/ViewMapper.java similarity index 64% rename from src/main/java/view/mapper/ViewMapper.java rename to src/main/java/ui/mapper/ViewMapper.java index 04b5ebc991..d772ab633f 100644 --- a/src/main/java/view/mapper/ViewMapper.java +++ b/src/main/java/ui/mapper/ViewMapper.java @@ -1,29 +1,37 @@ -package view.mapper; +package ui.mapper; -import static model.board.JanggiFormation.MA_SANG_MA_SANG; -import static model.board.JanggiFormation.MA_SANG_SANG_MA; -import static model.board.JanggiFormation.SANG_MA_MA_SANG; -import static model.board.JanggiFormation.SANG_MA_SANG_MA; +import static model.board.FormationType.MA_SANG_MA_SANG; +import static model.board.FormationType.MA_SANG_SANG_MA; +import static model.board.FormationType.SANG_MA_MA_SANG; +import static model.board.FormationType.SANG_MA_SANG_MA; import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.Map; +import model.GameStatus; import model.Team; -import model.board.JanggiFormation; +import model.board.FormationType; import model.piece.PieceType; public class ViewMapper { public static final Map> SYMBOL_MAP = new EnumMap<>(PieceType.class); - public static Map FORMATION_ORDER_MAPPER = new LinkedHashMap<>(); + public static Map FORMATION_ORDER_MAPPER = new LinkedHashMap<>(); - public static Map FORMATION_DISPLAY_MAPPER = Map.of( + public static Map FORMATION_DISPLAY_MAPPER = Map.of( SANG_MA_SANG_MA, "상마상마", MA_SANG_MA_SANG, "마상마상", MA_SANG_SANG_MA, "마상상마", SANG_MA_MA_SANG, "상마마상" ); + public static Map GAME_STATUS_DISPLAY_MAPPER = Map.of( + GameStatus.PLAYING, "진행", + GameStatus.BIG_JANG, "진행", + GameStatus.BIG_JANG_DONE, "종료", + GameStatus.DONE, "종료" + ); + static { SYMBOL_MAP.put(PieceType.CHARIOT, Map.of(Team.HAN, "車", Team.CHO, "車")); SYMBOL_MAP.put(PieceType.CANNON, Map.of(Team.HAN, "包", Team.CHO, "包")); diff --git a/src/main/java/view/parser/InputParser.java b/src/main/java/ui/parser/InputParser.java similarity index 50% rename from src/main/java/view/parser/InputParser.java rename to src/main/java/ui/parser/InputParser.java index 8eeb8874f1..697ba99610 100644 --- a/src/main/java/view/parser/InputParser.java +++ b/src/main/java/ui/parser/InputParser.java @@ -1,11 +1,14 @@ -package view.parser; +package ui.parser; import java.util.Arrays; import java.util.List; public class InputParser { - public int parseNumber(String input) { + private InputParser() { + } + + public static int parseNumber(String input) { try { return Integer.parseInt(input.strip()); } catch (NumberFormatException e) { @@ -13,7 +16,15 @@ public int parseNumber(String input) { } } - public List parseToken(String input, String delimiter) { + public static long parseLong(String input) { + try { + return Long.parseLong(input.strip()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("잘못된 입력입니다. 수를 입력해주세요: " + input); + } + } + + public static List parseToken(String input, String delimiter) { return Arrays.stream(input.strip() .split(delimiter)) .map(String::strip) diff --git a/src/main/java/ui/view/InputView.java b/src/main/java/ui/view/InputView.java new file mode 100644 index 0000000000..15e4e2b011 --- /dev/null +++ b/src/main/java/ui/view/InputView.java @@ -0,0 +1,98 @@ +package ui.view; + +import static ui.formater.BoardFormatter.formatSymbol; +import static ui.mapper.ViewMapper.FORMATION_DISPLAY_MAPPER; +import static ui.mapper.ViewMapper.FORMATION_ORDER_MAPPER; +import static ui.mapper.ViewMapper.GAME_STATUS_DISPLAY_MAPPER; +import static ui.parser.InputParser.parseLong; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Scanner; +import model.GameStatus; +import model.Team; +import model.board.FormationType; +import model.board.TeamFormation; +import model.coordinate.Position; +import model.piece.Piece; +import ui.GameMenu; +import ui.InputCommand; +import ui.parser.InputParser; + +public class InputView { + + private static final Scanner SCANNER = new Scanner(System.in); + + public static TeamFormation readFormationByTeam(Team team) { + int order = readFormationOrder(team); + FormationType formationType = Optional.ofNullable(FORMATION_ORDER_MAPPER.get(order)) + .orElseThrow(() -> new IllegalArgumentException("올바른 상차림을 선택해주세요.")); + + return new TeamFormation(team, formationType); + } + + public static Position readPiecePositionForMove(Team turn) { + System.out.println(); + System.out.printf("[%s] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리)%n", turn.getName()); + System.out.print("기물: "); + return extractPosition(); + } + + public static Position readPiecePositionForArrange(Team turn, Piece piece) { + System.out.println(); + System.out.printf("[%s] 기물 %s의 다음 위치를 선택해주세요. (쉼표 기준으로 분리)%n", turn.getName(), formatSymbol(piece)); + System.out.print("기물: "); + return extractPosition(); + } + + public static boolean readBigJangStatus(Team turn) { + System.out.printf("%n[%s] 빅장입니다! 종료하시겠습니까? (Y, N)%n", turn.getName()); + String input = SCANNER.nextLine(); + InputCommand command = InputCommand.parse(input); + return command == InputCommand.Y; + } + + private static Position extractPosition() { + List tokens = InputParser.parseToken(SCANNER.nextLine(), ","); + int row = InputParser.parseNumber(tokens.get(0)); + int col = InputParser.parseNumber(tokens.get(1)); + return new Position(row, col); + } + + private static int readFormationOrder(Team team) { + System.out.printf("%n%s의 상차림을 선택해주세요.%n", team.getName()); + FORMATION_ORDER_MAPPER.forEach((order, formation) -> + System.out.printf("%d. %s%n", order, FORMATION_DISPLAY_MAPPER.get(formation))); + + String input = SCANNER.nextLine(); + return InputParser.parseNumber(input); + } + + public static GameMenu readGameMenu() { + System.out.println(); + System.out.println("> 장기 게임 메뉴"); + System.out.println(); + Arrays.stream(GameMenu.values()) + .forEach(gameMenu -> System.out.printf("%d. %s%n", gameMenu.getValue(), gameMenu.getDescription())); + + System.out.println(); + System.out.println("메뉴를 선택해주세요:"); + return GameMenu.select(InputParser.parseNumber(SCANNER.nextLine())); + } + + public static Long readPlayGameId(Map gameStatusById) { + System.out.println(); + System.out.println("> 이어서 진행할 게임을 선택해주세요."); + gameStatusById.entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(entry -> System.out.printf("%d번 게임: %s 상태%n", + entry.getKey(), GAME_STATUS_DISPLAY_MAPPER.get(entry.getValue()))); + + System.out.println(); + System.out.println("번호를 입력해주세요:"); + return parseLong(SCANNER.nextLine()); + } +} diff --git a/src/main/java/ui/view/OutputView.java b/src/main/java/ui/view/OutputView.java new file mode 100644 index 0000000000..4565f15166 --- /dev/null +++ b/src/main/java/ui/view/OutputView.java @@ -0,0 +1,68 @@ +package ui.view; + +import static ui.formater.BoardFormatter.COL_NUM; +import static ui.formater.BoardFormatter.RED; +import static ui.formater.BoardFormatter.RESET; +import static ui.formater.BoardFormatter.ROW_NUM; +import static ui.formater.BoardFormatter.SPACE; +import static ui.formater.BoardFormatter.VERTICAL_LINE; +import static ui.formater.BoardFormatter.formatHorizon; +import static ui.formater.BoardFormatter.formatSymbol; + +import java.text.DecimalFormat; +import java.util.Map; +import model.Team; +import model.board.Board; +import model.coordinate.Position; +import model.piece.Piece; + +public class OutputView { + + public static void displayBoard(Map board) { + displayColIndex(); + String border = formatHorizon(Board.BOARD_COL); + System.out.println(border); + + displayPositionByPiece(board); + + System.out.println(border); + } + + public static void displayError(String message) { + System.out.println(RED + "[ERROR] " + message + RESET); + } + + public static void displayWinner(Team winner) { + System.out.println(winner.getName() + " 승"); + } + + public static void displayScore(Map finalScore) { + DecimalFormat formatter = new DecimalFormat("#.#"); + finalScore.forEach((team, score) -> System.out.printf("%s: %s점%n", team.getName(), formatter.format(score))); + } + + private static void displayPositionByPiece(Map board) { + for (int row = 0; row < Board.BOARD_ROW; row++) { + System.out.print(ROW_NUM[row] + " " + VERTICAL_LINE); + for (int col = 0; col < Board.BOARD_COL; col++) { + Piece piece = board.get(new Position(row, col)); + System.out.print(SPACE + formatSymbol(piece)); + } + System.out.println(SPACE + VERTICAL_LINE); + } + } + + private static void displayColIndex() { + System.out.println(); + System.out.print(SPACE + SPACE + SPACE); + for (String column : COL_NUM) { + System.out.print(SPACE + column); + } + System.out.println(); + } + + public static void displayNoGame() { + System.out.println(); + System.out.println("진행한 게임이 없습니다."); + } +} \ No newline at end of file diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java deleted file mode 100644 index 4a746f5c24..0000000000 --- a/src/main/java/view/InputView.java +++ /dev/null @@ -1,52 +0,0 @@ -package view; - -import static view.formater.BoardFormatter.formatSymbol; -import static view.mapper.ViewMapper.FORMATION_DISPLAY_MAPPER; -import static view.mapper.ViewMapper.FORMATION_ORDER_MAPPER; - -import java.util.List; -import java.util.Optional; -import java.util.Scanner; -import model.Team; -import model.board.JanggiFormation; -import model.board.Position; -import model.piece.Piece; -import view.parser.InputParser; - -public class InputView { - private static final Scanner SCANNER = new Scanner(System.in); - private static final InputParser PARSER = new InputParser(); - - public JanggiFormation readFormationByTeam(Team team) { - System.out.printf("%n%s의 상차림을 선택해주세요.%n", team.getName()); - FORMATION_ORDER_MAPPER.forEach((order, formation) -> - System.out.printf("%d. %s%n", order, FORMATION_DISPLAY_MAPPER.get(formation))); - - String input = SCANNER.nextLine(); - int order = PARSER.parseNumber(input); - - return Optional.ofNullable(FORMATION_ORDER_MAPPER.get(order)) - .orElseThrow(() -> new IllegalArgumentException("올바른 상차림을 선택해주세요.")); - } - - public Position readPiecePositionForMove(Team turn) { - System.out.println(); - System.out.printf("[%s] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리)%n", turn.getName()); - System.out.print("기물: "); - return extractPosition(); - } - - private Position extractPosition() { - List tokens = PARSER.parseToken(SCANNER.nextLine(), ","); - int row = PARSER.parseNumber(tokens.get(0)); - int col = PARSER.parseNumber(tokens.get(1)); - return new Position(row, col); - } - - public Position readPiecePositionForArrange(Team turn, Piece piece) { - System.out.println(); - System.out.printf("[%s] 기물 %s의 다음 위치를 선택해주세요. (쉼표 기준으로 분리)%n", turn.getName(), formatSymbol(piece)); - System.out.print("기물: "); - return extractPosition(); - } -} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java deleted file mode 100644 index dbbe2be93a..0000000000 --- a/src/main/java/view/OutputView.java +++ /dev/null @@ -1,52 +0,0 @@ -package view; - -import static view.formater.BoardFormatter.COL_NUM; -import static view.formater.BoardFormatter.RED; -import static view.formater.BoardFormatter.RESET; -import static view.formater.BoardFormatter.ROW_NUM; -import static view.formater.BoardFormatter.SPACE; -import static view.formater.BoardFormatter.VERTICAL_LINE; -import static view.formater.BoardFormatter.formatHorizon; -import static view.formater.BoardFormatter.formatSymbol; - -import java.util.Map; -import model.board.Board; -import model.board.Position; -import model.piece.Piece; - -public class OutputView { - - private static void displayPositionByPiece(Map board) { - for (int row = 0; row < Board.BOARD_ROW; row++) { - System.out.print(ROW_NUM[row] + " " + VERTICAL_LINE); - for (int col = 0; col < Board.BOARD_COL; col++) { - Piece piece = board.get(new Position(row, col)); - System.out.print(SPACE + formatSymbol(piece)); - } - System.out.println(SPACE + VERTICAL_LINE); - } - } - - private static void displayColIndex() { - System.out.println(); - System.out.print(SPACE + SPACE + SPACE); - for (String column : COL_NUM) { - System.out.print(SPACE + column); - } - System.out.println(); - } - - public void displayBoard(Map board) { - displayColIndex(); - String border = formatHorizon(Board.BOARD_COL); - System.out.println(border); - - displayPositionByPiece(board); - - System.out.println(border); - } - - public void displayError(String message) { - System.out.println(RED + "[ERROR] " + message + RESET); - } -} \ No newline at end of file diff --git a/src/test/java/model/JanggiGameTest.java b/src/test/java/model/JanggiGameTest.java index 3dab3216e1..58380ef2a3 100644 --- a/src/test/java/model/JanggiGameTest.java +++ b/src/test/java/model/JanggiGameTest.java @@ -2,44 +2,57 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; -import model.board.Position; -import model.piece.Horse; +import java.util.Map; +import model.coordinate.Position; +import model.piece.Chariot; import model.piece.Piece; -import model.testdouble.FakePiece; import model.testdouble.SpyBoard; import org.junit.jupiter.api.Test; class JanggiGameTest { @Test - void 장기말을_정상적으로_옮기면_보드에서_이동하고_다음_차례가_된다() { - // given: (1,1)에 CHO의 이동 가능한 기물이 있고, 경로가 비어있는 상황 시뮬레이션 - Position source = new Position(1, 1); - Position destination = new Position(2, 2); - FakePiece piece = FakePiece.createFake(Team.CHO); + void 장기_게임을_시작하면_초나라부터_시작하고_이어서_게임을_진행할_수_있다() { + // given: 왕만 배치 + SpyBoard board = SpyBoard.withBothGenerals(); - SpyBoard board = new SpyBoard(piece); + // when JanggiGame janggiGame = new JanggiGame(board); - Team prevTurn = janggiGame.getTurn(); - // when - janggiGame.movePiece(source, destination); + // then + assertThat(janggiGame) + .extracting(JanggiGame::turn, JanggiGame::canPlaying) + .containsExactly(Team.CHO, true); + } + + @Test + void 장기게임_시작_후_정상적으로_기물을_옮기면_다음_차례가_된다() { + // given: 왕만 배치 + SpyBoard board = SpyBoard.withBothGenerals(); + JanggiGame janggiGame = new JanggiGame(board); + Team prevTurn = janggiGame.turn(); + + // when: 초나라 왕만 옆으로 이동 + janggiGame.movePiece(new Position(8, 4), new Position(8, 5)); // then - assertThat(board.pickPiece(destination)).isEqualTo(piece); - assertThat(janggiGame.getTurn()).isEqualTo(prevTurn.next()); + assertThat(janggiGame) + .extracting(JanggiGame::turn, JanggiGame::canPlaying) + .containsExactly(prevTurn.opposite(), true); } @Test void 현재_턴의_팀에_맞는_기물을_선택할_수_있다() { // given - Piece piece = new Horse(Team.CHO); - SpyBoard board = new SpyBoard(piece); + Position position = new Position(1, 1); + Piece piece = new Chariot(Team.CHO); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(position, piece); JanggiGame janggiGame = new JanggiGame(board); // when - Piece selectedPiece = janggiGame.selectPiece(new Position(1, 1)); + Piece selectedPiece = janggiGame.selectPiece(position); // then assertThat(selectedPiece).isSameAs(piece); @@ -48,11 +61,180 @@ class JanggiGameTest { @Test void 다른_팀의_기물을_선택하면_예외가_발생한다() { // given: CHO 턴인데 HAN 기물 배치 - SpyBoard board = new SpyBoard(new Horse(Team.HAN)); + Position position = new Position(1, 1); + Piece piece = new Chariot(Team.HAN); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(position, piece); JanggiGame janggiGame = new JanggiGame(board); // when & then - assertThatThrownBy(() -> janggiGame.selectPiece(new Position(1, 1))) + assertThatThrownBy(() -> janggiGame.selectPiece(position)) .isInstanceOf(IllegalArgumentException.class); } + + @Test + void 초나라_기물이_한나라_왕을_잡을_경우_게임은_종료되고_승자는_초나라가된다() { + // given: 차가 왕을 잡을 수 있는 위치에 존재 + Position killPosition = new Position(6, 4); + Position hanGeneralPosition = new Position(1, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(killPosition, new Chariot(Team.CHO)); + JanggiGame janggiGame = new JanggiGame(board); + + // when: 초나라 차로 한나라 왕 잡기 + janggiGame.movePiece(killPosition, hanGeneralPosition); + + // then + assertThat(janggiGame) + .extracting(JanggiGame::canPlaying, JanggiGame::resolveWinner) + .containsExactly(false, Team.CHO); + } + + @Test + void 한나라_기물이_초나라_왕을_잡을_경우_게임은_종료되고_승자는_한나라가된다() { + // given: 차가 왕을 잡을 수 있는 위치에 존재 + 다음 차례를 위해 초나라 왕을 일직선 상으로 한칸 아래 이동 + Position killPosition = new Position(3, 4); + Position choGeneralPosition = new Position(9, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(killPosition, new Chariot(Team.HAN)); + JanggiGame janggiGame = new JanggiGame(board); + janggiGame.movePiece(new Position(8, 4), choGeneralPosition); + + // when: 한나라 차로 초나라 왕 잡기 + janggiGame.movePiece(killPosition, choGeneralPosition); + + // then + assertThat(janggiGame) + .extracting(JanggiGame::canPlaying, JanggiGame::resolveWinner) + .containsExactly(false, Team.HAN); + } + + @Test + void 게임이_종료되지_않았을_때_승자를_요청하면_예외가_발생한다() { + // given + SpyBoard board = SpyBoard.withBothGenerals(); + JanggiGame janggiGame = new JanggiGame(board); + + // when & then + assertThatThrownBy(janggiGame::resolveWinner) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 정상적으로_게임이_종료되었을_때_게임을_진행할_수_없다() { + // given: 초나라가 승리한 상황 + Position killPosition = new Position(6, 4); + Position hanGeneralPosition = new Position(1, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(killPosition, new Chariot(Team.CHO)); + JanggiGame janggiGame = new JanggiGame(board); + janggiGame.movePiece(killPosition, hanGeneralPosition); + + // when & then + assertAll( + () -> assertThatThrownBy(() -> janggiGame.selectPiece(hanGeneralPosition)) + .isInstanceOf(IllegalStateException.class), + () -> assertThatThrownBy(() -> janggiGame.movePiece(hanGeneralPosition, killPosition)) + .isInstanceOf(IllegalStateException.class) + ); + } + + @Test + void 기물_이동_후_왕이_마주보고_있는_상황이_되면_빅장_상태가_되고_게임은_유지된다() { + // given: 왕 사이를 막고 있는 기물이 있는 보드 제공 + Position barrierPos = new Position(6, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(barrierPos, new Chariot(Team.CHO)); + JanggiGame janggiGame = new JanggiGame(board); + + // when: 왕 사이를 막고 있는 차를 이동 + janggiGame.movePiece(barrierPos, new Position(6, 0)); + + // then + assertThat(janggiGame) + .extracting(JanggiGame::isBigJang, JanggiGame::canPlaying) + .containsExactly(true, true); + } + + @Test + void 빅장_상태가_되면_게임을_빅장으로_종료_할_수_있다() { + // given: 빅장 상태 + Position barrierPos = new Position(6, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(barrierPos, new Chariot(Team.CHO)); + JanggiGame janggiGame = new JanggiGame(board); + janggiGame.movePiece(barrierPos, new Position(6, 0)); + + // when + janggiGame.finishByBigJang(); + + // then + assertThat(janggiGame) + .extracting(JanggiGame::isBigJangDone, JanggiGame::canPlaying, JanggiGame::isBigJang) + .containsExactly(true, false, false); + } + + @Test + void 빅장_상태가_아닐_때_빅장으로_종료_할_수_없다() { + // given: 빅장 상태가 아님 + SpyBoard board = SpyBoard.withBothGenerals(); + JanggiGame janggiGame = new JanggiGame(board); + + // when & then + assertThatThrownBy(janggiGame::finishByBigJang) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 빅장_상태에서_승자를_요청하면_예외가_발생한다() { + // given + Position barrierPos = new Position(6, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(barrierPos, new Chariot(Team.CHO)); + JanggiGame janggiGame = new JanggiGame(board); + janggiGame.movePiece(barrierPos, new Position(6, 0)); + + // when & then + assertThatThrownBy(janggiGame::resolveWinner) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 장기에서_기물의_점수를_계산하면_후차례에게_가점을_준다() { + // given: 양 팀 다 0점인 상황 + SpyBoard board = SpyBoard.withBothGenerals(); + JanggiGame janggiGame = new JanggiGame(board); + + // when + Map finalScore = janggiGame.calculateFinalScore(); + + // then: 후 턴은 1.5점을 더한 결과를 받는다. + double startTurnScore = finalScore.get(Team.CHO); + double afterTurnScore = finalScore.get(Team.HAN); + assertThat(afterTurnScore).isEqualTo(startTurnScore + 1.5); + } + + @Test + void 빅장으로_게임이_종료되었을_때_초나라의_점수가_더_높다면_초나라가_승리한다() { + // given: 빅장으로 종료한 상황 + 초나라 기물 하나 남아있음. + Position barrierPos = new Position(6, 4); + SpyBoard board = SpyBoard.withBothGenerals().addPiece(barrierPos, new Chariot(Team.CHO)); + JanggiGame janggiGame = new JanggiGame(board); + janggiGame.movePiece(barrierPos, new Position(6, 0)); + janggiGame.finishByBigJang(); + + // when + Team winner = janggiGame.resolveWinner(); + + // then + assertThat(winner).isEqualTo(Team.CHO); + } + + @Test + void 빅장으로_게임이_종료되었을_때_두나라의_점수가_동일하다면_후턴인_한나라가_승리한다() { + // given: 두 나라 모두 장만 남고 + 빅장으로 종료한 상황 + SpyBoard board = SpyBoard.withBothGenerals(); + JanggiGame janggiGame = new JanggiGame(board); + janggiGame.movePiece(new Position(8, 4), new Position(9, 4)); // 아래로만 이동 + janggiGame.finishByBigJang(); + + // when + Team winner = janggiGame.resolveWinner(); + + // then + assertThat(winner).isEqualTo(Team.HAN); + } } \ No newline at end of file diff --git a/src/test/java/model/TeamTest.java b/src/test/java/model/TeamTest.java index 5f925e4cc0..5dc14457df 100644 --- a/src/test/java/model/TeamTest.java +++ b/src/test/java/model/TeamTest.java @@ -1,10 +1,7 @@ package model; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import model.piece.Piece; -import model.testdouble.FakePiece; import org.junit.jupiter.api.Test; class TeamTest { @@ -15,7 +12,7 @@ class TeamTest { Team han = Team.HAN; // when - Team next = han.next(); + Team next = han.opposite(); // then assertThat(next).isEqualTo(Team.CHO); @@ -27,20 +24,9 @@ class TeamTest { Team cho = Team.CHO; // when - Team next = cho.next(); + Team next = cho.opposite(); // then assertThat(next).isEqualTo(Team.HAN); } - - @Test - void 자신의_팀이_아닌_기물을_검증하면_예외가_발생한다() { - // given - Team cho = Team.CHO; - Piece hanPiece = FakePiece.createFake(Team.HAN); - - // when & then - assertThatThrownBy(() -> cho.validateAlly(hanPiece)) - .isInstanceOf(IllegalArgumentException.class); - } } \ No newline at end of file diff --git a/src/test/java/model/board/BoardTest.java b/src/test/java/model/board/BoardTest.java index f3dc059f55..81402188ac 100644 --- a/src/test/java/model/board/BoardTest.java +++ b/src/test/java/model/board/BoardTest.java @@ -6,6 +6,8 @@ import java.util.List; import java.util.Map; import model.Team; +import model.coordinate.Position; +import model.piece.General; import model.piece.Piece; import model.testdouble.FakePiece; import org.junit.jupiter.api.Test; @@ -49,7 +51,7 @@ class BoardTest { board.move(source, destination); // then - assertSuccessMoved(board, destination, piece, source); + assertSuccessMoved(board, source, piece, destination); } @Test @@ -75,7 +77,7 @@ class BoardTest { board.move(source, destination); // then - assertSuccessMoved(board, destination, piece, source); + assertSuccessMoved(board, source, piece, destination); } @Test @@ -116,8 +118,71 @@ class BoardTest { .containsExactly(hurdle1, hurdle2); } + @Test + void 팀의_왕이_살아있는지_알_수_있다() { + // given + Position generalPos = new Position(1, 4); + Board board = new Board(Map.of(generalPos, new General(Team.HAN))); + + // when + boolean alive = board.isAliveGeneral(Team.HAN); + + // then + assertThat(alive).isTrue(); + } + + @Test + void 해당_팀의_왕이_죽었는지_알_수_잇다() { + // given + Board board = new Board(Map.of()); + + // when + boolean alive = board.isAliveGeneral(Team.HAN); + + // then + assertThat(alive).isFalse(); + } + + @Test + void 팀의_왕의_위치를_찾을_수_있다() { + // given + Position generalPos = new Position(1, 4); + General general = new General(Team.HAN); + Board board = new Board(Map.of(generalPos, general)); + + // when + Position foundPosition = board.findGeneralPositionByTeam(Team.HAN); + + // then + assertThat(foundPosition).isEqualTo(generalPos); + } + + @Test + void 팀의_왕이_없으면_위치를_찾을_때_예외가_발생한다() { + // given + Board board = new Board(Map.of()); + + // when & then + assertThatThrownBy(() -> board.findGeneralPositionByTeam(Team.HAN)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 특정_팀의_기물_점수_합계를_계산한다() { + // given + Board board = BoardFactory.generateDefaultPieces(); + Map choPieces = FormationType.SANG_MA_MA_SANG.generateByTeam(Team.CHO); + board.arrangePieces(choPieces); + + // when + double score = board.calculateBaseScore(Team.CHO); + + // then: 기본 총 점수는 72점이다 + assertThat(score).isEqualTo(72); + } + private void assertSuccessMoved(Board board, Position source, FakePiece piece, Position destination) { - assertThat(board.pickPiece(source)).isEqualTo(piece); - assertThat(board.board()).doesNotContainKey(destination); + assertThat(board.pickPiece(destination)).isEqualTo(piece); + assertThat(board.board()).doesNotContainKey(source); } } \ No newline at end of file diff --git a/src/test/java/model/board/DirectionTest.java b/src/test/java/model/board/DirectionTest.java index 2d229e5bee..73382ae432 100644 --- a/src/test/java/model/board/DirectionTest.java +++ b/src/test/java/model/board/DirectionTest.java @@ -4,7 +4,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.stream.Stream; -import model.movement.Direction; +import model.coordinate.Direction; +import model.coordinate.Position; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; diff --git a/src/test/java/model/board/JanggiFormationTest.java b/src/test/java/model/board/FormationTypeTest.java similarity index 71% rename from src/test/java/model/board/JanggiFormationTest.java rename to src/test/java/model/board/FormationTypeTest.java index 41888b4a09..6c7bbf28ba 100644 --- a/src/test/java/model/board/JanggiFormationTest.java +++ b/src/test/java/model/board/FormationTypeTest.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.stream.Stream; import model.Team; +import model.coordinate.Position; import model.piece.Elephant; import model.piece.Horse; import model.piece.Piece; @@ -13,24 +14,24 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; -class JanggiFormationTest { +class FormationTypeTest { static Stream formationTestProvider() { return Stream.of( - Arguments.of(JanggiFormation.SANG_MA_SANG_MA, Elephant.class, Horse.class, Elephant.class, Horse.class), - Arguments.of(JanggiFormation.MA_SANG_MA_SANG, Horse.class, Elephant.class, Horse.class, Elephant.class), - Arguments.of(JanggiFormation.MA_SANG_SANG_MA, Horse.class, Elephant.class, Elephant.class, Horse.class), - Arguments.of(JanggiFormation.SANG_MA_MA_SANG, Elephant.class, Horse.class, Horse.class, Elephant.class) + Arguments.of(FormationType.SANG_MA_SANG_MA, Elephant.class, Horse.class, Elephant.class, Horse.class), + Arguments.of(FormationType.MA_SANG_MA_SANG, Horse.class, Elephant.class, Horse.class, Elephant.class), + Arguments.of(FormationType.MA_SANG_SANG_MA, Horse.class, Elephant.class, Elephant.class, Horse.class), + Arguments.of(FormationType.SANG_MA_MA_SANG, Elephant.class, Horse.class, Horse.class, Elephant.class) ); } static Stream 포메이션_별_기물_위치() { return Stream.of( // formation, 좌외(1), 좌내(2), 우내(6), 우외(7) 순서 - Arguments.of(JanggiFormation.SANG_MA_SANG_MA, Elephant.class, Horse.class, Elephant.class, Horse.class), - Arguments.of(JanggiFormation.MA_SANG_MA_SANG, Horse.class, Elephant.class, Horse.class, Elephant.class), - Arguments.of(JanggiFormation.MA_SANG_SANG_MA, Horse.class, Elephant.class, Elephant.class, Horse.class), - Arguments.of(JanggiFormation.SANG_MA_MA_SANG, Elephant.class, Horse.class, Horse.class, Elephant.class) + Arguments.of(FormationType.SANG_MA_SANG_MA, Elephant.class, Horse.class, Elephant.class, Horse.class), + Arguments.of(FormationType.MA_SANG_MA_SANG, Horse.class, Elephant.class, Horse.class, Elephant.class), + Arguments.of(FormationType.MA_SANG_SANG_MA, Horse.class, Elephant.class, Elephant.class, Horse.class), + Arguments.of(FormationType.SANG_MA_MA_SANG, Elephant.class, Horse.class, Horse.class, Elephant.class) ); } @@ -41,7 +42,7 @@ static Stream choFormationProvider() { @ParameterizedTest(name = "{0} 차림 일 때") @MethodSource("포메이션_별_기물_위치") void 한나라_상차림_기물_순서_테스트( - JanggiFormation formation, + FormationType formation, Class leftOuter, Class leftInner, Class rightInner, @@ -58,7 +59,7 @@ static Stream choFormationProvider() { @ParameterizedTest(name = "{0} 차림일 때") @MethodSource("choFormationProvider") void 초나라_상차림_기물_순서_테스트( - JanggiFormation formation, + FormationType formation, Class leftOuter, Class leftInner, Class rightInner, @@ -77,7 +78,7 @@ static Stream choFormationProvider() { "SANG_MA_SANG_MA, HAN", "MA_SANG_MA_SANG, HAN", "MA_SANG_SANG_MA, HAN", "SANG_MA_MA_SANG, HAN", "SANG_MA_SANG_MA, CHO", "MA_SANG_MA_SANG, CHO", "MA_SANG_SANG_MA, CHO", "SANG_MA_MA_SANG, CHO" }) - void 각_팀별_상차림의_기물_수는_4개여야_한다(JanggiFormation formation, Team team) { + void 각_팀별_상차림의_기물_수는_4개여야_한다(FormationType formation, Team team) { // when assertThat(formation.generateByTeam(team)).hasSize(4); } diff --git a/src/test/java/model/board/PositionTest.java b/src/test/java/model/board/PositionTest.java index 9148ae7a52..f0d5d342bc 100644 --- a/src/test/java/model/board/PositionTest.java +++ b/src/test/java/model/board/PositionTest.java @@ -3,7 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import model.movement.Displacement; +import model.coordinate.Displacement; +import model.coordinate.Position; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -43,7 +44,7 @@ class PositionTest { Position end = new Position(3, 7); // when - Displacement result = end.minus(start); + Displacement result = end.toDisplacement(start); // then assertThat(result.rowDiff()).isEqualTo(-2); diff --git a/src/test/java/model/fixture/PalaceMovePositionFixture.java b/src/test/java/model/fixture/PalaceMovePositionFixture.java new file mode 100644 index 0000000000..c17e501312 --- /dev/null +++ b/src/test/java/model/fixture/PalaceMovePositionFixture.java @@ -0,0 +1,145 @@ +package model.fixture; + +import java.util.List; +import java.util.stream.Stream; +import model.Team; +import model.coordinate.Position; +import model.piece.PieceType; +import org.junit.jupiter.params.provider.Arguments; + +public class PalaceMovePositionFixture { + + // ============================ + // 將 & 士 (PalacePiece) 성공 케이스 + // ============================ + + public static Stream 장_사_이동_가능한_위치() { + return Stream.of( + // 1. 한나라 (HAN): 상단 궁성 (0,3~2,5) + Arguments.of(Team.HAN, new Position(1, 4), new Position(0, 4), PieceType.GENERAL), // 사방위 (중앙 -> 하) + Arguments.of(Team.HAN, new Position(1, 4), new Position(2, 4), PieceType.GUARD), // 사방위 (중앙 -> 상) + Arguments.of(Team.HAN, new Position(1, 4), new Position(1, 3), PieceType.GENERAL), // 사방위 (중앙 -> 좌) + Arguments.of(Team.HAN, new Position(1, 4), new Position(1, 5), PieceType.GUARD), // 사방위 (중앙 -> 우) + Arguments.of(Team.HAN, new Position(1, 4), new Position(2, 5), PieceType.GUARD), // 대각선 (중앙->우하) + Arguments.of(Team.HAN, new Position(1, 4), new Position(0, 5), PieceType.GUARD), // 대각선 (중앙->우상) + Arguments.of(Team.HAN, new Position(1, 4), new Position(2, 3), PieceType.GENERAL), // 대각선 (중앙->좌하) + Arguments.of(Team.HAN, new Position(1, 4), new Position(0, 3), PieceType.GENERAL), // 대각선 (중앙->좌상) + + // 2. 초나라 (CHO): 하단 궁성 (7,3~9,5) + Arguments.of(Team.CHO, new Position(8, 4), new Position(9, 4), PieceType.GENERAL), // 사방위 (중앙 -> 하) + Arguments.of(Team.CHO, new Position(8, 4), new Position(7, 4), PieceType.GUARD), // 사방위 (중앙 -> 상) + Arguments.of(Team.CHO, new Position(8, 4), new Position(8, 3), PieceType.GENERAL), // 사방위 (중앙 -> 좌) + Arguments.of(Team.CHO, new Position(8, 4), new Position(8, 5), PieceType.GUARD), // 사방위 (중앙 -> 우) + Arguments.of(Team.CHO, new Position(8, 4), new Position(9, 5), PieceType.GENERAL), // 대각선 (중앙->좌상) + Arguments.of(Team.CHO, new Position(8, 4), new Position(9, 3), PieceType.GUARD), // 대각선 (중앙->좌하) + Arguments.of(Team.CHO, new Position(8, 4), new Position(7, 5), PieceType.GUARD), // 대각선 (중앙->우상) + Arguments.of(Team.CHO, new Position(8, 4), new Position(7, 3), PieceType.GUARD) // 대각선 (중앙->우하) + ); + } + + // ============================ + // 將 & 士 (PalacePiece) 실패 케이스 + // ============================ + + public static Stream 장_사_이동_불가능한_위치() { + return Stream.of( + // 1. 궁성 사방위 이탈 (한나라 기준) - 상단은 장기판 밖 + Arguments.of(Team.HAN, new Position(0, 3), new Position(0, 2), PieceType.GENERAL), // 좌측 이탈 + Arguments.of(Team.HAN, new Position(0, 5), new Position(0, 6), PieceType.GUARD), // 우측 이탈 + Arguments.of(Team.HAN, new Position(2, 4), new Position(3, 4), PieceType.GUARD), // 하단 이탈 + + // 2. 궁성 사방위 이탈 (초나라 기준) - 하단은 장기판 밖 + Arguments.of(Team.CHO, new Position(9, 3), new Position(9, 2), PieceType.GENERAL), // 좌측 이탈 + Arguments.of(Team.CHO, new Position(9, 5), new Position(9, 6), PieceType.GUARD), // 우측 이탈 + Arguments.of(Team.CHO, new Position(7, 4), new Position(6, 4), PieceType.GENERAL), // 상단 이탈 + + // 3. 잘못된 대각선 시도 (대각선 선이 없는 지점) + Arguments.of(Team.HAN, new Position(0, 4), new Position(1, 3), PieceType.GENERAL), // 상단 변 -> 좌측 중앙 + Arguments.of(Team.HAN, new Position(0, 4), new Position(1, 5), PieceType.GENERAL), // 상단 변 -> 우측 중앙 + Arguments.of(Team.HAN, new Position(1, 3), new Position(0, 4), PieceType.GUARD), // 좌측 변 -> 상단 중앙 + Arguments.of(Team.HAN, new Position(1, 3), new Position(0, 2), PieceType.GUARD), // 좌측 변 -> 하단 중앙 + Arguments.of(Team.CHO, new Position(9, 4), new Position(8, 5), PieceType.GENERAL), // 하단 변 -> 우측 중앙 + Arguments.of(Team.CHO, new Position(9, 4), new Position(8, 3), PieceType.GENERAL), // 하단 변 -> 좌측 중앙 + Arguments.of(Team.CHO, new Position(8, 5), new Position(9, 4), PieceType.GUARD), // 우측 변 -> 하단 중앙 + Arguments.of(Team.CHO, new Position(8, 5), new Position(7, 4), PieceType.GUARD), // 우측 변 -> 상단 중앙 + + // 4. 거리 초과 및 특수 실패 + Arguments.of(Team.HAN, new Position(0, 4), new Position(2, 4), PieceType.GENERAL), // 직선 2칸 + Arguments.of(Team.CHO, new Position(7, 3), new Position(9, 5), PieceType.GUARD), // 대각선 2칸 + Arguments.of(Team.HAN, new Position(1, 4), new Position(1, 4), PieceType.GENERAL), // 제자리 이동 + + // 5. 출발지가 궁성 밖인 경우 + Arguments.of(Team.HAN, new Position(3, 4), new Position(2, 4), PieceType.GENERAL) + ); + } + + // ============================ + // 車 & 包 궁성 대각선 케이스 + // ============================ + public static Stream 차_포_궁성_대각선_이동_경로() { + return Stream.of( + // 한나라 - 교차점 → 중앙 한 칸 (경로 없음) + Arguments.of(Team.HAN, new Position(0, 3), new Position(1, 4), List.of()), + Arguments.of(Team.HAN, new Position(0, 5), new Position(1, 4), List.of()), + Arguments.of(Team.HAN, new Position(2, 3), new Position(1, 4), List.of()), + Arguments.of(Team.HAN, new Position(2, 5), new Position(1, 4), List.of()), + // 한나라 - 중앙 → 교차점 한 칸 (경로 없음) + Arguments.of(Team.HAN, new Position(1, 4), new Position(0, 3), List.of()), + Arguments.of(Team.HAN, new Position(1, 4), new Position(0, 5), List.of()), + Arguments.of(Team.HAN, new Position(1, 4), new Position(2, 3), List.of()), + Arguments.of(Team.HAN, new Position(1, 4), new Position(2, 5), List.of()), + // 한나라 - 교차점 → 교차점 두 칸 (중앙 경유) + Arguments.of(Team.HAN, new Position(0, 3), new Position(2, 5), List.of(new Position(1, 4))), + Arguments.of(Team.HAN, new Position(0, 5), new Position(2, 3), List.of(new Position(1, 4))), + Arguments.of(Team.HAN, new Position(2, 3), new Position(0, 5), List.of(new Position(1, 4))), + Arguments.of(Team.HAN, new Position(2, 5), new Position(0, 3), List.of(new Position(1, 4))), + + // 초나라 - 교차점 → 중앙 한 칸 (경로 없음) + Arguments.of(Team.CHO, new Position(7, 3), new Position(8, 4), List.of()), + Arguments.of(Team.CHO, new Position(7, 5), new Position(8, 4), List.of()), + Arguments.of(Team.CHO, new Position(9, 3), new Position(8, 4), List.of()), + Arguments.of(Team.CHO, new Position(9, 5), new Position(8, 4), List.of()), + // 초나라 - 중앙 → 교차점 한 칸 (경로 없음) + Arguments.of(Team.CHO, new Position(8, 4), new Position(7, 3), List.of()), + Arguments.of(Team.CHO, new Position(8, 4), new Position(7, 5), List.of()), + Arguments.of(Team.CHO, new Position(8, 4), new Position(9, 3), List.of()), + Arguments.of(Team.CHO, new Position(8, 4), new Position(9, 5), List.of()), + // 초나라 - 교차점 → 교차점 두 칸 (중앙 경유) + Arguments.of(Team.CHO, new Position(7, 3), new Position(9, 5), List.of(new Position(8, 4))), + Arguments.of(Team.CHO, new Position(7, 5), new Position(9, 3), List.of(new Position(8, 4))), + Arguments.of(Team.CHO, new Position(9, 3), new Position(7, 5), List.of(new Position(8, 4))), + Arguments.of(Team.CHO, new Position(9, 5), new Position(7, 3), List.of(new Position(8, 4))) + ); + } + + public static Stream 차_포_궁성_대각선_이동_불가능한_위치() { + return Stream.of( + // 한나라 - 교차점이 아닌 곳에서 대각선 시도 + Arguments.of(Team.HAN, new Position(0, 4), new Position(1, 5)), + Arguments.of(Team.HAN, new Position(0, 4), new Position(1, 3)), + Arguments.of(Team.HAN, new Position(1, 3), new Position(2, 4)), + Arguments.of(Team.HAN, new Position(1, 5), new Position(2, 4)), + // 한나라 - 궁성 밖으로 대각선 이동 + Arguments.of(Team.HAN, new Position(0, 3), new Position(3, 6)), + Arguments.of(Team.HAN, new Position(2, 5), new Position(4, 7)), + + // 초나라 - 교차점이 아닌 곳에서 대각선 시도 + Arguments.of(Team.CHO, new Position(7, 4), new Position(8, 5)), + Arguments.of(Team.CHO, new Position(7, 4), new Position(8, 3)), + Arguments.of(Team.CHO, new Position(8, 3), new Position(9, 4)), + Arguments.of(Team.CHO, new Position(8, 5), new Position(9, 4)), + // 초나라 - 궁성 밖으로 대각선 이동 + Arguments.of(Team.CHO, new Position(7, 3), new Position(4, 0)), + Arguments.of(Team.CHO, new Position(9, 5), new Position(6, 2)) + ); + } + + public static Stream 졸_병_궁성_이동_불가능한_위치() { + return Stream.of( + // 적군 궁성 내 '후퇴' 대각선 시도 + Arguments.of(Team.CHO, new Position(2, 4), new Position(3, 5)), + // 적군 궁성 내 대각선 2칸 이동 시도 + Arguments.of(Team.HAN, new Position(7, 3), new Position(9, 5)) + ); + } +} \ No newline at end of file diff --git a/src/test/java/model/fixture/PieceMovePathFixture.java b/src/test/java/model/fixture/PieceMovePathFixture.java index 6548acb4cb..eb33316ab2 100644 --- a/src/test/java/model/fixture/PieceMovePathFixture.java +++ b/src/test/java/model/fixture/PieceMovePathFixture.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.stream.Stream; import model.Team; -import model.board.Position; +import model.coordinate.Position; import org.junit.jupiter.params.provider.Arguments; public class PieceMovePathFixture { @@ -27,6 +27,12 @@ public class PieceMovePathFixture { new Position(0, 0), new Position(3, 0), List.of(new Position(1, 0), new Position(2, 0)) + ), + // 4. 궁성 교차점에서 수평 이동 + Arguments.of( + new Position(2, 5), + new Position(2, 8), + List.of(new Position(2, 6), new Position(2, 7)) ) ); } @@ -76,7 +82,11 @@ public class PieceMovePathFixture { // 초나라 전진/좌/우 Arguments.of(Team.CHO, new Position(6, 2), new Position(5, 2)), Arguments.of(Team.CHO, new Position(6, 2), new Position(6, 1)), - Arguments.of(Team.CHO, new Position(6, 2), new Position(6, 3)) + Arguments.of(Team.CHO, new Position(6, 2), new Position(6, 3)), + // 한나라 적진 궁성 이동 + Arguments.of(Team.HAN, new Position(7, 5), new Position(8, 4)), // 대각선 + // 초나라 적진 궁성 이동 + Arguments.of(Team.CHO, new Position(2, 4), new Position(1, 4)) // 대각선 ); } } diff --git a/src/test/java/model/fixture/PieceMovePositionFixture.java b/src/test/java/model/fixture/PieceMovePositionFixture.java index 75c0c05cca..cb70e8188e 100644 --- a/src/test/java/model/fixture/PieceMovePositionFixture.java +++ b/src/test/java/model/fixture/PieceMovePositionFixture.java @@ -2,29 +2,15 @@ import java.util.stream.Stream; import model.Team; -import model.board.Position; +import model.coordinate.Position; import org.junit.jupiter.params.provider.Arguments; public class PieceMovePositionFixture { - // 공통 - public static Stream 제자리_이동_케이스() { - return Stream.of(Arguments.of(new Position(0, 0), new Position(0, 0))); - } - // ============================ // 직선 이동 (車, 包 공통) // ============================ - public static Stream 사방위_이동_방향_케이스() { - return Stream.of( - Arguments.of(new Position(0, 0), new Position(0, 5)), // 우 이동 - Arguments.of(new Position(0, 0), new Position(5, 0)), // 하 이동 - Arguments.of(new Position(5, 5), new Position(0, 5)), // 상 이동 - Arguments.of(new Position(5, 5), new Position(5, 0)) // 좌 이동 - ); - } - public static Stream 사간방_대각선_이동_방향_케이스() { return Stream.of( Arguments.of(new Position(2, 2), new Position(4, 4)), // 1. 북동 (NE): rowDiff 증가, colDiff 증가 @@ -38,19 +24,6 @@ public class PieceMovePositionFixture { // 馬 // ============================ - public static Stream 마_이동_가능한_위치() { - return Stream.of( - Arguments.of(new Position(5, 5), new Position(3, 4)), // 상+좌 (rowDiff-2, colDiff-1) - Arguments.of(new Position(5, 5), new Position(3, 6)), // 상+우 (rowDiff-2, colDiff+1) - Arguments.of(new Position(5, 5), new Position(7, 4)), // 하+좌 (rowDiff+2, colDiff-1) - Arguments.of(new Position(5, 5), new Position(7, 6)), // 하+우 (rowDiff+2, colDiff+1) - Arguments.of(new Position(5, 5), new Position(4, 3)), // 좌+상 (rowDiff-1, colDiff-2) - Arguments.of(new Position(5, 5), new Position(6, 3)), // 좌+하 (rowDiff+1, colDiff-2) - Arguments.of(new Position(5, 5), new Position(4, 7)), // 우+상 (rowDiff-1, colDiff+2) - Arguments.of(new Position(5, 5), new Position(6, 7)) // 우+하 (rowDiff+1, colDiff+2) - ); - } - public static Stream 마_이동_불가능한_위치() { return Stream.of( Arguments.of(new Position(5, 5), new Position(5, 7)), // 직선 이동 @@ -65,19 +38,6 @@ public class PieceMovePositionFixture { // 象 // ============================ - public static Stream 상_이동_가능한_위치() { - return Stream.of( - Arguments.of(new Position(5, 5), new Position(2, 3)), // 상+좌 (rowDiff-3, colDiff-2) - Arguments.of(new Position(5, 5), new Position(2, 7)), // 상+우 (rowDiff-3, colDiff+2) - Arguments.of(new Position(5, 5), new Position(8, 3)), // 하+좌 (rowDiff+3, colDiff-2) - Arguments.of(new Position(5, 5), new Position(8, 7)), // 하+우 (rowDiff+3, colDiff+2) - Arguments.of(new Position(5, 5), new Position(3, 2)), // 좌+상 (rowDiff-2, colDiff-3) - Arguments.of(new Position(5, 5), new Position(7, 2)), // 좌+하 (rowDiff+2, colDiff-3) - Arguments.of(new Position(5, 5), new Position(3, 8)), // 우+상 (rowDiff-2, colDiff+3) - Arguments.of(new Position(5, 5), new Position(7, 8)) // 우+하 (rowDiff+2, colDiff+3) - ); - } - public static Stream 상_이동_불가능한_위치() { return Stream.of( Arguments.of(new Position(5, 5), new Position(3, 4)), // 마 이동 (rowDiff-2, colDiff-1) @@ -91,23 +51,6 @@ public class PieceMovePositionFixture { // ============================ // 兵 & 卒 (Soldier) 통합 데이터 // ============================ - public static Stream 졸_병_이동_가능한_위치() { - return Stream.concat( - // 한나라 (HAN): 전진(rowDiff+1), 좌우 - Stream.of( - Arguments.of(Team.HAN, new Position(3, 2), new Position(4, 2)), // 전진 - Arguments.of(Team.HAN, new Position(3, 2), new Position(3, 1)), // 좌 - Arguments.of(Team.HAN, new Position(3, 2), new Position(3, 3)) // 우 - ), - // 초나라 (CHO): 전진(rowDiff-1), 좌우 - Stream.of( - Arguments.of(Team.CHO, new Position(6, 2), new Position(5, 2)), // 전진 - Arguments.of(Team.CHO, new Position(6, 2), new Position(6, 1)), // 좌 - Arguments.of(Team.CHO, new Position(6, 2), new Position(6, 3)) // 우 - ) - ); - } - public static Stream 졸_병_이동_불가능한_위치() { return Stream.concat( // 한나라 (HAN) 불가능 diff --git a/src/test/java/model/piece/CannonTest.java b/src/test/java/model/piece/CannonTest.java index 796291c9f3..50d3b4848c 100644 --- a/src/test/java/model/piece/CannonTest.java +++ b/src/test/java/model/piece/CannonTest.java @@ -6,24 +6,13 @@ import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; public class CannonTest { - @ParameterizedTest - @MethodSource("model.fixture.PieceMovePositionFixture#사방위_이동_방향_케이스") - void 포는_직선으로_이동할_수_있다(Position current, Position next) { - // given - Piece cannon = new Cannon(Team.HAN); - - // when & then - assertThatCode(() -> cannon.validateMove(current, next)) - .doesNotThrowAnyException(); - } - @ParameterizedTest @MethodSource("model.fixture.PieceMovePositionFixture#사간방_대각선_이동_방향_케이스") void 포는_대각선이나_제자리로_이동할_수_없다(Position current, Position next) { @@ -31,7 +20,7 @@ public class CannonTest { Piece cannon = new Cannon(Team.HAN); // when & then - assertThatThrownBy(() -> cannon.validateMove(current, next)) + assertThatThrownBy(() -> cannon.pathTo(current, next)) .isInstanceOf(IllegalArgumentException.class); } @@ -42,7 +31,7 @@ public class CannonTest { Piece chariot = new Cannon(Team.HAN); // when - List path = chariot.extractPath(current, next); + List path = chariot.pathTo(current, next); // then assertThat(path).isEqualTo(expectedPath); @@ -85,6 +74,18 @@ public class CannonTest { .isInstanceOf(IllegalArgumentException.class); } + @Test + void 포가_정상적으로_하나의_기물을_넘어가는_경우_예외가_발생하지_않는다() { + // given + Piece cannon = new Cannon(Team.HAN); + Piece hurdle = new Soldier(Team.CHO); + List piecesOnPath = List.of(hurdle); + + // when & then + assertThatCode(() -> cannon.validatePathCondition(piecesOnPath)) + .doesNotThrowAnyException(); + } + @Test void 포가_상대_포를_잡으려_하면_예외가_발생한다() { // given @@ -95,4 +96,39 @@ public class CannonTest { assertThatThrownBy(() -> cannon.validateTarget(targetCannon)) .isInstanceOf(IllegalArgumentException.class); } + + @Test + void 포가_상대_포가_아닌_기물을_잡으려_하면_예외가_발생하지_않는다() { + // given + Piece cannon = new Cannon(Team.HAN); + Piece target = new Soldier(Team.CHO); + + // when & then + assertThatCode(() -> cannon.validateTarget(target)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @MethodSource("model.fixture.PalaceMovePositionFixture#차_포_궁성_대각선_이동_경로") + void 포는_궁성_교차점에서_대각선으로_이동_경로를_구할_수_있다(Team team, Position current, Position next, List expectedPath) { + // given + Piece cannon = new Cannon(team); + + // when & then + List path = cannon.pathTo(current, next); + + // then + assertThat(path).isEqualTo(expectedPath); + } + + @ParameterizedTest + @MethodSource("model.fixture.PalaceMovePositionFixture#차_포_궁성_대각선_이동_불가능한_위치") + void 포는_궁성_교차점이_아닌_곳에서_대각선으로_이동할_수_없다(Team team, Position current, Position next) { + // given + Piece cannon = new Cannon(team); + + // when & then + assertThatThrownBy(() -> cannon.pathTo(current, next)) + .isInstanceOf(IllegalArgumentException.class); + } } \ No newline at end of file diff --git a/src/test/java/model/piece/ChariotTest.java b/src/test/java/model/piece/ChariotTest.java index c0c228b0e3..2ada118414 100644 --- a/src/test/java/model/piece/ChariotTest.java +++ b/src/test/java/model/piece/ChariotTest.java @@ -1,12 +1,11 @@ package model.piece; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; import model.testdouble.FakePiece; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -14,17 +13,6 @@ public class ChariotTest { - @ParameterizedTest - @MethodSource("model.fixture.PieceMovePositionFixture#사방위_이동_방향_케이스") - void 차는_직선으로_이동할_수_있다(Position current, Position next) { - // given - Piece chariot = new Chariot(Team.HAN); - - // when & then - assertThatCode(() -> chariot.validateMove(current, next)) - .doesNotThrowAnyException(); - } - @ParameterizedTest @MethodSource("model.fixture.PieceMovePositionFixture#사간방_대각선_이동_방향_케이스") void 차는_대각선_또는_제자리로_이동할_수_없다(Position current, Position next) { @@ -32,7 +20,7 @@ public class ChariotTest { Piece chariot = new Chariot(Team.HAN); // when & then - assertThatThrownBy(() -> chariot.validateMove(current, next)) + assertThatThrownBy(() -> chariot.pathTo(current, next)) .isInstanceOf(IllegalArgumentException.class); } @@ -43,7 +31,7 @@ public class ChariotTest { Piece chariot = new Chariot(Team.HAN); // when - List path = chariot.extractPath(current, next); + List path = chariot.pathTo(current, next); // then assertThat(path).isEqualTo(expectedPath); @@ -59,4 +47,28 @@ public class ChariotTest { assertThatThrownBy(() -> chariot.validatePathCondition(obstacles)) .isInstanceOf(IllegalArgumentException.class); } + + @ParameterizedTest + @MethodSource("model.fixture.PalaceMovePositionFixture#차_포_궁성_대각선_이동_경로") + void 차는_궁성_교차점에서_대각선으로_이동_경로를_구할_수_있다(Team team, Position current, Position next, List expectedPath) { + // given + Piece cannon = new Chariot(team); + + // when & then + List path = cannon.pathTo(current, next); + + // then + assertThat(path).isEqualTo(expectedPath); + } + + @ParameterizedTest + @MethodSource("model.fixture.PalaceMovePositionFixture#차_포_궁성_대각선_이동_불가능한_위치") + void 차는_궁성_교차점이_아닌_곳에서_대각선으로_이동할_수_없다(Team team, Position current, Position next) { + // given + Piece cannon = new Chariot(team); + + // when & then + assertThatThrownBy(() -> cannon.pathTo(current, next)) + .isInstanceOf(IllegalArgumentException.class); + } } \ No newline at end of file diff --git a/src/test/java/model/piece/ElephantTest.java b/src/test/java/model/piece/ElephantTest.java index 7d14631c14..0dea94ac0f 100644 --- a/src/test/java/model/piece/ElephantTest.java +++ b/src/test/java/model/piece/ElephantTest.java @@ -1,12 +1,11 @@ package model.piece; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; import model.testdouble.FakePiece; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -14,17 +13,6 @@ public class ElephantTest { - @ParameterizedTest - @MethodSource("model.fixture.PieceMovePositionFixture#상_이동_가능한_위치") - void 상은_두칸_직진_후_대각선으로_이동할_수_있다(Position current, Position next) { - // given - Piece elephant = new Elephant(Team.HAN); - - // when & then - assertThatCode(() -> elephant.validateMove(current, next)) - .doesNotThrowAnyException(); - } - @ParameterizedTest @MethodSource("model.fixture.PieceMovePositionFixture#상_이동_불가능한_위치") void 상은_이동_규칙에_맞지_않으면_이동할_수_없다(Position current, Position next) { @@ -32,7 +20,8 @@ public class ElephantTest { Piece elephant = new Elephant(Team.HAN); // when & then - assertThatThrownBy(() -> elephant.validateMove(current, next)); + assertThatThrownBy(() -> elephant.pathTo(current, next)) + .isInstanceOf(IllegalArgumentException.class); } @ParameterizedTest @@ -42,7 +31,7 @@ public class ElephantTest { Piece chariot = new Elephant(Team.HAN); // when - List path = chariot.extractPath(current, next); + List path = chariot.pathTo(current, next); // then assertThat(path).isEqualTo(expectedPath); diff --git a/src/test/java/model/piece/HorseTest.java b/src/test/java/model/piece/HorseTest.java index 8dbe1c958f..cf4eb615c5 100644 --- a/src/test/java/model/piece/HorseTest.java +++ b/src/test/java/model/piece/HorseTest.java @@ -1,12 +1,11 @@ package model.piece; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; import model.testdouble.FakePiece; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -14,17 +13,6 @@ public class HorseTest { - @ParameterizedTest - @MethodSource("model.fixture.PieceMovePositionFixture#마_이동_가능한_위치") - void 마는_한칸_직진_후_대각선으로_이동할_수_있다(Position current, Position next) { - // given - Piece horse = new Horse(Team.HAN); - - // when & then - assertThatCode(() -> horse.validateMove(current, next)) - .doesNotThrowAnyException(); - } - @ParameterizedTest @MethodSource("model.fixture.PieceMovePositionFixture#마_이동_불가능한_위치") void 마는_이동_규칙에_맞지_않으면_이동할_수_없다(Position current, Position next) { @@ -32,7 +20,7 @@ public class HorseTest { Piece horse = new Horse(Team.HAN); // when & then - assertThatThrownBy(() -> horse.validateMove(current, next)) + assertThatThrownBy(() -> horse.pathTo(current, next)) .isInstanceOf(IllegalArgumentException.class); } @@ -43,7 +31,7 @@ public class HorseTest { Piece chariot = new Horse(Team.HAN); // when - List path = chariot.extractPath(current, next); + List path = chariot.pathTo(current, next); // then assertThat(path).isEqualTo(expectedPath); diff --git a/src/test/java/model/piece/PalacePieceTest.java b/src/test/java/model/piece/PalacePieceTest.java new file mode 100644 index 0000000000..105fa6a0a3 --- /dev/null +++ b/src/test/java/model/piece/PalacePieceTest.java @@ -0,0 +1,44 @@ +package model.piece; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import model.Team; +import model.coordinate.Position; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class PalacePieceTest { + + @ParameterizedTest(name = "{0}팀 {3}이 {1}에서 {2}로 이동 성공") + @MethodSource("model.fixture.PalaceMovePositionFixture#장_사_이동_가능한_위치") + void 장_사_이동_성공_테스트(Team team, Position current, Position next, PieceType type) { + // given + Piece piece = createPiece(team, type); + + // when + List result = piece.pathTo(current, next); + + // then + assertThat(result).isEmpty(); + } + + @ParameterizedTest(name = "{0}팀 {3}이 {1}에서 {2}로 이동 실패") + @MethodSource("model.fixture.PalaceMovePositionFixture#장_사_이동_불가능한_위치") + void 장_사_이동_실패_테스트(Team team, Position current, Position next, PieceType type) { + // given + Piece piece = createPiece(team, type); + + // when & then + assertThatThrownBy(() -> piece.pathTo(current, next)) + .isInstanceOf(IllegalArgumentException.class); + } + + private Piece createPiece(Team team, PieceType type) { + if (type == PieceType.GENERAL) { + return new General(team); + } + return new Guard(team); + } +} \ No newline at end of file diff --git a/src/test/java/model/piece/SoldierTest.java b/src/test/java/model/piece/SoldierTest.java index f229aff1e3..159c5da4aa 100644 --- a/src/test/java/model/piece/SoldierTest.java +++ b/src/test/java/model/piece/SoldierTest.java @@ -1,12 +1,11 @@ package model.piece; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; import model.testdouble.FakePiece; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -14,17 +13,6 @@ public class SoldierTest { - @ParameterizedTest(name = "{0}나라 졸 일 때, {1}에서 {2}로 이동할 수 있다.") - @MethodSource("model.fixture.PieceMovePositionFixture#졸_병_이동_가능한_위치") - void 졸_병_이동_성공_테스트(Team team, Position current, Position next) { - // given - Piece soldier = new Soldier(team); - - // when & then - assertThatCode(() -> soldier.validateMove(current, next)) - .doesNotThrowAnyException(); - } - @ParameterizedTest(name = "{0}나라 졸 일 때, {1}에서 {2}로 이동할 수 없다.") @MethodSource("model.fixture.PieceMovePositionFixture#졸_병_이동_불가능한_위치") void 졸_병_이동_실패_테스트(Team team, Position current, Position next) { @@ -32,7 +20,7 @@ public class SoldierTest { Piece soldier = new Soldier(team); // when & then - assertThatThrownBy(() -> soldier.validateMove(current, next)) + assertThatThrownBy(() -> soldier.pathTo(current, next)) .isInstanceOf(IllegalArgumentException.class); } @@ -43,7 +31,7 @@ public class SoldierTest { Piece soldier = new Soldier(team); // when - List path = soldier.extractPath(current, next); + List path = soldier.pathTo(current, next); // then assertThat(path).isEmpty(); @@ -59,4 +47,15 @@ public class SoldierTest { assertThatThrownBy(() -> soldier.validatePathCondition(obstacles)) .isInstanceOf(IllegalArgumentException.class); } + + @ParameterizedTest + @MethodSource("model.fixture.PalaceMovePositionFixture#졸_병_궁성_이동_불가능한_위치") + void 졸은_궁성_교차점이_아닌_곳에서_대각선으로_이동할_수_없다(Team team, Position current, Position next) { + // given + Piece cannon = new Soldier(team); + + // when & then + assertThatThrownBy(() -> cannon.pathTo(current, next)) + .isInstanceOf(IllegalArgumentException.class); + } } \ No newline at end of file diff --git a/src/test/java/model/testdouble/FakePiece.java b/src/test/java/model/testdouble/FakePiece.java index c8b05d087e..842cd4cb77 100644 --- a/src/test/java/model/testdouble/FakePiece.java +++ b/src/test/java/model/testdouble/FakePiece.java @@ -2,27 +2,31 @@ import java.util.List; import model.Team; -import model.board.Position; +import model.coordinate.Position; import model.piece.Piece; import model.piece.PieceType; public class FakePiece extends Piece { - private final boolean movable; private final List path; + private final double score; - FakePiece(Team team, PieceType type, boolean movable, List path) { + FakePiece(Team team, PieceType type, List path, double score) { super(team, type); - this.movable = movable; this.path = path; + this.score = score; } public static FakePiece createFake(Team team) { - return new FakePiece(team, PieceType.SOLDIER, true, List.of()); + return new FakePiece(team, PieceType.SOLDIER, List.of(), 0.0); + } + + public static FakePiece createFakeWithScore(Team team, double score) { + return new FakePiece(team, PieceType.SOLDIER, List.of(), score); } @Override - public List extractPath(Position current, Position next) { + public List pathTo(Position current, Position next) { return path; } @@ -30,4 +34,9 @@ public List extractPath(Position current, Position next) { protected void validateMove(Position current, Position next) { } + + @Override + public double score() { + return score; + } } diff --git a/src/test/java/model/testdouble/SpyBoard.java b/src/test/java/model/testdouble/SpyBoard.java index de1816869a..059318d670 100644 --- a/src/test/java/model/testdouble/SpyBoard.java +++ b/src/test/java/model/testdouble/SpyBoard.java @@ -1,38 +1,34 @@ package model.testdouble; +import java.util.HashMap; import java.util.Map; import model.Team; import model.board.Board; -import model.board.Position; -import model.piece.Horse; +import model.coordinate.Position; +import model.piece.General; import model.piece.Piece; public class SpyBoard extends Board { - // pickPiece 반환용 - private final Piece piece; - public boolean isMoved = false; - - public SpyBoard(Piece piece) { - super(Map.of()); - this.piece = piece; + public SpyBoard(Map pieces) { + super(pieces); } - public static SpyBoard cho() { - return new SpyBoard(new Horse(Team.CHO)); + public static SpyBoard withBothGenerals() { + Map pieces = new HashMap<>(Map.of( + new Position(1, 4), new General(Team.HAN), + new Position(8, 4), new General(Team.CHO) + )); + return new SpyBoard(pieces); } - public static SpyBoard han() { - return new SpyBoard(new Horse(Team.HAN)); + public SpyBoard addPiece(Position position, Piece piece) { + arrangePieces(Map.of(position, piece)); + return new SpyBoard(this.board()); } @Override public void move(Position current, Position next) { - isMoved = true; - } - - @Override - public Piece pickPiece(Position position) { - return piece; + super.move(current, next); } }