diff --git a/README.md b/README.md index 9775dda0ae..8204a7762d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,195 @@ # java-janggi -장기 미션 저장소 +> 장기 미션 저장소 + +페어 : [스타크](https://github.com/MODUGGAGI), [러키](https://github.com/Jiihyun) + +--- + +## 👥 페어 프로그래밍 규칙 + +네비게이터, 드라이버 역할 교환은 **15분**에 한번씩 + +| **네비게이터** | **드라이버** | +|:---------------------------:|:------------------------------:| +| 드라이버에게 지시와 동시에 서로의 대화 내용 기록 | 자신이 작성하려는 코드의 의도를 항상 명확히 말로 전달 | + +--- + +## 🀄️ 구현 기능 목록 + +## ✅ 입력 + +- [x] 게임 시작 시 상차림 입력 받기 + - [x] 지정된 명령어 이외의 문자열 및 숫자 값은 `IllegalArgumentException`을 발생시킨다. + +- [x] 플레이어의 선택 기물 좌표 입력 + - [x] 공백일 경우 `IllegalArgumentException`을 발생시킨다. + - [x] 숫자가 아닐 경우 `IllegalArgumentException`을 발생시킨다. + - [x] 구분자가 `,`가 아닐 경우 `IllegalArgumentException`을 발생시킨다. + +- [x] 플레이어의 목표 좌표 입력 + - [x] 공백일 경우 `IllegalArgumentException`을 발생시킨다. + - [x] 숫자가 아닐 경우 `IllegalArgumentException`을 발생시킨다. + - [x] 구분자가 `,`가 아닐 경우 `IllegalArgumentException`을 발생시킨다. + +## ✅ 출력 + +- [x] 장기판 출력 + - 상차림 선택 후 + - 목표 좌표 입력 후 +- [x] 현재 차례인 나라 출력 + - 『한』, 『초』 + +## ✅ 비지니스 기능 + +### 보드 초기화 + +- [x] 『한』 상차림 +- [x] 『초』 상차림 + +```text +- 좌측 상단을 0행 0열 이라 가정 + +장*1개 +시작 위치 +- 한나라: 1행 4열 +- 초나라: 8행 4열 + +사*2개 +시작 위치 +- 한나라: 0행 3열, 0행 5열 +- 초나라: 9행 3열, 9행 5열 + +차*2개 +시작 위치 +- 한나라: 0행 0열, 0행 8열 +- 초나라: 9행 0열, 9행 8열 + +포*2개 +시작 위치 +- 한나라: 2행 1열, 2행 7열 +- 초나라: 7행 1열, 7행 7열 + +마*2개 +시작 위치 +- 한나라: 0행 2열, 0행 7열 +- 초나라: 9행 2열, 9행 7열 + +상*2개 +시작 위치 +- 한나라: 0행 1열, 0행 6열 +- 초나라: 9행 1열, 9행 6열 + +졸*5개 +시작 위치 +- 한나라: 3행 0열, 3행 2열, 3행 4열, 3행 6열, 3행 8열 +- 초나라: 6행 0열, 6행 2열, 6행 4열, 6행 6열, 6행 8열 +``` + +### 기물 이동 + +- [x] 이동 시키려는 기물의 좌표를 확인한다. + - [x] 좌표가 장기판 범위 벗어난 좌표일 경우 `IllegalArgumentException`을 발생시킨다. + - [x] 좌표에 기물이 존재 하지 않을 경우 `IllegalArgumentException`을 발생시킨다. + - [x] 좌표의 기물이 자신의 진영이 아닐 경우 `IllegalArgumentException`을 발생시킨다. + +- [x] 기물을 목적지 좌표로 이동한다. + - [x] 목적지 좌표가 장기판 범위 벗어난 좌표일 경우 `IllegalArgumentException`을 발생시킨다. + - [x] 기물 규칙 및 제약에 맞게 이동한다. + - [차(車)] + - [x] 상하좌우로 장애물을 만날 때까지 칸 수 제한 없이 이동한다. + - [x] 이동하려는 경로 중간에 다른 기물이 **있**다면 `IllegalArgumentException`을 발생시킨다. + + - [포(包)] + - [x] 상하좌우로 이동하되, 반드시 중간에 다른 기물을 하나 뛰어넘어야 합니다. + - [x] 이동하려는 경로 중간에 다른 기물이 **없**다면 `IllegalArgumentException`을 발생시킨다. + - [x] 넘으려는 기물이 포(包) 일 경우 `IllegalArgumentException`을 발생시킨다. + - [x] 목적지 좌표의 기물이 포(包)일 경우 `IllegalArgumentException`을 발생시킨다. + + - [마(馬)] + - [x] 직선 1칸 + 대각선 1칸(日) 이동한다. + - [x] 목적지 좌표(직선 1칸 또는 대각선 1칸)에 다른 기물이 있으면 `IllegalArgumentException`을 발생시킨다. + + - [상(象)] + - [x] 직선 1칸 + 대각선 2칸(用) 이동한다. + - [x] 목적지 좌표(직선 1칸 또는 대각선 1칸)에 다른 기물이 있으면 `IllegalArgumentException`을 발생시킨다. + + - [궁(楚/漢), 사(士)] + - [x] 상하좌우 직선 1칸 이동한다. + + - [졸/병(卒/兵)] + - [x] 상좌우 직선 1칸 이동한다. 후진은 불가 주의 + +--- + +## 💻 실행 결과 + +```text +1. 상마상마 +2. 마상마상 +3. 마상상마 +4. 상마마상 +한나라의 상차림을 선택해주세요. +1 + +1. 상마상마 +2. 마상마상 +3. 마상상마 +4. 상마마상 +초나라의 상차림을 선택해주세요. +3 + +[장기판] + 0 1 2 3 4 5 6 7 8 +0 車 馬 象 士 . 士 象 馬 車 +1 . . . . 楚 . . . . +2 . 包 . . . . . 包 . +3 卒 . 卒 . 卒 . 卒 . 卒 +4 . . . . . . . . . +5 . . . . . . . . . +6 兵 . 兵 . 兵 . 兵 . 兵 +7 . 包 . . . . . 包 . +8 . . . . 漢 . . . . +9 車 象 馬 士 . 士 象 馬 車 + +초나라 차례 입니다. +공격할 기물의 좌표를 입력해주세요. +3,0 + +이동 시킬 목적지 좌표를 입력해주세요. +4,0 + +[장기판] + 0 1 2 3 4 5 6 7 8 +0 車 馬 象 士 . 士 象 馬 車 +1 . . . . 楚 . . . . +2 . 包 . . . . . 包 . +3 . . 卒 . 卒 . 卒 . 卒 +4 卒 . . . . . . . . +5 . . . . . . . . . +6 兵 . 兵 . 兵 . 兵 . 兵 +7 . 包 . . . . . 包 . +8 . . . . 漢 . . . . +9 車 象 馬 士 . 士 象 馬 車 + +한나라 차례 입니다. +공격할 기물의 좌표를 입력해주세요. +9,0 + +이동 시킬 목적지 좌표를 입력해주세요. +8,0 + +[장기판] + 0 1 2 3 4 5 6 7 8 +0 車 馬 象 士 . 士 象 馬 車 +1 . . . . 楚 . . . . +2 . 包 . . . . . 包 . +3 . . 卒 . 卒 . 卒 . 卒 +4 卒 . . . . . . . . +5 . . . . . . . . . +6 兵 . 兵 . 兵 . 兵 . 兵 +7 . 包 . . . . . 包 . +8 車 . . . 漢 . . . . +9 . 象 馬 士 . 士 象 馬 車 +``` diff --git a/src/main/java/janggi/JanggiApplication.java b/src/main/java/janggi/JanggiApplication.java new file mode 100644 index 0000000000..548a04ecda --- /dev/null +++ b/src/main/java/janggi/JanggiApplication.java @@ -0,0 +1,8 @@ +package janggi; + +public class JanggiApplication { + public static void main(String[] args) { + JanggiGame game = new JanggiGame(); + game.run(); + } +} diff --git a/src/main/java/janggi/JanggiGame.java b/src/main/java/janggi/JanggiGame.java new file mode 100644 index 0000000000..dbb2f8ce31 --- /dev/null +++ b/src/main/java/janggi/JanggiGame.java @@ -0,0 +1,76 @@ +package janggi; + +import janggi.domain.Position; +import janggi.domain.Turn; +import janggi.domain.board.Board; +import janggi.domain.board.ElephantFormation; +import janggi.domain.board.InitialPiecePlacement; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.dto.PiecePositionDto; +import janggi.util.RetryHandler; +import janggi.view.InputView; +import janggi.view.OutputView; +import janggi.view.format.ElephantSetUpFormat; +import java.util.List; +import java.util.Map; + +public class JanggiGame { + + public void run() { + Board board = createBoard(); + OutputView.printBoard(toPiecePositions(board.getBoard())); + play(board); + } + + private Board createBoard() { + ElephantFormation hanElephantFormation = readElephantFormation(Camp.HAN); + ElephantFormation choElephantFormation = readElephantFormation(Camp.CHO); + return InitialPiecePlacement.initialize(hanElephantFormation, choElephantFormation); + } + + private ElephantFormation readElephantFormation(Camp camp) { + return RetryHandler.retryOnInvalidInput(() -> { + ElephantSetUpFormat elephantSetUpFormat = InputView.readElephantSettingCommand(camp); + return elephantSetUpFormat.toElephantFormation(camp); + }); + } + + private List toPiecePositions(Map boardState) { + return boardState.entrySet().stream() + .map(entry -> PiecePositionDto.of(entry.getKey(), entry.getValue())) + .toList(); + } + + private void play(Board board) { + Turn turn = new Turn(); + while (true) { + RetryHandler.retryOnInvalidInput(() -> playTurn(board, turn)); + OutputView.printBoard(toPiecePositions(board.getBoard())); + } + } + + private void playTurn(Board board, Turn turn) { + Camp camp = turn.currentTurn(); + Position source = readSource(board, camp); + Position destination = readDestination(board, source, camp); + board.movePiece(source, destination, camp); + turn.finishTurn(); + } + + private Position readSource(Board board, Camp camp) { + return RetryHandler.retryOnInvalidInput(() -> { + Position source = Position.from(InputView.readSource(camp)); + board.validateSource(source, camp); + return source; + }); + } + + private Position readDestination(Board board, Position source, Camp camp) { + return RetryHandler.retryOnInvalidInput(() -> { + Position destination = Position.from(InputView.readDestination()); + board.validateDestination(destination, source, camp); + return destination; + }); + } +} diff --git a/src/main/java/janggi/domain/Position.java b/src/main/java/janggi/domain/Position.java new file mode 100644 index 0000000000..c2d4417c7f --- /dev/null +++ b/src/main/java/janggi/domain/Position.java @@ -0,0 +1,60 @@ +package janggi.domain; + +import janggi.exception.ExceptionMessage; +import java.util.List; + +public record Position(int row, int column) { + + private static final int POSITION_SIZE = 2; + private static final int MIN_POSITION_INDEX = 0; + private static final int MAX_ROW_INDEX = 9; + private static final int MAX_COLUMN_INDEX = 8; + + public static Position from(List rawPosition) { + validatePositionSize(rawPosition); + return new Position(rawPosition.get(0), rawPosition.get(1)); + } + + private static void validatePositionSize(List position) { + if (position.size() != POSITION_SIZE) { + throw new IllegalArgumentException(ExceptionMessage.INVALID_INPUT_FORMAT.getMessage()); + } + } + + public Position { + validateRow(row); + validateColumn(column); + } + + private void validateRow(int row) { + if (row < MIN_POSITION_INDEX || MAX_ROW_INDEX < row) { + throw new IllegalArgumentException(ExceptionMessage.ROW_OUT_OF_RANGE.getMessage(MIN_POSITION_INDEX, MAX_ROW_INDEX)); + } + } + + private void validateColumn(int column) { + if (column < MIN_POSITION_INDEX || MAX_COLUMN_INDEX < column) { + throw new IllegalArgumentException(ExceptionMessage.COLUMN_OUT_OF_RANGE.getMessage(MIN_POSITION_INDEX, MAX_COLUMN_INDEX)); + } + } + + public int calculateRowDistance(Position other) { + return row - other.row; + } + + public int calculateColumnDistance(Position other) { + return column - other.column; + } + + public Position moveRow(int distance) { + return new Position(row + distance, column); + } + + public Position moveCol(int distance) { + return new Position(row, column + distance); + } + + public Position moveDiagonal(int rowDistance, int colDistance) { + return new Position(row + rowDistance, column + colDistance); + } +} diff --git a/src/main/java/janggi/domain/Turn.java b/src/main/java/janggi/domain/Turn.java new file mode 100644 index 0000000000..2fac1e7bff --- /dev/null +++ b/src/main/java/janggi/domain/Turn.java @@ -0,0 +1,16 @@ +package janggi.domain; + +import janggi.domain.piece.Camp; + +public class Turn { + + private Camp currentCamp = Camp.CHO; + + public Camp currentTurn() { + return currentCamp; + } + + public void finishTurn() { + currentCamp = currentCamp.next(); + } +} diff --git a/src/main/java/janggi/domain/board/Board.java b/src/main/java/janggi/domain/board/Board.java new file mode 100644 index 0000000000..013900d4cf --- /dev/null +++ b/src/main/java/janggi/domain/board/Board.java @@ -0,0 +1,74 @@ +package janggi.domain.board; + +import janggi.domain.Position; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceRule; +import janggi.exception.ExceptionMessage; +import java.util.HashMap; +import java.util.Map; + +public class Board implements BoardChecker { + + private final Map board; + + public Board(Map board) { + this.board = new HashMap<>(board); + } + + @Override + public boolean hasPieceAt(Position position) { + return board.containsKey(position); + } + + @Override + public boolean hasSameCampPieceAt(Position position, Camp camp) { + if (hasPieceAt(position)) { + Piece piece = board.get(position); + return piece.isSameCamp(camp); + } + return false; + } + + @Override + public boolean hasSamePieceRuleAt(Position position, PieceRule pieceRule) { + if (hasPieceAt(position)) { + Piece piece = board.get(position); + return piece.isSamePieceRule(pieceRule); + } + return false; + } + + public void movePiece(Position source, Position destination, Camp camp) { + validateSource(source, camp); + validateDestination(destination, source, camp); + + Piece piece = board.get(source); + piece.validateMove(source, destination, this); + + board.put(destination, piece); + board.remove(source); + } + + public void validateSource(Position source, Camp camp) { + if (!hasPieceAt(source)) { + throw new IllegalArgumentException(ExceptionMessage.SOURCE_NOT_EXISTS.getMessage()); + } + if (!hasSameCampPieceAt(source, camp)) { + throw new IllegalArgumentException(ExceptionMessage.INVALID_CAMP_PIECE.getMessage()); + } + } + + public void validateDestination(Position destination, Position source, Camp camp) { + if (destination.equals(source)) { + throw new IllegalArgumentException(ExceptionMessage.PIECE_MUST_MOVE.getMessage()); + } + if (hasSameCampPieceAt(destination, camp)) { + throw new IllegalArgumentException(ExceptionMessage.SAME_CAMP_PIECE_AT_DESTINATION.getMessage()); + } + } + + public Map getBoard() { + return Map.copyOf(board); + } +} diff --git a/src/main/java/janggi/domain/board/BoardChecker.java b/src/main/java/janggi/domain/board/BoardChecker.java new file mode 100644 index 0000000000..1805f496ee --- /dev/null +++ b/src/main/java/janggi/domain/board/BoardChecker.java @@ -0,0 +1,14 @@ +package janggi.domain.board; + +import janggi.domain.Position; +import janggi.domain.piece.Camp; +import janggi.domain.piece.PieceRule; + +public interface BoardChecker { + + boolean hasPieceAt(Position position); + + boolean hasSameCampPieceAt(Position position, Camp camp); + + boolean hasSamePieceRuleAt(Position position, PieceRule pieceRule); +} diff --git a/src/main/java/janggi/domain/board/ElephantFormation.java b/src/main/java/janggi/domain/board/ElephantFormation.java new file mode 100644 index 0000000000..f400ba11df --- /dev/null +++ b/src/main/java/janggi/domain/board/ElephantFormation.java @@ -0,0 +1,40 @@ +package janggi.domain.board; + +import janggi.domain.Position; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ElephantFormation { + + private static final List SETTING_COLS = List.of(1, 2, 6, 7); + + private final Camp camp; + private final ElephantSetUp setUp; + + public ElephantFormation(Camp camp, ElephantSetUp setUp) { + this.camp = camp; + this.setUp = setUp; + } + + public Map placeElephantSetUpPieces() { + if (camp == Camp.HAN) { + return createByCamp(SETTING_COLS); + } + return createByCamp(SETTING_COLS.reversed()); + } + + private Map createByCamp(List settingCols) { + Map map = new HashMap<>(); + + for (int i = 0; i < settingCols.size(); i++) { + map.put( + new Position(camp.getStartRowPosition(), settingCols.get(i)), + new Piece(setUp.getPieceRules().get(i), camp) + ); + } + return map; + } +} diff --git a/src/main/java/janggi/domain/board/ElephantSetUp.java b/src/main/java/janggi/domain/board/ElephantSetUp.java new file mode 100644 index 0000000000..feb89ad378 --- /dev/null +++ b/src/main/java/janggi/domain/board/ElephantSetUp.java @@ -0,0 +1,25 @@ +package janggi.domain.board; + +import static janggi.domain.piece.PieceRule.ELEPHANT; +import static janggi.domain.piece.PieceRule.HORSE; + +import janggi.domain.piece.PieceRule; +import java.util.List; + +public enum ElephantSetUp { + + LEFT_ELEPHANT(List.of(ELEPHANT, HORSE, ELEPHANT, HORSE)), + RIGHT_ELEPHANT(List.of(HORSE, ELEPHANT, HORSE, ELEPHANT)), + INNER_ELEPHANT(List.of(HORSE, ELEPHANT, ELEPHANT, HORSE)), + OUTER_ELEPHANT(List.of(ELEPHANT, HORSE, HORSE, ELEPHANT)); + + private final List pieceRules; + + ElephantSetUp(List pieceRules) { + this.pieceRules = pieceRules; + } + + public List getPieceRules() { + return pieceRules; + } +} diff --git a/src/main/java/janggi/domain/board/InitialPiecePlacement.java b/src/main/java/janggi/domain/board/InitialPiecePlacement.java new file mode 100644 index 0000000000..924b484cf6 --- /dev/null +++ b/src/main/java/janggi/domain/board/InitialPiecePlacement.java @@ -0,0 +1,56 @@ +package janggi.domain.board; + +import janggi.domain.Position; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceRule; +import java.util.HashMap; +import java.util.Map; + +public enum InitialPiecePlacement { + + CHO_SOLDIER_1(3, 0, Camp.CHO, PieceRule.SOLDIER), + CHO_SOLDIER_2(3, 2, Camp.CHO, PieceRule.SOLDIER), + CHO_SOLDIER_3(3, 4, Camp.CHO, PieceRule.SOLDIER), + CHO_SOLDIER_4(3, 6, Camp.CHO, PieceRule.SOLDIER), + CHO_SOLDIER_5(3, 8, Camp.CHO, PieceRule.SOLDIER), + CHO_CHARIOT_LEFT(0, 0, Camp.CHO, PieceRule.CHARIOT), + CHO_GUARD_LEFT(0, 3, Camp.CHO, PieceRule.GUARD), + CHO_GUARD_RIGHT(0, 5, Camp.CHO, PieceRule.GUARD), + CHO_CHARIOT_RIGHT(0, 8, Camp.CHO, PieceRule.CHARIOT), + CHO_GENERAL(1, 4, Camp.CHO, PieceRule.GENERAL), + CHO_CANNON_LEFT(2, 1, Camp.CHO, PieceRule.CANNON), + CHO_CANNON_RIGHT(2, 7, Camp.CHO, PieceRule.CANNON), + + HAN_SOLDIER_1(6, 0, Camp.HAN, PieceRule.SOLDIER), + HAN_SOLDIER_2(6, 2, Camp.HAN, PieceRule.SOLDIER), + HAN_SOLDIER_3(6, 4, Camp.HAN, PieceRule.SOLDIER), + HAN_SOLDIER_4(6, 6, Camp.HAN, PieceRule.SOLDIER), + HAN_SOLDIER_5(6, 8, Camp.HAN, PieceRule.SOLDIER), + HAN_CANNON_LEFT(7, 1, Camp.HAN, PieceRule.CANNON), + HAN_CANNON_RIGHT(7, 7, Camp.HAN, PieceRule.CANNON), + HAN_GENERAL(8, 4, Camp.HAN, PieceRule.GENERAL), + HAN_CHARIOT_LEFT(9, 0, Camp.HAN, PieceRule.CHARIOT), + HAN_GUARD_LEFT(9, 3, Camp.HAN, PieceRule.GUARD), + HAN_GUARD_RIGHT(9, 5, Camp.HAN, PieceRule.GUARD), + HAN_CHARIOT_RIGHT(9, 8, Camp.HAN, PieceRule.CHARIOT); + + private final Position position; + private final Piece piece; + + InitialPiecePlacement(int row, int column, Camp camp, PieceRule pieceRule) { + this.position = new Position(row, column); + this.piece = new Piece(pieceRule, camp); + } + + public static Board initialize(ElephantFormation hanFormation, ElephantFormation choFormation) { + Map board = new HashMap<>(); + + for (InitialPiecePlacement placement : values()) { + board.put(placement.position, placement.piece); + } + board.putAll(hanFormation.placeElephantSetUpPieces()); + board.putAll(choFormation.placeElephantSetUpPieces()); + return new Board(board); + } +} diff --git a/src/main/java/janggi/domain/piece/Camp.java b/src/main/java/janggi/domain/piece/Camp.java new file mode 100644 index 0000000000..d0326ba964 --- /dev/null +++ b/src/main/java/janggi/domain/piece/Camp.java @@ -0,0 +1,31 @@ +package janggi.domain.piece; + +public enum Camp { + + HAN(-1, 9), + CHO(1, 0), + ; + + private final int forwardDirection; + private final int startRowPosition; + + Camp(int forwardDirection, int startRowPosition) { + this.forwardDirection = forwardDirection; + this.startRowPosition = startRowPosition; + } + + public boolean matchesForwardDirection(int direction) { + return forwardDirection == direction; + } + + public int getStartRowPosition() { + return startRowPosition; + } + + public Camp next() { + if (this == CHO) { + return HAN; + } + return CHO; + } +} diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java new file mode 100644 index 0000000000..5aa846a9a4 --- /dev/null +++ b/src/main/java/janggi/domain/piece/Piece.java @@ -0,0 +1,19 @@ +package janggi.domain.piece; + +import janggi.domain.Position; +import janggi.domain.board.BoardChecker; + +public record Piece(PieceRule pieceRule, Camp camp) { + + public void validateMove(Position source, Position destination, BoardChecker board) { + pieceRule.validateMove(source, destination, camp, board); + } + + public boolean isSamePieceRule(PieceRule pieceRule) { + return this.pieceRule == pieceRule; + } + + public boolean isSameCamp(Camp camp) { + return this.camp == camp; + } +} diff --git a/src/main/java/janggi/domain/piece/PieceRule.java b/src/main/java/janggi/domain/piece/PieceRule.java new file mode 100644 index 0000000000..ef41808015 --- /dev/null +++ b/src/main/java/janggi/domain/piece/PieceRule.java @@ -0,0 +1,30 @@ +package janggi.domain.piece; + +import janggi.domain.Position; +import janggi.domain.board.BoardChecker; +import janggi.domain.piece.strategy.DiagonalStepRule; +import janggi.domain.piece.strategy.EmptySlidingRule; +import janggi.domain.piece.strategy.JumpingSlidingRule; +import janggi.domain.piece.strategy.MoveRule; +import janggi.domain.piece.strategy.SingleStepRule; + +public enum PieceRule { + + GENERAL(new SingleStepRule(false)), + CHARIOT(new EmptySlidingRule()), + HORSE(new DiagonalStepRule(1)), + CANNON(new JumpingSlidingRule()), + GUARD(new SingleStepRule(false)), + ELEPHANT(new DiagonalStepRule(2)), + SOLDIER(new SingleStepRule(true)); + + private final MoveRule moveRule; + + PieceRule(MoveRule moveRule) { + this.moveRule = moveRule; + } + + public void validateMove(Position source, Position destination, Camp camp, BoardChecker board) { + moveRule.validate(source, destination, camp, board, this); + } +} diff --git a/src/main/java/janggi/domain/piece/strategy/BaseMoveRule.java b/src/main/java/janggi/domain/piece/strategy/BaseMoveRule.java new file mode 100644 index 0000000000..43f252bbbf --- /dev/null +++ b/src/main/java/janggi/domain/piece/strategy/BaseMoveRule.java @@ -0,0 +1,22 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Position; +import janggi.domain.board.BoardChecker; +import janggi.domain.piece.Camp; +import janggi.domain.piece.PieceRule; +import janggi.exception.ExceptionMessage; +import java.util.List; + +public abstract class BaseMoveRule implements MoveRule { + + @Override + public abstract void validate(Position source, Position destination, Camp camp, BoardChecker board, PieceRule pieceRule); + + protected void validateEmptyPath(List path, BoardChecker board) { + for (int i = 0; i < path.size() - 1; i++) { + if (board.hasPieceAt(path.get(i))) { + throw new IllegalArgumentException(ExceptionMessage.PATH_NOT_EMPTY.getMessage()); + } + } + } +} diff --git a/src/main/java/janggi/domain/piece/strategy/DiagonalStepRule.java b/src/main/java/janggi/domain/piece/strategy/DiagonalStepRule.java new file mode 100644 index 0000000000..6ecadddae9 --- /dev/null +++ b/src/main/java/janggi/domain/piece/strategy/DiagonalStepRule.java @@ -0,0 +1,64 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Position; +import janggi.domain.board.BoardChecker; +import janggi.domain.piece.Camp; +import janggi.domain.piece.PieceRule; +import janggi.exception.ExceptionMessage; +import java.util.ArrayList; +import java.util.List; + +public class DiagonalStepRule extends BaseMoveRule { + + private static final int STRAIGHT_DISTANCE = 1; + + private final int diagonalDistance; + + public DiagonalStepRule(int diagonalDistance) { + this.diagonalDistance = diagonalDistance; + } + + @Override + public void validate(Position source, Position destination, Camp camp, BoardChecker board, PieceRule pieceRule) { + DirectionInformation direction = new DirectionInformation(source, destination); + validateDistance(direction); + List path = findPath(source, direction); + validateEmptyPath(path, board); + } + + private void validateDistance(DirectionInformation direction) { + if (!direction.hasAbsDifferences(diagonalDistance, STRAIGHT_DISTANCE + diagonalDistance)) { + throw new IllegalArgumentException( + ExceptionMessage.INVALID_DIAGONAL_STEP_MOVE.getMessage(STRAIGHT_DISTANCE, diagonalDistance) + ); + } + } + + private List findPath(Position source, DirectionInformation direction) { + List path = new ArrayList<>(); + + Position first = moveStraightStep(source, direction); + path.add(first); + path.addAll(moveDiagonal(first, direction)); + return path; + } + + private Position moveStraightStep(Position source, DirectionInformation direction) { + if (direction.isRowBiggerThanCol()) { + return source.moveRow(direction.calculateRowDirection()); + } + return source.moveCol(direction.calculateColDirection()); + } + + private List moveDiagonal(Position source, DirectionInformation direction) { + List path = new ArrayList<>(); + int rowDirection = direction.calculateRowDirection(); + int colDirection = direction.calculateColDirection(); + + for (int i = 0; i < diagonalDistance; i++) { + source = source.moveDiagonal(rowDirection, colDirection); + path.add(source); + } + return path; + } +} diff --git a/src/main/java/janggi/domain/piece/strategy/DirectionInformation.java b/src/main/java/janggi/domain/piece/strategy/DirectionInformation.java new file mode 100644 index 0000000000..838ce1e469 --- /dev/null +++ b/src/main/java/janggi/domain/piece/strategy/DirectionInformation.java @@ -0,0 +1,46 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Position; + +public record DirectionInformation(int rowDistance, int colDistance) { + + public DirectionInformation(Position source, Position destination) { + this(destination.calculateRowDistance(source), destination.calculateColumnDistance(source)); + } + + public int calculateRowDirection() { + if (rowDistance == 0) { + return 0; + } + return rowDistance / Math.abs(rowDistance); + } + + public int calculateColDirection() { + if (colDistance == 0) { + return 0; + } + return colDistance / Math.abs(colDistance); + } + + public int calculateDistance() { + return Math.abs(rowDistance) + Math.abs(colDistance); + } + + public boolean isRowBiggerThanCol() { + return Math.abs(rowDistance) > Math.abs(colDistance); + } + + public boolean isHorizontal() { + return rowDistance == 0; + } + + public boolean isVertical() { + return colDistance == 0; + } + + public boolean hasAbsDifferences(int distance1, int distance2) { + int absRow = Math.abs(rowDistance); + int absCol = Math.abs(colDistance); + return (absRow == distance1 && absCol == distance2) || (absRow == distance2 && absCol == distance1); + } +} diff --git a/src/main/java/janggi/domain/piece/strategy/EmptySlidingRule.java b/src/main/java/janggi/domain/piece/strategy/EmptySlidingRule.java new file mode 100644 index 0000000000..ee46ad0481 --- /dev/null +++ b/src/main/java/janggi/domain/piece/strategy/EmptySlidingRule.java @@ -0,0 +1,17 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Position; +import janggi.domain.board.BoardChecker; +import janggi.domain.piece.Camp; +import janggi.domain.piece.PieceRule; +import java.util.List; + +public class EmptySlidingRule extends SlidingRule { + + @Override + public void validate(Position source, Position destination, Camp camp, BoardChecker board, PieceRule pieceRule) { + DirectionInformation directionInformation = new DirectionInformation(source, destination); + List path = findPath(source, directionInformation); + validateEmptyPath(path, board); + } +} diff --git a/src/main/java/janggi/domain/piece/strategy/JumpingSlidingRule.java b/src/main/java/janggi/domain/piece/strategy/JumpingSlidingRule.java new file mode 100644 index 0000000000..34e6943cbd --- /dev/null +++ b/src/main/java/janggi/domain/piece/strategy/JumpingSlidingRule.java @@ -0,0 +1,60 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Position; +import janggi.domain.board.BoardChecker; +import janggi.domain.piece.Camp; +import janggi.domain.piece.PieceRule; +import janggi.exception.ExceptionMessage; +import java.util.List; + +public class JumpingSlidingRule extends SlidingRule { + + private static final int REQUIRED_PIECE_COUNT = 1; + + @Override + public void validate(Position source, Position destination, Camp camp, BoardChecker board, PieceRule pieceRule) { + DirectionInformation directionInformation = new DirectionInformation(source, destination); + + List path = findPath(source, directionInformation); + checkJumpingPath(path, board, pieceRule); + } + + private void checkJumpingPath(List path, BoardChecker board, PieceRule pieceRule) { + int jumpedPieceCount = countJumpedPiece(path, board, pieceRule); + validateJumpedPieceCount(jumpedPieceCount); + validateDestination(board, pieceRule, path.getLast()); + } + + private int countJumpedPiece(List path, BoardChecker board, PieceRule pieceRule) { + int jumpedPieceCount = 0; + for (int i = 0; i < path.size() - 1; i++) { + Position position = path.get(i); + if (!board.hasPieceAt(position)) { + continue; + } + validateDifferentPieceRule(board, pieceRule, position); + jumpedPieceCount++; + } + return jumpedPieceCount; + } + + private void validateDifferentPieceRule(BoardChecker board, PieceRule pieceRule, Position position) { + if (board.hasSamePieceRuleAt(position, pieceRule)) { + throw new IllegalArgumentException(ExceptionMessage.SAME_PIECE_TYPE_IN_PATH.getMessage()); + } + } + + private void validateJumpedPieceCount(int jumpedPieceCount) { + if (jumpedPieceCount != REQUIRED_PIECE_COUNT) { + throw new IllegalArgumentException( + ExceptionMessage.INVALID_JUMPED_PIECE_COUNT.getMessage(REQUIRED_PIECE_COUNT) + ); + } + } + + private void validateDestination(BoardChecker board, PieceRule pieceRule, Position position) { + if (board.hasSamePieceRuleAt(position, pieceRule)) { + throw new IllegalArgumentException(ExceptionMessage.SAME_PIECE_TYPE_AT_DESTINATION.getMessage()); + } + } +} diff --git a/src/main/java/janggi/domain/piece/strategy/MoveRule.java b/src/main/java/janggi/domain/piece/strategy/MoveRule.java new file mode 100644 index 0000000000..fc28b5e4e7 --- /dev/null +++ b/src/main/java/janggi/domain/piece/strategy/MoveRule.java @@ -0,0 +1,11 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Position; +import janggi.domain.board.BoardChecker; +import janggi.domain.piece.Camp; +import janggi.domain.piece.PieceRule; + +public interface MoveRule { + + void validate(Position source, Position destination, Camp camp, BoardChecker board, PieceRule pieceRule); +} diff --git a/src/main/java/janggi/domain/piece/strategy/SingleStepRule.java b/src/main/java/janggi/domain/piece/strategy/SingleStepRule.java new file mode 100644 index 0000000000..b7c5a92ce6 --- /dev/null +++ b/src/main/java/janggi/domain/piece/strategy/SingleStepRule.java @@ -0,0 +1,43 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Position; +import janggi.domain.board.BoardChecker; +import janggi.domain.piece.Camp; +import janggi.domain.piece.PieceRule; +import janggi.exception.ExceptionMessage; + +public class SingleStepRule extends BaseMoveRule { + + private static final int SINGLE_STEP_DISTANCE = 1; + + private final boolean forwardOnly; + + public SingleStepRule(boolean forwardOnly) { + this.forwardOnly = forwardOnly; + } + + @Override + public void validate(Position source, Position destination, Camp camp, BoardChecker board, PieceRule pieceRule) { + DirectionInformation direction = new DirectionInformation(source, destination); + validateDistance(direction); + + if (forwardOnly) { + validateForwardDirection(direction.calculateRowDirection(), camp); + } + } + + private void validateDistance(DirectionInformation direction) { + if (direction.calculateDistance() != SINGLE_STEP_DISTANCE) { + throw new IllegalArgumentException(ExceptionMessage.INVALID_SINGLE_STEP_MOVE.getMessage(SINGLE_STEP_DISTANCE)); + } + } + + private void validateForwardDirection(int rowDirection, Camp camp) { + boolean isRowMove = rowDirection != 0; + boolean isForward = camp.matchesForwardDirection(rowDirection); + + if (isRowMove && !isForward) { + throw new IllegalArgumentException(ExceptionMessage.INVALID_BACKWARD_MOVEMENT.getMessage()); + } + } +} diff --git a/src/main/java/janggi/domain/piece/strategy/SlidingRule.java b/src/main/java/janggi/domain/piece/strategy/SlidingRule.java new file mode 100644 index 0000000000..492c536f29 --- /dev/null +++ b/src/main/java/janggi/domain/piece/strategy/SlidingRule.java @@ -0,0 +1,32 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Position; +import janggi.exception.ExceptionMessage; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; + +public abstract class SlidingRule extends BaseMoveRule { + + protected List findPath(Position source, DirectionInformation directionInformation) { + if (directionInformation.isHorizontal()) { + return createPath(source, directionInformation.colDistance(), Position::moveCol); + } + if (directionInformation.isVertical()) { + return createPath(source, directionInformation.rowDistance(), Position::moveRow); + } + throw new IllegalArgumentException(ExceptionMessage.ONLY_STRAIGHT_MOVE_ALLOWED.getMessage()); + } + + private List createPath(Position source, int difference, BiFunction move) { + List path = new ArrayList<>(); + int direction = Integer.signum(difference); + int distance = Math.abs(difference); + + for (int i = 0; i < distance; i++) { + source = move.apply(source, direction); + path.add(source); + } + return path; + } +} diff --git a/src/main/java/janggi/dto/PiecePositionDto.java b/src/main/java/janggi/dto/PiecePositionDto.java new file mode 100644 index 0000000000..25726e279d --- /dev/null +++ b/src/main/java/janggi/dto/PiecePositionDto.java @@ -0,0 +1,23 @@ +package janggi.dto; + +import janggi.domain.Position; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.view.format.PieceFormat; + +public record PiecePositionDto( + int row, + int column, + String type, + Camp camp +) { + public static PiecePositionDto of(Position position, Piece piece) { + PieceFormat pieceFormat = PieceFormat.from(piece); + return new PiecePositionDto( + position.row(), + position.column(), + pieceFormat.getFormat(), + piece.camp() + ); + } +} diff --git a/src/main/java/janggi/exception/ExceptionMessage.java b/src/main/java/janggi/exception/ExceptionMessage.java new file mode 100644 index 0000000000..82bc456738 --- /dev/null +++ b/src/main/java/janggi/exception/ExceptionMessage.java @@ -0,0 +1,38 @@ +package janggi.exception; + +public enum ExceptionMessage { + + ONLY_NUMBERS_ALLOWED("숫자만 입력 가능합니다"), + INVALID_INPUT_FORMAT("잘못된 입력 형식입니다."), + INVALID_ELEPHANT_SET_UP_FORMAT("존재하지 않는 상차림 입니다."), + ROW_OUT_OF_RANGE("행은 %d행 이상 %d행 이하여야 합니다."), + COLUMN_OUT_OF_RANGE("열은 %d열 이상 %d열 이하여야 합니다."), + SOURCE_NOT_EXISTS("출발지에 기물이 존재하지 않습니다."), + INVALID_CAMP_PIECE("상대 진영의 기물은 이동할 수 없습니다."), + PATH_NOT_EMPTY("경로 상에 기물이 존재합니다."), + SAME_CAMP_PIECE_AT_DESTINATION("목적지에 같은 진영의 기물이 존재합니다."), + SAME_PIECE_TYPE_IN_PATH("경로상에 같은 종류의 기물이 존재합니다."), + SAME_PIECE_TYPE_AT_DESTINATION("목적지에 같은 종류의 기물이 존재합니다."), + ONLY_STRAIGHT_MOVE_ALLOWED("해당 기물은 직선 이동만 가능합니다."), + PIECE_MUST_MOVE("기물은 반드시 이동해야 합니다."), + INVALID_BACKWARD_MOVEMENT("해당 기물은 후진할 수 없습니다."), + INVALID_SINGLE_STEP_MOVE("해당 기물은 %d칸만 이동할 수 있습니다."), + INVALID_JUMPED_PIECE_COUNT("해당 기물은 정확히 %d개의 기물만 뛰어넘을 수 있습니다."), + INVALID_DIAGONAL_STEP_MOVE("해당 기물은 직선 %d칸 이동 후 대각선 %d칸 이동만 가능합니다."), + CAMP_FORMAT_NOT_FOUND("존재하지 않는 진영 형식입니다."), + PIECE_FORMAT_NOT_FOUND("존재하지 않는 기물 형식입니다."), + ; + + + private static final String ERROR_PREFIX = "[ERROR] "; + + private final String message; + + ExceptionMessage(String message) { + this.message = ERROR_PREFIX + message; + } + + public String getMessage(Object... args) { + return String.format(message, args); + } +} diff --git a/src/main/java/janggi/util/Parser.java b/src/main/java/janggi/util/Parser.java new file mode 100644 index 0000000000..7518e7b898 --- /dev/null +++ b/src/main/java/janggi/util/Parser.java @@ -0,0 +1,26 @@ +package janggi.util; + +import janggi.exception.ExceptionMessage; +import java.util.Arrays; +import java.util.List; + +public final class Parser { + + private Parser() { + } + + public static List parseByDelimiter(String delimiter, String input) { + return Arrays.stream(input.split(delimiter)) + .map(String::strip) + .map(Parser::parseToInt) + .toList(); + } + + private static int parseToInt(String number) { + try { + return Integer.parseInt(number); + } catch (NumberFormatException numberFormatException) { + throw new IllegalArgumentException(ExceptionMessage.ONLY_NUMBERS_ALLOWED.getMessage()); + } + } +} diff --git a/src/main/java/janggi/util/RetryHandler.java b/src/main/java/janggi/util/RetryHandler.java new file mode 100644 index 0000000000..434abeb0af --- /dev/null +++ b/src/main/java/janggi/util/RetryHandler.java @@ -0,0 +1,31 @@ +package janggi.util; + +import janggi.view.OutputView; +import java.util.function.Supplier; + +public final class RetryHandler { + + private RetryHandler() { + } + + public static T retryOnInvalidInput(Supplier input) { + while (true) { + try { + return input.get(); + } catch (IllegalArgumentException e) { + OutputView.printError(e.getMessage()); + } + } + } + + public static void retryOnInvalidInput(Runnable input) { + while (true) { + try { + input.run(); + return; + } catch (IllegalArgumentException e) { + OutputView.printError(e.getMessage()); + } + } + } +} diff --git a/src/main/java/janggi/view/InputView.java b/src/main/java/janggi/view/InputView.java new file mode 100644 index 0000000000..d05949887a --- /dev/null +++ b/src/main/java/janggi/view/InputView.java @@ -0,0 +1,52 @@ +package janggi.view; + +import janggi.domain.piece.Camp; +import janggi.exception.ExceptionMessage; +import janggi.util.Parser; +import janggi.view.format.CampFormat; +import janggi.view.format.ElephantSetUpFormat; +import java.util.List; +import java.util.Scanner; + +public final class InputView { + + private static final Scanner SCANNER = new Scanner(System.in); + private static final String DELIMITER = ","; + private static final String LINE_SEPARATOR = System.lineSeparator(); + + private InputView() { + } + + public static ElephantSetUpFormat readElephantSettingCommand(Camp camp) { + CampFormat campFormat = CampFormat.from(camp); + System.out.println(LINE_SEPARATOR + "%s나라의 상차림을 선택해주세요.".formatted(campFormat.getName())); + for (ElephantSetUpFormat elephantSetUpFormat : ElephantSetUpFormat.values()) { + System.out.println(elephantSetUpFormat.getCommand() + ". " + elephantSetUpFormat.getDescription()); + } + return ElephantSetUpFormat.findElephantSettingBy(readLine()); + } + + public static List readSource(Camp camp) { + CampFormat campFormat = CampFormat.from(camp); + System.out.println(LINE_SEPARATOR + "%s나라 차례 입니다.".formatted(campFormat.getName())); + System.out.println("공격할 기물의 좌표를 행,열 순으로 입력해 주세요. (예: 9,8)"); + return Parser.parseByDelimiter(DELIMITER, readLine()); + } + + public static List readDestination() { + System.out.println(LINE_SEPARATOR + "이동 시킬 목적지의 좌표를 행,열 순으로 입력해 주세요. (예: 2,0)"); + return Parser.parseByDelimiter(DELIMITER, readLine()); + } + + private static String readLine() { + String input = SCANNER.nextLine().strip(); + validateInput(input); + return input; + } + + private static void validateInput(String input) { + if (input == null || input.isBlank()) { + throw new IllegalArgumentException(ExceptionMessage.INVALID_INPUT_FORMAT.getMessage()); + } + } +} diff --git a/src/main/java/janggi/view/OutputView.java b/src/main/java/janggi/view/OutputView.java new file mode 100644 index 0000000000..137e985c13 --- /dev/null +++ b/src/main/java/janggi/view/OutputView.java @@ -0,0 +1,81 @@ +package janggi.view; + +import static java.util.stream.Collectors.joining; + +import janggi.dto.PiecePositionDto; +import janggi.view.format.CampFormat; +import java.util.Arrays; +import java.util.List; +import java.util.stream.IntStream; + +public final class OutputView { + + private static final String LINE_SEPARATOR = System.lineSeparator(); + + private static final int ROW_SIZE = 10; + private static final int COLUMN_SIZE = 9; + + private static final String EMPTY_CELL = "."; + + private static final String RESET_COLOR = "\u001B[0m"; + + private static final String[] FULL_WIDTH_NUMBERS = { + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" + }; + + private OutputView() { + } + + public static void printError(String errorMessage) { + System.out.println(errorMessage); + } + + public static void printBoard(List piecePositions) { + System.out.println(renderBoard(piecePositions)); + } + + private static String renderBoard(List piecePositions) { + String[][] board = initializeBoard(); + applyPieces(board, piecePositions); + return LINE_SEPARATOR + "[장기판]" + LINE_SEPARATOR + renderHeader() + LINE_SEPARATOR + renderRows(board); + } + + private static String[][] initializeBoard() { + String[][] board = new String[ROW_SIZE][COLUMN_SIZE]; + for (String[] row : board) { + Arrays.fill(row, EMPTY_CELL); + } + return board; + } + + private static String renderHeader() { + return " " + IntStream.range(0, COLUMN_SIZE) + .mapToObj(OutputView::fullWidthNumber) + .collect(joining(" ")); + } + + private static void applyPieces(String[][] board, List piecePositions) { + for (PiecePositionDto piecePosition : piecePositions) { + board[piecePosition.row()][piecePosition.column()] = colorize(piecePosition); + } + } + + private static String renderRows(String[][] board) { + return IntStream.range(0, ROW_SIZE) + .mapToObj(row -> renderRow(row, board[row])) + .collect(joining(LINE_SEPARATOR)); + } + + private static String renderRow(int row, String[] cells) { + return fullWidthNumber(row) + ' ' + String.join(" ", cells); + } + + private static String colorize(PiecePositionDto piecePosition) { + CampFormat campFormat = CampFormat.from(piecePosition.camp()); + return campFormat.getColor() + piecePosition.type() + RESET_COLOR; + } + + private static String fullWidthNumber(int number) { + return FULL_WIDTH_NUMBERS[number]; + } +} diff --git a/src/main/java/janggi/view/format/CampFormat.java b/src/main/java/janggi/view/format/CampFormat.java new file mode 100644 index 0000000000..4f6c7892fc --- /dev/null +++ b/src/main/java/janggi/view/format/CampFormat.java @@ -0,0 +1,37 @@ +package janggi.view.format; + +import janggi.domain.piece.Camp; +import janggi.exception.ExceptionMessage; +import java.util.Arrays; + +public enum CampFormat { + + CHO(Camp.CHO, "초", "\u001B[32m"), + HAN(Camp.HAN, "한", "\u001B[31m"), + ; + + private final Camp camp; + private final String name; + private final String color; + + CampFormat(Camp camp, String name, String color) { + this.camp = camp; + this.name = name; + this.color = color; + } + + public static CampFormat from(Camp camp) { + return Arrays.stream(CampFormat.values()) + .filter(element -> element.camp.equals(camp)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(ExceptionMessage.CAMP_FORMAT_NOT_FOUND.getMessage())); + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } +} diff --git a/src/main/java/janggi/view/format/ElephantSetUpFormat.java b/src/main/java/janggi/view/format/ElephantSetUpFormat.java new file mode 100644 index 0000000000..516da371c9 --- /dev/null +++ b/src/main/java/janggi/view/format/ElephantSetUpFormat.java @@ -0,0 +1,45 @@ +package janggi.view.format; + +import janggi.domain.board.ElephantFormation; +import janggi.domain.board.ElephantSetUp; +import janggi.domain.piece.Camp; +import janggi.exception.ExceptionMessage; +import java.util.Arrays; + +public enum ElephantSetUpFormat { + + LEFT_ELEPHANT("1", "상마상마", ElephantSetUp.LEFT_ELEPHANT), + RIGHT_ELEPHANT("2", "마상마상", ElephantSetUp.RIGHT_ELEPHANT), + INNER_ELEPHANT("3", "마상상마", ElephantSetUp.INNER_ELEPHANT), + OUTER_ELEPHANT("4", "상마마상", ElephantSetUp.OUTER_ELEPHANT), + ; + + private final String command; + private final String description; + private final ElephantSetUp elephantSetUp; + + ElephantSetUpFormat(String command, String description, ElephantSetUp elephantSetUp) { + this.command = command; + this.description = description; + this.elephantSetUp = elephantSetUp; + } + + public static ElephantSetUpFormat findElephantSettingBy(String command) { + return Arrays.stream(values()) + .filter(element -> element.command.equals(command)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(ExceptionMessage.INVALID_ELEPHANT_SET_UP_FORMAT.getMessage())); + } + + public String getCommand() { + return command; + } + + public String getDescription() { + return description; + } + + public ElephantFormation toElephantFormation(Camp camp) { + return new ElephantFormation(camp, elephantSetUp); + } +} diff --git a/src/main/java/janggi/view/format/PieceFormat.java b/src/main/java/janggi/view/format/PieceFormat.java new file mode 100644 index 0000000000..1f83213623 --- /dev/null +++ b/src/main/java/janggi/view/format/PieceFormat.java @@ -0,0 +1,57 @@ +package janggi.view.format; + +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceRule; +import janggi.exception.ExceptionMessage; +import java.util.Arrays; + +public enum PieceFormat { + + GENERAL_CHO(PieceRule.GENERAL, Camp.CHO, "楚"), + GENERAL_HAN(PieceRule.GENERAL, Camp.HAN, "漢"), + + SOLDIER_CHO(PieceRule.SOLDIER, Camp.CHO, "卒"), + SOLDIER_HAN(PieceRule.SOLDIER, Camp.HAN, "兵"), + + CHARIOT_CHO(PieceRule.CHARIOT, Camp.CHO, "車"), + CHARIOT_HAN(PieceRule.CHARIOT, Camp.HAN, "車"), + + HORSE_CHO(PieceRule.HORSE, Camp.CHO, "馬"), + HORSE_HAN(PieceRule.HORSE, Camp.HAN, "馬"), + + CANNON_CHO(PieceRule.CANNON, Camp.CHO, "包"), + CANNON_HAN(PieceRule.CANNON, Camp.HAN, "包"), + + GUARD_CHO(PieceRule.GUARD, Camp.CHO, "士"), + GUARD_HAN(PieceRule.GUARD, Camp.HAN, "士"), + + ELEPHANT_CHO(PieceRule.ELEPHANT, Camp.CHO, "象"), + ELEPHANT_HAN(PieceRule.ELEPHANT, Camp.HAN, "象"), + ; + + private final PieceRule pieceRule; + private final Camp camp; + private final String format; + + PieceFormat(PieceRule pieceRule, Camp camp, String format) { + this.pieceRule = pieceRule; + this.camp = camp; + this.format = format; + } + + public static PieceFormat from(Piece piece) { + return Arrays.stream(values()) + .filter(element -> element.match(piece)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(ExceptionMessage.PIECE_FORMAT_NOT_FOUND.getMessage())); + } + + private boolean match(Piece piece) { + return piece.isSamePieceRule(pieceRule) && piece.isSameCamp(camp); + } + + public String getFormat() { + return format; + } +} diff --git a/src/test/java/janggi/domain/PositionTest.java b/src/test/java/janggi/domain/PositionTest.java new file mode 100644 index 0000000000..7b440f73e0 --- /dev/null +++ b/src/test/java/janggi/domain/PositionTest.java @@ -0,0 +1,80 @@ +package janggi.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.exception.ExceptionMessage; +import java.util.List; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +public class PositionTest { + + private static final int MIN_POSITION_INDEX = 0; + private static final int MAX_ROW_INDEX = 9; + private static final int MAX_COLUMN_INDEX = 8; + + static Stream> createPositionFormat() { + return Stream.of( + List.of(), + List.of(1), + List.of(1, 2, 3) + ); + } + + @ParameterizedTest + @MethodSource("createPositionFormat") + void 포지션에_행과_열이_존재하지_않을_경우_예외가_발생한다(List position) { + Assertions.assertThatThrownBy(() -> Position.from(position)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_INPUT_FORMAT.getMessage()); + } + + @ParameterizedTest + @CsvSource(value = { + "-1,8", + "10,1", + }) + void 포지션의_행이_0부터_9행까지가_아닐_경우_예외가_발생한다(int row, int col) { + Assertions.assertThatThrownBy(() -> new Position(row, col)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.ROW_OUT_OF_RANGE.getMessage(MIN_POSITION_INDEX, MAX_ROW_INDEX)); + } + + + @ParameterizedTest + @CsvSource(value = { + "0,9", + "0,-1", + }) + void 포지션의_열이_0부터_8열까지가_아닐_경우_예외가_발생한다(int row, int col) { + Assertions.assertThatThrownBy(() -> new Position(row, col)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.COLUMN_OUT_OF_RANGE.getMessage(MIN_POSITION_INDEX, MAX_COLUMN_INDEX)); + } + + @Test + void 행의_차이를_계산한다() { + //given + Position source = new Position(5, 3); + Position destination = new Position(3, 3); + //when + int result = source.calculateRowDistance(destination); + //then + assertThat(result).isEqualTo(2); + } + + @Test + void 열의_차이를_계산한다() { + //given + Position source = new Position(5, 3); + Position destination = new Position(5, 0); + //when + int result = source.calculateColumnDistance(destination); + //then + assertThat(result).isEqualTo(3); + } +} diff --git a/src/test/java/janggi/domain/TurnTest.java b/src/test/java/janggi/domain/TurnTest.java new file mode 100644 index 0000000000..8e40972fab --- /dev/null +++ b/src/test/java/janggi/domain/TurnTest.java @@ -0,0 +1,34 @@ +package janggi.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.domain.piece.Camp; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; + +class TurnTest { + + @Test + void 초나라부터_차례를_진행한다() { + // given + Turn turn = new Turn(); + // when + Camp result = turn.currentTurn(); + // then + assertThat(result).isEqualTo(Camp.CHO); + } + + @Test + void 초나라_차례가_끝나면_한나라_차례가_된다() { + // given + Turn turn = new Turn(); + Camp firstTurn = turn.currentTurn(); + // when + turn.finishTurn(); + // then + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(firstTurn).isEqualTo(Camp.CHO); + assertSoftly.assertThat(turn.currentTurn()).isEqualTo(Camp.HAN); + }); + } +} diff --git a/src/test/java/janggi/domain/board/BoardTest.java b/src/test/java/janggi/domain/board/BoardTest.java new file mode 100644 index 0000000000..b7224bb636 --- /dev/null +++ b/src/test/java/janggi/domain/board/BoardTest.java @@ -0,0 +1,67 @@ +package janggi.domain.board; + +import janggi.domain.Position; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceRule; +import janggi.exception.ExceptionMessage; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; + +class BoardTest { + + @Test + void 목적지에_반대_진영_기물이_있으면_해당_기물을_제거한다() { + // given + Position source = new Position(7, 1); + Position destination = new Position(0, 1); + + Board board = new Board(Map.of( + new Position(4, 1), new Piece(PieceRule.SOLDIER, Camp.HAN), + destination, new Piece(PieceRule.HORSE, Camp.CHO), + source, new Piece(PieceRule.CANNON, Camp.HAN) + )); + // when + board.movePiece(source, destination, Camp.HAN); + // then + boolean destinationExists = board.hasSamePieceRuleAt(destination, PieceRule.CANNON); + boolean sourceExists = board.hasPieceAt(source); + + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(destinationExists).isTrue(); + assertSoftly.assertThat(sourceExists).isFalse(); + }); + } + + @Test + void 상대_진영의_기물을_이동_시키면_예외가_발생한다() { + // given + Position source = new Position(7, 1); + Position destination = new Position(0, 1); + + // when + Board board = new Board(Map.of( + destination, new Piece(PieceRule.HORSE, Camp.CHO), + source, new Piece(PieceRule.CANNON, Camp.CHO) + )); + // then + Assertions.assertThatThrownBy(() -> board.movePiece(source, destination, Camp.HAN)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_CAMP_PIECE.getMessage()); + } + + @Test + void 출발지에_기물이_존재하지_않으면_예외가_발생한다() { + // given + Position source = new Position(7, 1); + Position destination = new Position(0, 1); + // when + Board board = new Board(Map.of()); + // then + Assertions.assertThatThrownBy(() -> board.movePiece(source, destination, Camp.HAN)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.SOURCE_NOT_EXISTS.getMessage()); + } +} diff --git a/src/test/java/janggi/domain/board/InitialPiecePlacementTest.java b/src/test/java/janggi/domain/board/InitialPiecePlacementTest.java new file mode 100644 index 0000000000..d3b37ce4e9 --- /dev/null +++ b/src/test/java/janggi/domain/board/InitialPiecePlacementTest.java @@ -0,0 +1,106 @@ +package janggi.domain.board; + +import janggi.domain.Position; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceRule; +import java.util.HashMap; +import java.util.Map; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; + +public class InitialPiecePlacementTest { + + @Test + void 한_상마상마_초_마상마상_으로_보드를_초기화한다() { + // given + ElephantFormation hanElephantFormation = new ElephantFormation(Camp.HAN, ElephantSetUp.LEFT_ELEPHANT); + ElephantFormation choElephantFormation = new ElephantFormation(Camp.CHO, ElephantSetUp.RIGHT_ELEPHANT); + + Map expectedBoard = createExpectedBoard(); + expectedBoard.putAll(createChoLeftHanRightBoard()); + // when + Board board = InitialPiecePlacement.initialize(hanElephantFormation, choElephantFormation); + // then + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(board.getBoard()).hasSize(32); + assertSoftly.assertThat(board.getBoard()).isEqualTo(expectedBoard); + }); + } + + @Test + void 한_상마마상_초_마상상마_으로_보드를_초기화한다() { + // given + ElephantFormation hanElephantFormation = new ElephantFormation(Camp.HAN, ElephantSetUp.OUTER_ELEPHANT); + ElephantFormation choElephantFormation = new ElephantFormation(Camp.CHO, ElephantSetUp.INNER_ELEPHANT); + + Map expectedBoard = createExpectedBoard(); + expectedBoard.putAll(createChoInnerHanOuterBoard()); + // when + Board board = InitialPiecePlacement.initialize(hanElephantFormation, choElephantFormation); + // then + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(board.getBoard()).hasSize(32); + assertSoftly.assertThat(board.getBoard()).isEqualTo(expectedBoard); + }); + } + + private Map createExpectedBoard() { + Map expectedBoard = new HashMap<>(); + + putPieces(expectedBoard, PieceRule.CHARIOT, Camp.CHO, createPosition(0, 0), createPosition(0, 8)); + putPieces(expectedBoard, PieceRule.GUARD, Camp.CHO, createPosition(0, 3), createPosition(0, 5)); + putPieces(expectedBoard, PieceRule.GENERAL, Camp.CHO, createPosition(1, 4)); + putPieces(expectedBoard, PieceRule.CANNON, Camp.CHO, createPosition(2, 1), createPosition(2, 7)); + putPieces(expectedBoard, PieceRule.SOLDIER, Camp.CHO, + createPosition(3, 0), createPosition(3, 2), + createPosition(3, 4), createPosition(3, 6), + createPosition(3, 8)); + + putPieces(expectedBoard, PieceRule.CHARIOT, Camp.HAN, createPosition(9, 0), createPosition(9, 8)); + putPieces(expectedBoard, PieceRule.GUARD, Camp.HAN, createPosition(9, 3), createPosition(9, 5)); + putPieces(expectedBoard, PieceRule.GENERAL, Camp.HAN, createPosition(8, 4)); + putPieces(expectedBoard, PieceRule.CANNON, Camp.HAN, createPosition(7, 1), createPosition(7, 7)); + putPieces(expectedBoard, PieceRule.SOLDIER, Camp.HAN, + createPosition(6, 0), createPosition(6, 2), + createPosition(6, 4), createPosition(6, 6), + createPosition(6, 8)); + + return expectedBoard; + } + + private Map createChoLeftHanRightBoard() { + Map board = new HashMap<>(); + + putPieces(board, PieceRule.ELEPHANT, Camp.CHO, createPosition(0, 1), createPosition(0, 6)); + putPieces(board, PieceRule.HORSE, Camp.CHO, createPosition(0, 2), createPosition(0, 7)); + + putPieces(board, PieceRule.ELEPHANT, Camp.HAN, createPosition(9, 1), createPosition(9, 6)); + putPieces(board, PieceRule.HORSE, Camp.HAN, createPosition(9, 2), createPosition(9, 7)); + + return board; + } + + private Map createChoInnerHanOuterBoard() { + Map board = new HashMap<>(); + + putPieces(board, PieceRule.ELEPHANT, Camp.CHO, createPosition(0, 2), createPosition(0, 6)); + putPieces(board, PieceRule.HORSE, Camp.CHO, createPosition(0, 1), createPosition(0, 7)); + + putPieces(board, PieceRule.ELEPHANT, Camp.HAN, createPosition(9, 1), createPosition(9, 7)); + putPieces(board, PieceRule.HORSE, Camp.HAN, createPosition(9, 2), createPosition(9, 6)); + + return board; + } + + private void putPieces(Map board, PieceRule pieceRule, Camp camp, + Position... positions) { + for (Position position : positions) { + board.put(position, new Piece(pieceRule, camp)); + } + } + + private Position createPosition(int row, int column) { + return new Position(row, column); + } +} diff --git a/src/test/java/janggi/domain/piece/CampTest.java b/src/test/java/janggi/domain/piece/CampTest.java new file mode 100644 index 0000000000..686961477a --- /dev/null +++ b/src/test/java/janggi/domain/piece/CampTest.java @@ -0,0 +1,30 @@ +package janggi.domain.piece; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class CampTest { + + @ParameterizedTest + @CsvSource({ + "CHO, HAN", + "HAN, CHO" + }) + void next를_호출하면_상대_진영을_반환한다(Camp current, Camp expectedResult) { + // when + Camp next = current.next(); + // then + assertThat(next).isEqualTo(expectedResult); + } + + @ParameterizedTest + @CsvSource({ + "CHO, 1", + "HAN, -1" + }) + void 전진_방향이_일치하는지_확인한다(Camp camp, int rowDirection) { + assertThat(camp.matchesForwardDirection(rowDirection)).isTrue(); + } +} diff --git a/src/test/java/janggi/domain/piece/PieceTest.java b/src/test/java/janggi/domain/piece/PieceTest.java new file mode 100644 index 0000000000..0aa985eb11 --- /dev/null +++ b/src/test/java/janggi/domain/piece/PieceTest.java @@ -0,0 +1,331 @@ +package janggi.domain.piece; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import janggi.domain.Position; +import janggi.domain.board.Board; +import janggi.domain.board.BoardChecker; +import janggi.exception.ExceptionMessage; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class PieceTest { + + private static final int SINGLE_STEP_DISTANCE = 1; + private static final int PASS_PIECE_COUNT = 1; + private static final int HORSE_STRAIGHT_MOVE_DISTANCE = 1; + private static final int HORSE_DIAGONAL_MOVE_DISTANCE = 1; + private static final int ELEPHANT_STRAIGHT_MOVE_DISTANCE = 1; + private static final int ELEPHANT_DIAGONAL_MOVE_DISTANCE = 2; + + @Test + void 같은_전략_및_이동_조건이_적용되는_기물인지_확인한다() { + // given + Piece piece = new Piece(PieceRule.CANNON, Camp.CHO); + // when + boolean result = piece.isSamePieceRule(PieceRule.CANNON); + // then + assertThat(result).isTrue(); + } + + @Test + void 기물이_같은_진영인지_확인한다() { + // given + Piece piece = new Piece(PieceRule.CANNON, Camp.CHO); + // when + boolean result = piece.isSameCamp(Camp.CHO); + // then + assertThat(result).isTrue(); + } + + @DisplayName("궁 행마법 테스트") + @Nested + class General { + @Test + void 궁은_이동_경로에_기물이_없으면_정상_이동한다() { + // given + Piece piece = new Piece(PieceRule.GENERAL, Camp.CHO); + BoardChecker board = new Board(Map.of()); + // when & then + assertDoesNotThrow(() -> + piece.validateMove(new Position(1, 4), new Position(2, 4), board) + ); + } + + @Test + void 궁은_행마법을_따르지_않으면_예외가_발생한다() { + // given + Piece piece = new Piece(PieceRule.GENERAL, Camp.CHO); + BoardChecker board = new Board(Map.of()); + // when & then + Assertions.assertThatThrownBy( + () -> piece.validateMove(new Position(1, 4), new Position(3, 4), board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_SINGLE_STEP_MOVE.getMessage(SINGLE_STEP_DISTANCE)); + } + } + + @DisplayName("사 행마법 테스트") + @Nested + class Guard { + @Test + void 사는_이동_경로에_기물이_없으면_정상_이동한다() { + // given + Piece piece = new Piece(PieceRule.GUARD, Camp.CHO); + BoardChecker board = new Board(Map.of()); + // when & then + assertDoesNotThrow(() -> + piece.validateMove(new Position(0, 3), new Position(0, 4), board) + ); + } + + @Test + void 사는_행마법을_따르지_않으면_예외가_발생한다() { + // given + Piece piece = new Piece(PieceRule.GUARD, Camp.CHO); + BoardChecker board = new Board(Map.of()); + // when & then + Assertions.assertThatThrownBy( + () -> piece.validateMove(new Position(0, 3), new Position(0, 5), board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_SINGLE_STEP_MOVE.getMessage(SINGLE_STEP_DISTANCE)); + } + } + + @DisplayName("마 행마법 테스트") + @Nested + class Horse { + @Test + void 마는_이동_경로에_기물이_없으면_정상_이동한다() { + // given + Piece piece = new Piece(PieceRule.HORSE, Camp.CHO); + BoardChecker board = new Board(Map.of()); + // when & then + assertDoesNotThrow(() -> + piece.validateMove(new Position(0, 1), new Position(2, 2), board) + ); + } + + @Test + void 마는_이동_경로에_기물이_있으면_예외가_발생한다() { + // given + Piece piece = new Piece(PieceRule.HORSE, Camp.CHO); + BoardChecker board = new Board(Map.of( + new Position(1, 1), new Piece(PieceRule.CHARIOT, Camp.HAN) + )); + // when & then + Assertions.assertThatThrownBy( + () -> piece.validateMove(new Position(0, 1), new Position(2, 2), board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.PATH_NOT_EMPTY.getMessage()); + } + + @Test + void 마는_행마법을_따르지_않으면_예외가_발생한다() { + // given + Piece piece = new Piece(PieceRule.HORSE, Camp.CHO); + BoardChecker board = new Board(Map.of()); + // when & then + Assertions.assertThatThrownBy( + () -> piece.validateMove(new Position(0, 1), new Position(0, 7), board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_DIAGONAL_STEP_MOVE.getMessage(HORSE_STRAIGHT_MOVE_DISTANCE, HORSE_DIAGONAL_MOVE_DISTANCE)); + } + } + + @DisplayName("상 행마법 테스트") + @Nested + class Elephant { + @Test + void 상은_이동_경로에_기물이_없으면_정상_이동한다() { + // given + Piece piece = new Piece(PieceRule.ELEPHANT, Camp.CHO); + BoardChecker board = new Board(Map.of()); + // when & then + assertDoesNotThrow(() -> + piece.validateMove(new Position(0, 6), new Position(3, 4), board) + ); + } + + @Test + void 상은_행마법을_따르지_않으면_예외가_발생한다() { + //given + Piece piece = new Piece(PieceRule.ELEPHANT, Camp.CHO); + BoardChecker board = new Board(Map.of()); + //when & then + Assertions.assertThatThrownBy( + () -> piece.validateMove(new Position(0, 6), new Position(3, 3), board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_DIAGONAL_STEP_MOVE.getMessage(ELEPHANT_STRAIGHT_MOVE_DISTANCE, ELEPHANT_DIAGONAL_MOVE_DISTANCE)); + } + } + + @DisplayName("포 행마법 테스트") + @Nested + class Cannon { + @Test + void 포는_이동_경로에_기물이_없으면_예외가_발생한다() { + //given + Piece piece = new Piece(PieceRule.CANNON, Camp.CHO); + BoardChecker board = new Board(Map.of()); + //when & then + Assertions.assertThatThrownBy( + () -> piece.validateMove(new Position(2, 1), new Position(8, 1), board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_JUMPED_PIECE_COUNT.getMessage(PASS_PIECE_COUNT)); + } + + @Test + void 포는_이동_경로에_기물이_2개_이상_있으면_예외가_발생한다() { + //given + Piece piece = new Piece(PieceRule.CANNON, Camp.CHO); + BoardChecker board = new Board(Map.of( + new Position(3, 1), new Piece(PieceRule.CHARIOT, Camp.HAN), + new Position(5, 1), new Piece(PieceRule.CHARIOT, Camp.CHO) + )); + //when & then + Assertions.assertThatThrownBy( + () -> piece.validateMove(new Position(2, 1), new Position(8, 1), board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_JUMPED_PIECE_COUNT.getMessage(PASS_PIECE_COUNT)); + } + + @Test + void 포는_이동_경로에_기물이_1개만_있으면_정상_이동한다() { + // given + Piece piece = new Piece(PieceRule.CANNON, Camp.CHO); + BoardChecker board = new Board(Map.of( + new Position(3, 1), new Piece(PieceRule.CHARIOT, Camp.HAN) + )); + // when & then + assertDoesNotThrow(() -> + piece.validateMove(new Position(2, 1), new Position(8, 1), board) + ); + } + } + + @DisplayName("차 행마법 테스트") + @Nested + class Chariot { + @Test + void 차는_이동_경로에_기물이_없으면_정상_이동한다() { + // given + Piece piece = new Piece(PieceRule.CHARIOT, Camp.CHO); + BoardChecker board = new Board(Map.of()); + // when & then + assertDoesNotThrow(() -> + piece.validateMove(new Position(0, 0), new Position(9, 0), board) + ); + } + + @Test + void 차는_이동_경로에_기물이_1개_이상_있으면_예외가_발생한다() { + //given + Piece piece = new Piece(PieceRule.CHARIOT, Camp.CHO); + BoardChecker board = new Board(Map.of( + new Position(1, 0), new Piece(PieceRule.CHARIOT, Camp.HAN), + new Position(5, 1), new Piece(PieceRule.CHARIOT, Camp.CHO) + + )); + //when & then + Assertions.assertThatThrownBy( + () -> piece.validateMove(new Position(0, 0), new Position(9, 0), board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.PATH_NOT_EMPTY.getMessage()); + } + + @Test + void 차는_행마법을_따르지_않으면_예외가_발생한다() { + //given + Piece piece = new Piece(PieceRule.CHARIOT, Camp.CHO); + BoardChecker board = new Board(Map.of()); + //when & then + Assertions.assertThatThrownBy( + () -> piece.validateMove(new Position(0, 0), new Position(3, 3), board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.ONLY_STRAIGHT_MOVE_ALLOWED.getMessage()); + } + } + + @DisplayName("졸 행마법 테스트") + @Nested + class SoldierCho { + @Test + void 졸은_이동_경로에_기물이_없으면_정상_이동한다() { + // given + Piece piece = new Piece(PieceRule.SOLDIER, Camp.CHO); + BoardChecker board = new Board(Map.of()); + // when & then + assertDoesNotThrow(() -> + piece.validateMove(new Position(3, 0), new Position(4, 0), board) + ); + } + + @Test + void 졸은_후진할_시_예외가_발생한다() { + // given + Piece piece = new Piece(PieceRule.SOLDIER, Camp.CHO); + BoardChecker board = new Board(Map.of()); + // when & then + Assertions.assertThatThrownBy( + () -> piece.validateMove(new Position(3, 0), new Position(2, 0), board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_BACKWARD_MOVEMENT.getMessage()); + } + + @Test + void 졸은_행마법을_따르지_않으면_예외가_발생한다() { + // given + Piece piece = new Piece(PieceRule.SOLDIER, Camp.CHO); + BoardChecker board = new Board(Map.of()); + // when & then + Assertions.assertThatThrownBy( + () -> piece.validateMove(new Position(3, 0), new Position(5, 0), board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_SINGLE_STEP_MOVE.getMessage(SINGLE_STEP_DISTANCE)); + } + } + + @DisplayName("병 행마법 테스트") + @Nested + class SoldierHan { + @Test + void 병은_이동_경로에_기물이_없으면_정상_이동한다() { + // given + Piece piece = new Piece(PieceRule.SOLDIER, Camp.HAN); + BoardChecker board = new Board(Map.of()); + // when & then + assertDoesNotThrow(() -> + piece.validateMove(new Position(6, 0), new Position(5, 0), board) + ); + } + + @Test + void 병은_후진할_시_예외가_발생한다() { + // given + Piece piece = new Piece(PieceRule.SOLDIER, Camp.HAN); + BoardChecker board = new Board(Map.of()); + // when & then + Assertions.assertThatThrownBy( + () -> piece.validateMove(new Position(6, 0), new Position(7, 0), board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_BACKWARD_MOVEMENT.getMessage()); + } + + @Test + void 병은_행마법을_따르지_않으면_예외가_발생한다() { + // given + Piece piece = new Piece(PieceRule.SOLDIER, Camp.HAN); + BoardChecker board = new Board(Map.of()); + // when & then + Assertions.assertThatThrownBy( + () -> piece.validateMove(new Position(6, 0), new Position(1, 0), board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_SINGLE_STEP_MOVE.getMessage(SINGLE_STEP_DISTANCE)); + } + } +} diff --git a/src/test/java/janggi/domain/piece/strategy/DiagonalStepRuleTest.java b/src/test/java/janggi/domain/piece/strategy/DiagonalStepRuleTest.java new file mode 100644 index 0000000000..e78bb58cc8 --- /dev/null +++ b/src/test/java/janggi/domain/piece/strategy/DiagonalStepRuleTest.java @@ -0,0 +1,138 @@ +package janggi.domain.piece.strategy; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import janggi.domain.Position; +import janggi.domain.board.Board; +import janggi.domain.board.BoardChecker; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceRule; +import janggi.exception.ExceptionMessage; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class DiagonalStepRuleTest { + + @DisplayName("직선 1칸 후 대각선 1칸 이동(마) 테스트") + @Nested + class OneStepDiagonal { + + private static final int STRAIGHT_DISTANCE = 1; + private static final int DIAGONAL_DISTANCE = 1; + private final MoveRule moveRule = new DiagonalStepRule(DIAGONAL_DISTANCE); + + private static Stream successPaths() { + Position source = new Position(6, 4); + return Stream.of( + Arguments.of(source, new Position(8, 5)), + Arguments.of(source, new Position(8, 3)), + Arguments.of(source, new Position(4, 3)), + Arguments.of(source, new Position(4, 5)), + Arguments.of(source, new Position(5, 2)), + Arguments.of(source, new Position(7, 2)), + Arguments.of(source, new Position(5, 6)), + Arguments.of(source, new Position(7, 6)) + ); + } + + @ParameterizedTest + @MethodSource("successPaths") + void 직선_1칸_이동_후_대각선_1칸_이동한다(Position source, Position destination) { + assertThatNoException().isThrownBy(() -> moveRule.validate(source, destination, Camp.HAN, new Board(Map.of()), PieceRule.HORSE)); + } + + private static Stream invalidDistancePositions() { + return Stream.of( + Arguments.of(new Position(6, 4), new Position(5, 1)), + Arguments.of(new Position(6, 4), new Position(4, 2)), + Arguments.of(new Position(6, 4), new Position(6, 4)) + ); + } + + @ParameterizedTest + @MethodSource("invalidDistancePositions") + void 행마법_대로_움직이지_않으면_예외가_발생한다(Position source, Position destination) { + assertThatThrownBy(() -> moveRule.validate(source, destination, Camp.CHO, new Board(Map.of()), PieceRule.HORSE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_DIAGONAL_STEP_MOVE.getMessage(STRAIGHT_DISTANCE, DIAGONAL_DISTANCE)); + } + + @Test + void 이동하려는_경로에_기물이_존재하면_예외가_발생한다() { + BoardChecker blockingBoard = new Board(Map.of( + new Position(8, 2), new Piece(PieceRule.SOLDIER, Camp.CHO) + )); + Position source = new Position(9, 2); + Position destination = new Position(7, 3); + // when & then + assertThatThrownBy(() -> moveRule.validate(source, destination, Camp.HAN, blockingBoard, PieceRule.HORSE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.PATH_NOT_EMPTY.getMessage()); + } + } + + @DisplayName("직선 1칸 후 대각선 2칸 이동(상) 테스트") + @Nested + class TwoStepDiagonal { + + private static final int STRAIGHT_DISTANCE = 1; + private static final int DIAGONAL_DISTANCE = 2; + private final MoveRule moveRule = new DiagonalStepRule(DIAGONAL_DISTANCE); + + private static Stream successPositions() { + Position source = new Position(6, 4); + return Stream.of( + Arguments.of(source, new Position(3, 2)), + Arguments.of(source, new Position(4, 1)), + Arguments.of(source, new Position(8, 1)), + Arguments.of(source, new Position(9, 2)), + Arguments.of(source, new Position(9, 6)), + Arguments.of(source, new Position(8, 7)), + Arguments.of(source, new Position(4, 7)), + Arguments.of(source, new Position(3, 6)) + ); + } + + @ParameterizedTest + @MethodSource("successPositions") + void 직선_1칸_이동_후_대각선_2칸_이동한다(Position source, Position destination) { + assertThatNoException().isThrownBy(() -> moveRule.validate(source, destination, Camp.HAN, new Board(Map.of()), PieceRule.ELEPHANT)); + } + + private static Stream invalidDistancePositions() { + return Stream.of( + Arguments.of(new Position(6, 4), new Position(3, 4)), + Arguments.of(new Position(6, 4), new Position(6, 4)) + ); + } + + @ParameterizedTest + @MethodSource("invalidDistancePositions") + void 행마법_대로_움직이지_않으면_예외가_발생한다(Position source, Position destination) { + assertThatThrownBy(() -> moveRule.validate(source, destination, Camp.CHO, new Board(Map.of()), PieceRule.ELEPHANT)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_DIAGONAL_STEP_MOVE.getMessage(STRAIGHT_DISTANCE, DIAGONAL_DISTANCE)); + } + + @Test + void 이동하려는_경로에_기물이_존재하면_예외가_발생한다() { + BoardChecker blockingBoard = new Board(Map.of( + new Position(7, 2), new Piece(PieceRule.SOLDIER, Camp.CHO) + )); + Position source = new Position(9, 1); + Position destination = new Position(6, 3); + // when & then + assertThatThrownBy(() -> moveRule.validate(source, destination, Camp.HAN, blockingBoard, PieceRule.ELEPHANT)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.PATH_NOT_EMPTY.getMessage()); + } + } +} diff --git a/src/test/java/janggi/domain/piece/strategy/DirectionInformationTest.java b/src/test/java/janggi/domain/piece/strategy/DirectionInformationTest.java new file mode 100644 index 0000000000..b5f2137124 --- /dev/null +++ b/src/test/java/janggi/domain/piece/strategy/DirectionInformationTest.java @@ -0,0 +1,63 @@ +package janggi.domain.piece.strategy; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class DirectionInformationTest { + + @ParameterizedTest + @CsvSource({ + "5, 1", + "-3, -1", + "0, 0" + }) + void 목적지와의_행_차이에_따라_행_방향을_결정한다(int rowDifference, int expectedResult) { + // given + DirectionInformation direction = new DirectionInformation(rowDifference, 0); + // when + int result = direction.calculateRowDirection(); + // then + assertThat(result).isEqualTo(expectedResult); + } + + @ParameterizedTest + @CsvSource({ + "7, 1", + "-4, -1", + "0, 0" + }) + void 목적지와의_열_차이에_따라_열_방향을_결정한다(int colDifference, int expectedResult) { + // given + DirectionInformation direction = new DirectionInformation(0, colDifference); + // when + int result = direction.calculateColDirection(); + // then + assertThat(result).isEqualTo(expectedResult); + } + + @ParameterizedTest + @CsvSource({ + "2, 3, 2, 3, true", + "2, 3, 3, 2, true", + "-2, 3, 2, 3, true", + "2, -3, 3, 2, true", + "1, 2, 1, 2, true", + "1, 2, 2, 1, true", + "1, 2, 1, 3, false", + "1, 2, 3, 2, false" + }) + void 두_차이값이_행열_절대_차이값과_일치하는지_확인한다( + int rowDifference, int colDifference, + int difference1, int difference2, + boolean expectedResult + ) { + // given + DirectionInformation direction = new DirectionInformation(rowDifference, colDifference); + // when + boolean result = direction.hasAbsDifferences(difference1, difference2); + // then + assertThat(result).isEqualTo(expectedResult); + } +} diff --git a/src/test/java/janggi/domain/piece/strategy/EmptySlidingRuleTest.java b/src/test/java/janggi/domain/piece/strategy/EmptySlidingRuleTest.java new file mode 100644 index 0000000000..ddeda278aa --- /dev/null +++ b/src/test/java/janggi/domain/piece/strategy/EmptySlidingRuleTest.java @@ -0,0 +1,54 @@ +package janggi.domain.piece.strategy; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import janggi.domain.Position; +import janggi.domain.board.Board; +import janggi.domain.board.BoardChecker; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceRule; +import janggi.exception.ExceptionMessage; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class EmptySlidingRuleTest { + + private final MoveRule rule = new EmptySlidingRule(); + + @Test + void 직선으로_여러_칸_이동한다() { + //given + Position source = new Position(0, 0); + Position destination = new Position(9, 0); + BoardChecker board = new Board(Map.of()); + //when & then + assertThatNoException().isThrownBy(() -> rule.validate(source, destination, Camp.CHO, board, PieceRule.CHARIOT)); + } + + @Test + void 직선으로_이동하지_않으면_예외가_발생한다() { + //given + Position source = new Position(0, 0); + Position destination = new Position(3, 3); + BoardChecker board = new Board(Map.of()); + //when & then + assertThatThrownBy(() -> rule.validate(source, destination, Camp.CHO, board, PieceRule.CHARIOT)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.ONLY_STRAIGHT_MOVE_ALLOWED.getMessage()); + } + + @Test + void 이동하려는_경로에_기물이_존재하면_예외가_발생한다() { + //given + Position source = new Position(0, 0); + Position destination = new Position(5, 0); + Position blockingPosition = new Position(3, 0); + BoardChecker board = new Board(Map.of(blockingPosition, new Piece(PieceRule.SOLDIER, Camp.CHO))); + //when & then + assertThatThrownBy(() -> rule.validate(source, destination, Camp.CHO, board, PieceRule.CHARIOT)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.PATH_NOT_EMPTY.getMessage()); + } +} diff --git a/src/test/java/janggi/domain/piece/strategy/JumpingSlidingRuleTest.java b/src/test/java/janggi/domain/piece/strategy/JumpingSlidingRuleTest.java new file mode 100644 index 0000000000..b77a643edb --- /dev/null +++ b/src/test/java/janggi/domain/piece/strategy/JumpingSlidingRuleTest.java @@ -0,0 +1,89 @@ +package janggi.domain.piece.strategy; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import janggi.domain.Position; +import janggi.domain.board.Board; +import janggi.domain.board.BoardChecker; +import janggi.domain.piece.Camp; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceRule; +import janggi.exception.ExceptionMessage; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class JumpingSlidingRuleTest { + + private static final int REQUIRED_PIECE_COUNT = 1; + + private final MoveRule rule = new JumpingSlidingRule(); + + @Test + void 직선_방향으로_하나의_기물을_넘어_이동한다() { + //given + Position source = new Position(0, 0); + Position destination = new Position(0, 5); + BoardChecker board = new Board(Map.of( + new Position(0, 3), new Piece(PieceRule.SOLDIER, Camp.CHO) + )); + //when & then + assertThatNoException().isThrownBy(() -> rule.validate(source, destination, Camp.CHO, board, PieceRule.CANNON)); + } + + @Test + void 이동하려는_경로에_기물이_존재하지_않으면_예외가_발생한다() { + //given + Position source = new Position(0, 0); + Position destination = new Position(0, 5); + BoardChecker board = new Board(Map.of()); + //when & then + assertThatThrownBy(() -> rule.validate(source, destination, Camp.CHO, board, PieceRule.CANNON)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_JUMPED_PIECE_COUNT.getMessage(REQUIRED_PIECE_COUNT)); + } + + @Test + void 이동하려는_경로에_기물이_2개_이상_존재하면_예외가_발생한다() { + //given + Position source = new Position(0, 0); + Position destination = new Position(0, 5); + BoardChecker board = new Board(Map.of( + new Position(0, 3), new Piece(PieceRule.CHARIOT, Camp.CHO), + new Position(0, 4), new Piece(PieceRule.ELEPHANT, Camp.CHO) + )); + //when & then + assertThatThrownBy(() -> rule.validate(source, destination, Camp.CHO, board, PieceRule.CANNON)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_JUMPED_PIECE_COUNT.getMessage(REQUIRED_PIECE_COUNT)); + } + + @Test + void 이동하려는_경로에_있는_기물이_같은_타입이면_예외가_발생한다() { + //given + Position source = new Position(0, 0); + Position destination = new Position(0, 5); + BoardChecker board = new Board(Map.of( + new Position(0, 4), new Piece(PieceRule.CANNON, Camp.CHO) + )); + //when & then + assertThatThrownBy(() -> rule.validate(source, destination, Camp.CHO, board, PieceRule.CANNON)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.SAME_PIECE_TYPE_IN_PATH.getMessage()); + } + + @Test + void 목적지에_있는_기물이_같은_타입이면_예외가_발생한다() { + //given + Position source = new Position(0, 0); + Position destination = new Position(0, 5); + BoardChecker board = new Board(Map.of( + new Position(0, 3), new Piece(PieceRule.SOLDIER, Camp.CHO), + new Position(0, 5), new Piece(PieceRule.CANNON, Camp.CHO) + )); + //when & then + assertThatThrownBy(() -> rule.validate(source, destination, Camp.CHO, board, PieceRule.CANNON)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.SAME_PIECE_TYPE_AT_DESTINATION.getMessage()); + } +} diff --git a/src/test/java/janggi/domain/piece/strategy/SingleStepRuleTest.java b/src/test/java/janggi/domain/piece/strategy/SingleStepRuleTest.java new file mode 100644 index 0000000000..7cd3c1264e --- /dev/null +++ b/src/test/java/janggi/domain/piece/strategy/SingleStepRuleTest.java @@ -0,0 +1,104 @@ +package janggi.domain.piece.strategy; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import janggi.domain.Position; +import janggi.domain.piece.Camp; +import janggi.domain.piece.PieceRule; +import janggi.exception.ExceptionMessage; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class SingleStepRuleTest { + + private static final int SINGLE_STEP_DISTANCE = 1; + + @DisplayName("전방향 1칸 이동(궁, 사) 테스트") + @Nested + class AllDirectionStep { + + private final MoveRule rule = new SingleStepRule(false); + + private static Stream successMovePositions() { + return Stream.of( + Arguments.of(new Position(1, 4), new Position(1, 5)), + Arguments.of(new Position(1, 4), new Position(1, 3)), + Arguments.of(new Position(1, 4), new Position(0, 4)), + Arguments.of(new Position(1, 4), new Position(2, 4)) + ); + } + + @ParameterizedTest + @MethodSource("successMovePositions") + void 직선_방향으로_1칸만_이동한다(Position source, Position destination) { + assertThatNoException().isThrownBy(() -> rule.validate(source, destination, Camp.HAN, null, PieceRule.GENERAL)); + } + + @Test + void 직선_방향으로_1칸만_이동하지_않으면_예외가_발생한다() { + assertThatThrownBy(() -> rule.validate(new Position(3, 0), new Position(5, 0), Camp.HAN, null, PieceRule.GENERAL)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_SINGLE_STEP_MOVE.getMessage(SINGLE_STEP_DISTANCE)); + } + } + + @DisplayName("전진 및 좌우 1칸 이동(졸, 병) 테스트") + @Nested + class ForwardSideStep { + + private final MoveRule rule = new SingleStepRule(true); + + private static Stream successMovePositions() { + return Stream.of( + Arguments.of(Camp.HAN, new Position(6, 0), new Position(5, 0)), + Arguments.of(Camp.HAN, new Position(6, 0), new Position(6, 1)), + Arguments.of(Camp.HAN, new Position(6, 1), new Position(6, 0)), + Arguments.of(Camp.CHO, new Position(3, 0), new Position(4, 0)), + Arguments.of(Camp.CHO, new Position(3, 0), new Position(3, 1)), + Arguments.of(Camp.CHO, new Position(3, 1), new Position(3, 0)) + ); + } + + @ParameterizedTest + @MethodSource("successMovePositions") + void 전진_또는_좌우_방향으로_1칸만_이동한다(Camp camp, Position source, Position destination) { + assertThatNoException().isThrownBy(() -> rule.validate(source, destination, camp, null, PieceRule.SOLDIER)); + } + + private static Stream invalidDistancePositions() { + return Stream.of( + Arguments.of(Camp.HAN, new Position(6, 0), new Position(4, 0)), + Arguments.of(Camp.CHO, new Position(3, 0), new Position(3, 6)) + ); + } + + @ParameterizedTest + @MethodSource("invalidDistancePositions") + void 직선_방향으로_1칸만_이동하지_않으면_예외가_발생한다(Camp camp, Position source, Position destination) { + assertThatThrownBy(() -> rule.validate(source, destination, camp, null, PieceRule.SOLDIER)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_SINGLE_STEP_MOVE.getMessage(SINGLE_STEP_DISTANCE)); + } + + private static Stream backwardMovePositions() { + return Stream.of( + Arguments.of(Camp.HAN, new Position(6, 0), new Position(7, 0)), + Arguments.of(Camp.CHO, new Position(3, 0), new Position(2, 0)) + ); + } + + @ParameterizedTest + @MethodSource("backwardMovePositions") + void 후진하는_경우_예외가_발생한다(Camp camp, Position source, Position destination) { + assertThatThrownBy(() -> rule.validate(source, destination, camp, null, PieceRule.SOLDIER)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ExceptionMessage.INVALID_BACKWARD_MOVEMENT.getMessage()); + } + } +} diff --git a/src/test/java/janggi/util/ParserTest.java b/src/test/java/janggi/util/ParserTest.java new file mode 100644 index 0000000000..3a5769c301 --- /dev/null +++ b/src/test/java/janggi/util/ParserTest.java @@ -0,0 +1,24 @@ +package janggi.util; + + +import java.util.List; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; + +class ParserTest { + + @Test + void 구분자를_기준으로_문자열을_분리한다() { + // given + String expression = "10,20"; + String delimiter = ","; + // when + List parsedExpression = Parser.parseByDelimiter(delimiter, expression); + // then + SoftAssertions.assertSoftly(assertSoftly -> { + assertSoftly.assertThat(parsedExpression).hasSize(2); + assertSoftly.assertThat(parsedExpression.getFirst()).isEqualTo(10); + assertSoftly.assertThat(parsedExpression.getLast()).isEqualTo(20); + }); + } +} diff --git a/src/test/java/janggi/view/format/ElephantSetUpFormatTest.java b/src/test/java/janggi/view/format/ElephantSetUpFormatTest.java new file mode 100644 index 0000000000..41b85a4f40 --- /dev/null +++ b/src/test/java/janggi/view/format/ElephantSetUpFormatTest.java @@ -0,0 +1,23 @@ +package janggi.view.format; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ElephantSetUpFormatTest { + + @ParameterizedTest + @CsvSource(value = { + "1, LEFT_ELEPHANT", + "2, RIGHT_ELEPHANT", + "3, INNER_ELEPHANT", + "4, OUTER_ELEPHANT" + }) + void 명령어와_매칭되는_상차림을_반환한다(String command, ElephantSetUpFormat expectedElephantSetUpFormat) { + // when + ElephantSetUpFormat result = ElephantSetUpFormat.findElephantSettingBy(command); + // then + assertThat(result).isEqualTo(expectedElephantSetUpFormat); + } +}