diff --git a/README.md b/README.md index 9775dda0ae..9003d49411 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,186 @@ -# java-janggi +# 1단계 - 보드 초기화 -장기 미션 저장소 +``` +한나라의 상차림을 선택해주세요. +1. 상마상마 +2. 마상마상 +3. 마상상마 +4. 상마마상 + +3 + +초나라의 상차림을 선택해주세요. +1. 상마상마 +2. 마상마상 +3. 마상상마 +4. 상마마상 + +5 +[ERROR] 유효하지 않은 상차림 번호입니다. + +3 + +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 馬 象 士 * 士 象 馬 車 | +1| * * * * 漢 * * * * | +2| * 包 * * * * * 包 * | +3| 兵 * 兵 * 兵 * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 * 卒 * 卒 * 卒 | +7| * 包 * * * * * 包 * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 馬 車 | + +-------------------+ +``` + +## 기능 정리 + +- 상차림 순서 관리 + - 한나라부터 상차림 입력을 받는다. + - 다음으로 초나라 상차림 입력을 받는다. + - 잘못된 입력시 예외 +- 상차림 별 상태 관리 + - 상마상마 + - 마상마상 + - 마상상마 + - 상마마상 +- 초기 배치를 위한 보드 정보 + - 위치 + - 격자 범위 + - 배치될 장기말 +- 장기말 정보 + - 차, 마, 상, 사, 졸(병), 포, 장 + - 자신의 나라 정보 +- 나라 정보 + - 한, 초 + +## 구현 순서 + +* 초나라/한나라 상차림 입력 + * 나라 별 유효한 상차림을 입력받는다. +* 상차림에 따른 장기판 초기 배치 + * 장기판에 위치할 장기말이 필요하다. + * 차, 상, 마, 포, 졸, 사, 장 + * 이름 + 정보: [나무위키](https://namu.wiki/w/%EC%9E%A5%EA%B8%B0/%EA%B8%B0%EB%AC%BC%20%EB%B0%8F%20%ED%96%89%EB%A7%88%EB%B2%95) + 참조 + * 장기말이 위치할 위치 정보가 필요하다. + * 장기판 위치 범위를 초과하면 예외가 발생해야한다. + * 장기판이 필요하다. + +# 2단계 - 기물 이동 + +- 2단계에서는 기물 이동 구현이 목적이니까 종료는 `Ctrl+C`로 강제 종료한다. + +## 잘못된 기물 선택 시나리오 + +1. 다른 나라의 기물을 선택했을 때 + 1. 예외: 다른나라의 기물 선택 + 2. `[ERROR] 다른 나라의 기물을 선택할 수 없습니다.` +2. 기물이 없는 곳을 선택했다. (빈칸 or 장기판 외부) + 1. 예외: 기물이 없는 곳을 선택 + 2. `[ERROR] 현재 위치에 존재하는 기물이 없습니다.` + +## 기물 이동 규칙 + +- 궁성 영역 - 고려하지 않는다. (사이클 2) +- 기물 이동 규칙 공통 + - 현재 위치로 이동할 수 없다. + - 보드 영역 밖으로 이동할 수 없다. + - 목적지에 아군이 있으면 이동할 수 없다. + - 목적지에 적군이 있으면 잡아낸다. + +- `차`: 4방위로 이동한다. + - 목적지까지의 경로에 기물이 있으면 이동할 수 없다. +- `포`: 4방위로 이동한다. + - 무조건 목적지까지의 경로에 포를 제외한 기물이 하나만 존재해야 한다. +- `마`: 4방위 중 한 방향으로 한 칸 이동하고 대각선의 한 방향으로 1칸 이동해야 한다. + - 목적지까지의 경로에 기물이 있으면 이동할 수 없다. +- `상`: 4방위 중 한 방향으로 한 칸 이동하고 대각선의 한 방향으로 2칸 이동해야 한다. + - 목적지까지의 경로에 기물이 있으면 이동할 수 없다. +- `졸`: 직진과 좌, 우로만 한 칸 이동해야 한다. + - 한 칸이라서 경로에 기물이 있을 리가 없다. +- `사, 장`: 궁성 영역이므로 구현하지 않는다. + +## 입출력 예시 + +``` +1단계 ... + +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 馬 象 士 * 士 象 馬 車 | +1| * * * * 漢 * * * * | +2| * 包 * * * * * 包 * | +3| 兵 * 兵 * 兵 * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 * 卒 * 卒 * 卒 | +7| * 包 * * * * * 包 * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 馬 車 | + +-------------------+ + +[초나라] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리) +기물: 6,8 + +[초나라] 기물 卒의 다음 위치를 선택해주세요. (쉼표 기준으로 분리) +위치: 6,7 + + +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 馬 象 士 * 士 象 馬 車 | +1| * * * * 漢 * * * * | +2| * 包 * * * * * 包 * | +3| 兵 * 兵 * 兵 * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 * 卒 * 卒 卒 * | +7| * 包 * * * * * 包 * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 馬 車 | + +-------------------+ + +[한나라] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리) +기물: 3,1 + +[ERROR] 현재 위치에 존재하는 기물이 없습니다. + + +[한나라] 이동할 기물을 선택해주세요. (쉼표 기준으로 분리) +기물: 3,0 + + +[한나라] 기물 兵의 다음 위치를 선택해주세요. (쉼표 기준으로 분리) +위치: 3,1 + +   0 1 2 3 4 5 6 7 8 + +-------------------+ +0| 車 馬 象 士 * 士 象 馬 車 | +1| * * * * 漢 * * * * | +2| * 包 * * * * * 包 * | +3| * 兵 兵 * 兵 * 兵 * 兵 | +4| * * * * * * * * * | +5| * * * * * * * * * | +6| 卒 * 卒 * 卒 * 卒 卒 * | +7| * 包 * * * * * 包 * | +8| * * * * 楚 * * * * | +9| 車 馬 象 士 * 士 象 馬 車 | + +-------------------+ +``` + +## 구현 순서 + +1. 기물 이동 기능 추가 + * 나라 별 입력 + * 이동할 기물의 위치를 입력 받는다. + * 해당 위치에 기물이 존재하지 않다면 예외가 발생한다. + * 해당 위치의 기물이 다른 팀이라면 예외가 발생한다. + * 잘못된 범위라면 예외를 발생한다. (보드 범위 벗어남) + * 기물의 이동할 위치를 입력 받는다. + * 잘못된 범위라면 예외를 발생한다. + * 해당 위치에 아군의 기물이 존재한다면 예외가 발생한다. + * 기물 별로 이동할 수 없는 목적지라면 예외가 발생한다. \ No newline at end of file diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 0000000000..c4e80ce0a4 --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,10 @@ +import controller.JanggiController; +import view.InputView; +import view.OutputView; + +public class Application { + public static void main(String[] args) { + JanggiController janggiController = new JanggiController(new InputView(), new OutputView()); + janggiController.run(); + } +} diff --git a/src/main/java/controller/JanggiController.java b/src/main/java/controller/JanggiController.java new file mode 100644 index 0000000000..12f5c8d293 --- /dev/null +++ b/src/main/java/controller/JanggiController.java @@ -0,0 +1,61 @@ +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/controller/Retrier.java b/src/main/java/controller/Retrier.java new file mode 100644 index 0000000000..76e3f8e2c5 --- /dev/null +++ b/src/main/java/controller/Retrier.java @@ -0,0 +1,31 @@ +package controller; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public final class Retrier { + private Retrier() { + } + + public static T retry(Supplier task, Consumer consumer) { + while (true) { + try { + return task.get(); + } catch (IllegalArgumentException e) { + consumer.accept(e); + } + } + } + + + public static void retry(Runnable task, Consumer consumer) { + while (true) { + try { + task.run(); + return; + } catch (IllegalArgumentException e) { + consumer.accept(e); + } + } + } +} diff --git a/src/main/java/model/JanggiGame.java b/src/main/java/model/JanggiGame.java new file mode 100644 index 0000000000..f68211fdb6 --- /dev/null +++ b/src/main/java/model/JanggiGame.java @@ -0,0 +1,39 @@ +package model; + +import java.util.List; +import model.board.Board; +import model.board.Position; +import model.piece.Piece; + +public class JanggiGame { + + private final Board board; + private Team turn; + + public JanggiGame(Board board) { + this.board = board; + this.turn = Team.CHO; + } + + public void movePiece(Position current, Position next) { + Piece piece = selectPiece(current); + + List path = piece.extractPath(current, next); + List pieces = board.extractPiecesByPath(path); + + piece.validatePathCondition(pieces); + board.move(current, next); + + this.turn = turn.next(); + } + + public Piece selectPiece(Position position) { + Piece piece = board.pickPiece(position); + turn.validateAlly(piece); + return piece; + } + + public Team getTurn() { + return turn; + } +} diff --git a/src/main/java/model/Team.java b/src/main/java/model/Team.java new file mode 100644 index 0000000000..68f566eade --- /dev/null +++ b/src/main/java/model/Team.java @@ -0,0 +1,34 @@ +package model; + +import model.piece.Piece; + +public enum Team { + HAN("한나라"), CHO("초나라"); + + private final String name; + + Team(String name) { + this.name = name; + } + + public void validateAlly(Piece piece) { + if (piece.isOtherTeam(this)) { + throw new IllegalArgumentException(this.name + "의 기물이 아닙니다."); + } + } + + public boolean isHan() { + return this == HAN; + } + + public Team next() { + if (isHan()) { + return CHO; + } + return HAN; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/model/board/Board.java b/src/main/java/model/board/Board.java new file mode 100644 index 0000000000..8abf94644a --- /dev/null +++ b/src/main/java/model/board/Board.java @@ -0,0 +1,55 @@ +package model.board; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import model.piece.Piece; + +public class Board { + + public static final int BOARD_ROW = 10; + public static final int BOARD_COL = 9; + + private final Map board; + + public Board(Map board) { + this.board = new HashMap<>(board); + } + + public void move(Position current, Position next) { + Piece piece = pickPiece(current); + findByPosition(next).ifPresent(piece::validateTarget); + + board.remove(current); + board.put(next, piece); + } + + public Piece pickPiece(Position position) { + return findByPosition(position) + .orElseThrow(() -> new IllegalArgumentException("해당 위치에 존재하는 장기말이 없습니다.")); + } + + public void arrangePieces(Map pieces) { + board.putAll(pieces); + } + + public List extractPiecesByPath(List path) { + return path.stream() + .filter(this::hasPieceAt) + .map(this::pickPiece) + .toList(); + } + + private Optional findByPosition(Position position) { + return Optional.ofNullable(board.get(position)); + } + + public Map board() { + return Map.copyOf(board); + } + + private boolean hasPieceAt(Position position) { + return board.containsKey(position); + } +} diff --git a/src/main/java/model/board/BoardFactory.java b/src/main/java/model/board/BoardFactory.java new file mode 100644 index 0000000000..b89731725c --- /dev/null +++ b/src/main/java/model/board/BoardFactory.java @@ -0,0 +1,53 @@ +package model.board; + +import static model.Team.CHO; +import static model.Team.HAN; + +import java.util.HashMap; +import java.util.Map; +import model.piece.Cannon; +import model.piece.Chariot; +import model.piece.General; +import model.piece.Guard; +import model.piece.Piece; +import model.piece.Soldier; + +public class BoardFactory { + + private static final Map HAN_PIECES = Map.ofEntries( + Map.entry(new Position(0, 0), new Chariot(HAN)), + Map.entry(new Position(0, 3), new Guard(HAN)), + Map.entry(new Position(0, 5), new Guard(HAN)), + Map.entry(new Position(0, 8), new Chariot(HAN)), + Map.entry(new Position(1, 4), new General(HAN)), + Map.entry(new Position(2, 1), new Cannon(HAN)), + Map.entry(new Position(2, 7), new Cannon(HAN)), + Map.entry(new Position(3, 0), new Soldier(HAN)), + Map.entry(new Position(3, 2), new Soldier(HAN)), + Map.entry(new Position(3, 4), new Soldier(HAN)), + Map.entry(new Position(3, 6), new Soldier(HAN)), + Map.entry(new Position(3, 8), new Soldier(HAN)) + ); + + private static final Map CHO_PIECES = Map.ofEntries( + Map.entry(new Position(9, 0), new Chariot(CHO)), + Map.entry(new Position(9, 3), new Guard(CHO)), + Map.entry(new Position(9, 5), new Guard(CHO)), + Map.entry(new Position(9, 8), new Chariot(CHO)), + Map.entry(new Position(8, 4), new General(CHO)), + Map.entry(new Position(7, 1), new Cannon(CHO)), + Map.entry(new Position(7, 7), new Cannon(CHO)), + Map.entry(new Position(6, 0), new Soldier(CHO)), + Map.entry(new Position(6, 2), new Soldier(CHO)), + Map.entry(new Position(6, 4), new Soldier(CHO)), + Map.entry(new Position(6, 6), new Soldier(CHO)), + Map.entry(new Position(6, 8), new Soldier(CHO)) + ); + + public static Board generateDefaultPieces() { + Map allPieces = new HashMap<>(); + allPieces.putAll(HAN_PIECES); + allPieces.putAll(CHO_PIECES); + return new Board(allPieces); + } +} diff --git a/src/main/java/model/board/FormationStrategy.java b/src/main/java/model/board/FormationStrategy.java new file mode 100644 index 0000000000..9e6c2af3d3 --- /dev/null +++ b/src/main/java/model/board/FormationStrategy.java @@ -0,0 +1,10 @@ +package model.board; + +import java.util.List; +import model.Team; +import model.piece.Piece; + +public interface FormationStrategy { + + List generateByTeam(Team team); +} diff --git a/src/main/java/model/board/JanggiFormation.java b/src/main/java/model/board/JanggiFormation.java new file mode 100644 index 0000000000..b3bf475b70 --- /dev/null +++ b/src/main/java/model/board/JanggiFormation.java @@ -0,0 +1,50 @@ +package model.board; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import model.Team; +import model.piece.Elephant; +import model.piece.Horse; +import model.piece.Piece; + +public enum JanggiFormation { + 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))), + SANG_MA_MA_SANG(team -> List.of(new Elephant(team), new Horse(team), new Horse(team), new Elephant(team))); + + private static final Position HAN_LEFT_OUTER = new Position(0, 1); + private static final Position HAN_LEFT_INNER = new Position(0, 2); + private static final Position HAN_RIGHT_INNER = new Position(0, 6); + private static final Position HAN_RIGHT_OUTER = new Position(0, 7); + + private static final Position CHO_LEFT_OUTER = new Position(9, 1); + private static final Position CHO_LEFT_INNER = new Position(9, 2); + private static final Position CHO_RIGHT_INNER = new Position(9, 6); + private static final Position CHO_RIGHT_OUTER = new Position(9, 7); + + private final FormationStrategy strategy; + + JanggiFormation(FormationStrategy strategy) { + this.strategy = strategy; + } + + private static List extractPositionsByTeam(Team team) { + if (team == Team.HAN) { + return List.of(HAN_LEFT_OUTER, HAN_LEFT_INNER, HAN_RIGHT_INNER, HAN_RIGHT_OUTER); + } + return List.of(CHO_LEFT_OUTER, CHO_LEFT_INNER, CHO_RIGHT_INNER, CHO_RIGHT_OUTER); + } + + public Map generateByTeam(Team team) { + List positions = extractPositionsByTeam(team); + List pieces = strategy.generateByTeam(team); + + Map formation = new HashMap<>(); + for (int i = 0; i < positions.size(); i++) { + formation.put(positions.get(i), pieces.get(i)); + } + return formation; + } +} diff --git a/src/main/java/model/board/Position.java b/src/main/java/model/board/Position.java new file mode 100644 index 0000000000..b2f1da040f --- /dev/null +++ b/src/main/java/model/board/Position.java @@ -0,0 +1,35 @@ +package model.board; + +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 { + if (row < 0 || row >= BOARD_ROW) { + throw new IllegalArgumentException("장기판의 행 범위를 벗어났습니다. 최대 범위: " + BOARD_ROW); + } + + if (col < 0 || col >= BOARD_COL) { + throw new IllegalArgumentException("장기판의 열 범위를 벗어났습니다. 최대 범위: " + BOARD_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 Position resolveNext(int rowDistance, int colDistance) { + return new Position(this.row + rowDistance, this.col + colDistance); + } +} diff --git a/src/main/java/model/movement/Direction.java b/src/main/java/model/movement/Direction.java new file mode 100644 index 0000000000..524c07c3f6 --- /dev/null +++ b/src/main/java/model/movement/Direction.java @@ -0,0 +1,46 @@ +package model.movement; + +import java.util.stream.Stream; +import model.board.Position; + +public enum Direction { + EAST(0, 1), + WEST(0, -1), + NORTH(-1, 0), + SOUTH(1, 0), + NORTH_EAST(-1, 1), + SOUTH_EAST(1, 1), + NORTH_WEST(-1, -1), + SOUTH_WEST(1, -1); + + private final int row; + private final int col; + + Direction(int row, int col) { + this.row = row; + this.col = col; + } + + public static Direction of(int rowDiff, int colDiff) { + return Stream.of(values()) + .filter(direction -> direction.isSameDirection(rowDiff, colDiff)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("제자리는 이동할 수 없는 방향입니다.")); + } + + 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 int col() { + return col; + } +} diff --git a/src/main/java/model/movement/Displacement.java b/src/main/java/model/movement/Displacement.java new file mode 100644 index 0000000000..cf8d3ae4f1 --- /dev/null +++ b/src/main/java/model/movement/Displacement.java @@ -0,0 +1,40 @@ +package model.movement; + +public record Displacement(int rowDiff, int colDiff) { + + public Direction extractCardinal() { + if (Math.abs(rowDiff) > Math.abs(colDiff)) { + return Direction.of(rowDiff, 0); + } + return Direction.of(0, colDiff); + } + + 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; + } + + public boolean isNotStepCombination(int longStep, int shortStep) { + return (absRowDiff() != longStep || absColDiff() != shortStep) && + (absRowDiff() != shortStep || absColDiff() != longStep); + } + + public boolean isForwardBy(int forwardCount) { + return rowDiff == forwardCount && colDiff == 0; + } + + public boolean isSideOneStep() { + return rowDiff == 0 && absColDiff() == 1; + } +} diff --git a/src/main/java/model/movement/LinearStrategy.java b/src/main/java/model/movement/LinearStrategy.java new file mode 100644 index 0000000000..26668460e3 --- /dev/null +++ b/src/main/java/model/movement/LinearStrategy.java @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000000..ea10f61c19 --- /dev/null +++ b/src/main/java/model/movement/MoveStrategy.java @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000000..5e2648cac3 --- /dev/null +++ b/src/main/java/model/movement/OneStepStrategy.java @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000000..6448585dff --- /dev/null +++ b/src/main/java/model/movement/SteppingStrategy.java @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000000..47f2b26260 --- /dev/null +++ b/src/main/java/model/piece/Cannon.java @@ -0,0 +1,43 @@ +package model.piece; + +import java.util.List; +import model.Team; +import model.board.Position; +import model.movement.Displacement; + +public class Cannon extends Piece { + + private static final int CANNON_HURDLE_COUNT = 1; + + public Cannon(Team team) { + super(team, PieceType.CANNON); + } + + @Override + public void validatePathCondition(List pieces) { + if (pieces.size() != CANNON_HURDLE_COUNT) { + throw new IllegalArgumentException("포는 정확히 하나의 기물을 뛰어넘어야 합니다."); + } + + boolean hasCannonAsHurdle = pieces.stream().anyMatch(Piece::isCannon); + if (hasCannonAsHurdle) { + throw new IllegalArgumentException("포는 포를 다리로 쓸 수 없습니다."); + } + } + + @Override + public void validateTarget(Piece otherPiece) { + super.validateTarget(otherPiece); + if (otherPiece.isCannon()) { + 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 new file mode 100644 index 0000000000..619f83ae32 --- /dev/null +++ b/src/main/java/model/piece/Chariot.java @@ -0,0 +1,20 @@ +package model.piece; + +import model.Team; +import model.board.Position; +import model.movement.Displacement; + +public class Chariot extends Piece { + + 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 new file mode 100644 index 0000000000..15da27c1e3 --- /dev/null +++ b/src/main/java/model/piece/Elephant.java @@ -0,0 +1,23 @@ +package model.piece; + +import model.Team; +import model.board.Position; +import model.movement.Displacement; + +public class Elephant extends Piece { + + private static final int ELEPHANT_LONG_STEP = 3; + private static final int ELEPHANT_SHORT_STEP = 2; + + public Elephant(Team team) { + super(team, PieceType.ELEPHANT); + } + + @Override + protected void validateMove(Position current, Position next) { + Displacement displacement = next.minus(current); + if (displacement.isNotStepCombination(ELEPHANT_LONG_STEP, ELEPHANT_SHORT_STEP)) { + throw new IllegalArgumentException("상이 이동할 수 없는 위치입니다."); + } + } +} diff --git a/src/main/java/model/piece/General.java b/src/main/java/model/piece/General.java new file mode 100644 index 0000000000..c0db5579e8 --- /dev/null +++ b/src/main/java/model/piece/General.java @@ -0,0 +1,16 @@ +package model.piece; + +import model.Team; +import model.board.Position; + +public class General extends Piece { + + 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 new file mode 100644 index 0000000000..9c635738f1 --- /dev/null +++ b/src/main/java/model/piece/Guard.java @@ -0,0 +1,16 @@ +package model.piece; + +import model.Team; +import model.board.Position; + +public class Guard extends Piece { + + 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 new file mode 100644 index 0000000000..cd34d14434 --- /dev/null +++ b/src/main/java/model/piece/Horse.java @@ -0,0 +1,23 @@ +package model.piece; + +import model.Team; +import model.board.Position; +import model.movement.Displacement; + +public class Horse extends Piece { + + private static final int HORSE_LONG_STEP = 2; + private static final int HORSE_SHORT_STEP = 1; + + public Horse(Team team) { + super(team, PieceType.HORSE); + } + + @Override + protected void validateMove(Position current, Position next) { + Displacement displacement = next.minus(current); + if (displacement.isNotStepCombination(HORSE_LONG_STEP, HORSE_SHORT_STEP)) { + throw new IllegalArgumentException("마가 이동할 수 없는 위치입니다."); + } + } +} diff --git a/src/main/java/model/piece/Piece.java b/src/main/java/model/piece/Piece.java new file mode 100644 index 0000000000..f6cea5ff83 --- /dev/null +++ b/src/main/java/model/piece/Piece.java @@ -0,0 +1,55 @@ +package model.piece; + +import java.util.List; +import model.Team; +import model.board.Position; + +public abstract class Piece { + + private final Team team; + private final PieceType type; + + protected Piece(Team team, PieceType type) { + this.team = team; + this.type = type; + } + + public List extractPath(Position current, Position next) { + validateMove(current, next); + return type.extractPath(current, next); + } + + public boolean isOtherTeam(Team team) { + return this.team != team; + } + + public void validatePathCondition(List pieces) { + if (!pieces.isEmpty()) { + throw new IllegalArgumentException("이동 경로에 기물이 있어 이동할 수 없는 위치입니다."); + } + } + + public void validateTarget(Piece otherPiece) { + if (getTeam() == otherPiece.team) { + throw new IllegalArgumentException("아군이 있는 위치로 이동할 수 없습니다."); + } + } + + protected abstract void validateMove(Position current, Position next); + + protected boolean isCho() { + return !team.isHan(); + } + + protected boolean isCannon() { + return getType() == PieceType.CANNON; + } + + public Team getTeam() { + return team; + } + + public PieceType getType() { + return type; + } +} diff --git a/src/main/java/model/piece/PieceType.java b/src/main/java/model/piece/PieceType.java new file mode 100644 index 0000000000..c2afe70b14 --- /dev/null +++ b/src/main/java/model/piece/PieceType.java @@ -0,0 +1,34 @@ +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 { + + 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; + + PieceType(MoveStrategy moveStrategy) { + this.moveStrategy = moveStrategy; + } + + public List extractPath(Position current, Position next) { + return moveStrategy.extractPath(current, next); + } +} diff --git a/src/main/java/model/piece/Soldier.java b/src/main/java/model/piece/Soldier.java new file mode 100644 index 0000000000..f5e21fc7c7 --- /dev/null +++ b/src/main/java/model/piece/Soldier.java @@ -0,0 +1,32 @@ +package model.piece; + +import model.Team; +import model.board.Position; +import model.movement.Displacement; + +public class Soldier extends Piece { + + private static final int SOLDIER_FORWARD_STEP = 1; + + public Soldier(Team team) { + super(team, PieceType.SOLDIER); + } + + @Override + protected void validateMove(Position current, Position next) { + Displacement displacement = next.minus(current); + int forwardCount = resolveForwardCount(); + + if (!(displacement.isForwardBy(forwardCount) || displacement.isSideOneStep())) { + throw new IllegalArgumentException("졸이 이동할 수 없는 위치입니다."); + } + } + + private int resolveForwardCount() { + if (isCho()) { + return -SOLDIER_FORWARD_STEP; + } + return SOLDIER_FORWARD_STEP; + } + +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 0000000000..4a746f5c24 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,52 @@ +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 new file mode 100644 index 0000000000..dbbe2be93a --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,52 @@ +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/main/java/view/formater/BoardFormatter.java b/src/main/java/view/formater/BoardFormatter.java new file mode 100644 index 0000000000..a01a712350 --- /dev/null +++ b/src/main/java/view/formater/BoardFormatter.java @@ -0,0 +1,46 @@ +package view.formater; + +import static view.mapper.ViewMapper.SYMBOL_MAP; + +import model.Team; +import model.piece.Piece; + +public class BoardFormatter { + + public static final String RED = "\u001B[31m"; + public static final String GREEN = "\u001B[32m"; + public static final String RESET = "\u001B[0m"; + + public static final String SPACE = " "; + public static final String VERTICAL_LINE = "|"; + + public static final String[] COL_NUM = {"0", "1", "2", "3", "4", "5", "6", "7", "8"}; + public static final String[] ROW_NUM = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; + + private static final String HORIZON_LINE = "-"; + private static final String CORNER = "+"; + private static final String EMPTY = "*"; + + private BoardFormatter() { + } + + public static String formatHorizon(int colSize) { + return SPACE + " " + CORNER + HORIZON_LINE.repeat(colSize * 2 + 1) + CORNER; + } + + 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()); + return color + symbol + RESET; + } + + private static String extractColor(Team team) { + if (team == Team.HAN) { + return RED; + } + return GREEN; + } +} diff --git a/src/main/java/view/mapper/ViewMapper.java b/src/main/java/view/mapper/ViewMapper.java new file mode 100644 index 0000000000..04b5ebc991 --- /dev/null +++ b/src/main/java/view/mapper/ViewMapper.java @@ -0,0 +1,44 @@ +package view.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 java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.Map; +import model.Team; +import model.board.JanggiFormation; +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_DISPLAY_MAPPER = Map.of( + SANG_MA_SANG_MA, "상마상마", + MA_SANG_MA_SANG, "마상마상", + MA_SANG_SANG_MA, "마상상마", + SANG_MA_MA_SANG, "상마마상" + ); + + static { + SYMBOL_MAP.put(PieceType.CHARIOT, Map.of(Team.HAN, "車", Team.CHO, "車")); + SYMBOL_MAP.put(PieceType.CANNON, Map.of(Team.HAN, "包", Team.CHO, "包")); + SYMBOL_MAP.put(PieceType.GENERAL, Map.of(Team.HAN, "漢", Team.CHO, "楚")); + SYMBOL_MAP.put(PieceType.HORSE, Map.of(Team.HAN, "馬", Team.CHO, "馬")); + SYMBOL_MAP.put(PieceType.ELEPHANT, Map.of(Team.HAN, "象", Team.CHO, "象")); + SYMBOL_MAP.put(PieceType.GUARD, Map.of(Team.HAN, "士", Team.CHO, "士")); + SYMBOL_MAP.put(PieceType.SOLDIER, Map.of(Team.HAN, "兵", Team.CHO, "卒")); + + FORMATION_ORDER_MAPPER.put(1, SANG_MA_SANG_MA); + FORMATION_ORDER_MAPPER.put(2, MA_SANG_MA_SANG); + FORMATION_ORDER_MAPPER.put(3, MA_SANG_SANG_MA); + FORMATION_ORDER_MAPPER.put(4, SANG_MA_MA_SANG); + } + + private ViewMapper() { + } +} diff --git a/src/main/java/view/parser/InputParser.java b/src/main/java/view/parser/InputParser.java new file mode 100644 index 0000000000..8eeb8874f1 --- /dev/null +++ b/src/main/java/view/parser/InputParser.java @@ -0,0 +1,22 @@ +package view.parser; + +import java.util.Arrays; +import java.util.List; + +public class InputParser { + + public int parseNumber(String input) { + try { + return Integer.parseInt(input.strip()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("잘못된 입력입니다. 수를 입력해주세요: " + input); + } + } + + public List parseToken(String input, String delimiter) { + return Arrays.stream(input.strip() + .split(delimiter)) + .map(String::strip) + .toList(); + } +} diff --git a/src/test/java/model/JanggiGameTest.java b/src/test/java/model/JanggiGameTest.java new file mode 100644 index 0000000000..3dab3216e1 --- /dev/null +++ b/src/test/java/model/JanggiGameTest.java @@ -0,0 +1,58 @@ +package model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import model.board.Position; +import model.piece.Horse; +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); + + SpyBoard board = new SpyBoard(piece); + JanggiGame janggiGame = new JanggiGame(board); + Team prevTurn = janggiGame.getTurn(); + + // when + janggiGame.movePiece(source, destination); + + // then + assertThat(board.pickPiece(destination)).isEqualTo(piece); + assertThat(janggiGame.getTurn()).isEqualTo(prevTurn.next()); + } + + @Test + void 현재_턴의_팀에_맞는_기물을_선택할_수_있다() { + // given + Piece piece = new Horse(Team.CHO); + SpyBoard board = new SpyBoard(piece); + JanggiGame janggiGame = new JanggiGame(board); + + // when + Piece selectedPiece = janggiGame.selectPiece(new Position(1, 1)); + + // then + assertThat(selectedPiece).isSameAs(piece); + } + + @Test + void 다른_팀의_기물을_선택하면_예외가_발생한다() { + // given: CHO 턴인데 HAN 기물 배치 + SpyBoard board = new SpyBoard(new Horse(Team.HAN)); + JanggiGame janggiGame = new JanggiGame(board); + + // when & then + assertThatThrownBy(() -> janggiGame.selectPiece(new Position(1, 1))) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/model/TeamTest.java b/src/test/java/model/TeamTest.java new file mode 100644 index 0000000000..5f925e4cc0 --- /dev/null +++ b/src/test/java/model/TeamTest.java @@ -0,0 +1,46 @@ +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 { + + @Test + void 한나라_다음_차례는_초나라다() { + // given + Team han = Team.HAN; + + // when + Team next = han.next(); + + // then + assertThat(next).isEqualTo(Team.CHO); + } + + @Test + void 초나라_다음_차례는_한나라다() { + // given + Team cho = Team.CHO; + + // when + Team next = cho.next(); + + // 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 new file mode 100644 index 0000000000..f3dc059f55 --- /dev/null +++ b/src/test/java/model/board/BoardTest.java @@ -0,0 +1,123 @@ +package model.board; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.Map; +import model.Team; +import model.piece.Piece; +import model.testdouble.FakePiece; +import org.junit.jupiter.api.Test; + +class BoardTest { + + private Position source = new Position(5, 0); + private Position destination = new Position(5, 4); + + @Test + void 해당_위치에_기물이_있으면_반환한다() { + // given + FakePiece piece = FakePiece.createFake(Team.CHO); + Board board = new Board(Map.of(source, piece)); + + // when + Piece pick = board.pickPiece(source); + + // then + assertThat(pick).isEqualTo(piece); + } + + @Test + void 해당_위치에_기물이_없으면_예외가_발생한다() { + // given + Board board = new Board(Map.of()); + + // when & then + assertThatThrownBy(() -> board.pickPiece(source)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 보드에서_도착_위치에_기물이_없으면_이동한다() { + // given + FakePiece piece = FakePiece.createFake(Team.CHO); + + Board board = new Board(Map.of(source, piece)); + + // when + board.move(source, destination); + + // then + assertSuccessMoved(board, destination, piece, source); + } + + @Test + void 보드에서_도착_위치에_아군이_있으면_이동할_수_없다() { + // given + FakePiece piece = FakePiece.createFake(Team.CHO); + FakePiece otherPiece = FakePiece.createFake(Team.CHO); + Board board = new Board(Map.of(source, piece, destination, otherPiece)); + + // when & then + assertThatThrownBy(() -> board.move(source, destination)); + } + + @Test + void 보드에서_도착_위치에_적군이_있으면_이동한다() { + // given + FakePiece piece = FakePiece.createFake(Team.CHO); + FakePiece enemy = FakePiece.createFake(Team.HAN); + + Board board = new Board(Map.of(source, piece, destination, enemy)); + + // when + board.move(source, destination); + + // then + assertSuccessMoved(board, destination, piece, source); + } + + @Test + void 여러_기물을_한번에_배치한다() { + // given + Board board = new Board(Map.of()); + Position pos1 = new Position(0, 0); + Position pos2 = new Position(0, 1); + FakePiece piece1 = FakePiece.createFake(Team.CHO); + FakePiece piece2 = FakePiece.createFake(Team.HAN); + + // when + board.arrangePieces(Map.of(pos1, piece1, pos2, piece2)); + + // then + assertThat(board.pickPiece(pos1)).isEqualTo(piece1); + assertThat(board.pickPiece(pos2)).isEqualTo(piece2); + } + + @Test + void 경로상에_존재하는_기물들을_추출한다() { + // given + Position path1 = new Position(5, 1); + Position path2 = new Position(5, 2); // 경로에 기물이 없는 경우 + Position path3 = new Position(5, 3); + + FakePiece hurdle1 = FakePiece.createFake(Team.HAN); + FakePiece hurdle2 = FakePiece.createFake(Team.CHO); + + Board board = new Board(Map.of(path1, hurdle1, path3, hurdle2)); + List path = List.of(path1, path2, path3); + + // when + List extracted = board.extractPiecesByPath(path); + + // then + assertThat(extracted).hasSize(2) + .containsExactly(hurdle1, hurdle2); + } + + private void assertSuccessMoved(Board board, Position source, FakePiece piece, Position destination) { + assertThat(board.pickPiece(source)).isEqualTo(piece); + assertThat(board.board()).doesNotContainKey(destination); + } +} \ No newline at end of file diff --git a/src/test/java/model/board/DirectionTest.java b/src/test/java/model/board/DirectionTest.java new file mode 100644 index 0000000000..2d229e5bee --- /dev/null +++ b/src/test/java/model/board/DirectionTest.java @@ -0,0 +1,75 @@ +package model.board; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.stream.Stream; +import model.movement.Direction; +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 DirectionTest { + + private static Stream 좌표_차이에_대한_방향_도출_정보() { + return Stream.of( + // 사방위 (Cardinal) - 거리가 1보다 커도 부호만 맞으면 성공해야 함 + Arguments.of(-3, 0, Direction.NORTH), + Arguments.of(5, 0, Direction.SOUTH), + Arguments.of(0, -1, Direction.WEST), + Arguments.of(0, 2, Direction.EAST), + + // 사간방 (Diagonal) - 마/상/궁성 이동 관련 + Arguments.of(-1, 1, Direction.NORTH_EAST), + Arguments.of(2, 2, Direction.SOUTH_EAST), + Arguments.of(-10, -10, Direction.NORTH_WEST), + Arguments.of(3, -2, Direction.SOUTH_WEST) + ); + } + + private static Stream 방향에_따라_5comma5에서_다음_위치로_이동하는_정보() { + return Stream.of( + // 사방위 (Cardinal) + Arguments.of(Direction.NORTH, 4, 5), + Arguments.of(Direction.SOUTH, 6, 5), + Arguments.of(Direction.WEST, 5, 4), + Arguments.of(Direction.EAST, 5, 6), + + // 사간방 (Diagonal) + Arguments.of(Direction.NORTH_WEST, 4, 4), + Arguments.of(Direction.NORTH_EAST, 4, 6), + Arguments.of(Direction.SOUTH_WEST, 6, 4), + Arguments.of(Direction.SOUTH_EAST, 6, 6) + ); + } + + @ParameterizedTest(name = "rowDiff: {0}, colDiff: {1} => {2}") + @MethodSource("좌표_차이에_대한_방향_도출_정보") + void 좌표_차이를_기반으로_올바른_방향을_구할_수_있다(int rowDiff, int colDiff, Direction expected) { + // when + Direction actual = Direction.of(rowDiff, colDiff); + + // then + assertThat(actual).isEqualTo(expected); + } + + @Test + void 제자리_이동의_경우_방향이_아니다() { + assertThatThrownBy(() -> Direction.of(0, 0)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest(name = "{0} 방향으로 이동: (5,5) -> ({1}, {2})") + @MethodSource("방향에_따라_5comma5에서_다음_위치로_이동하는_정보") + void 위치_정보에서_방향을_통해_이동한_위치를_구할_수_있다(Direction direction, int expectedRow, int expectedCol) { + // given + Position start = new Position(5, 5); + + // when + Position next = direction.move(start); + + // then + assertThat(next).isEqualTo(new Position(expectedRow, expectedCol)); + } +} \ No newline at end of file diff --git a/src/test/java/model/board/JanggiFormationTest.java b/src/test/java/model/board/JanggiFormationTest.java new file mode 100644 index 0000000000..41888b4a09 --- /dev/null +++ b/src/test/java/model/board/JanggiFormationTest.java @@ -0,0 +1,84 @@ +package model.board; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import java.util.stream.Stream; +import model.Team; +import model.piece.Elephant; +import model.piece.Horse; +import model.piece.Piece; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +class JanggiFormationTest { + + 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) + ); + } + + 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) + ); + } + + static Stream choFormationProvider() { + return 포메이션_별_기물_위치(); + } + + @ParameterizedTest(name = "{0} 차림 일 때") + @MethodSource("포메이션_별_기물_위치") + void 한나라_상차림_기물_순서_테스트( + JanggiFormation formation, + Class leftOuter, + Class leftInner, + Class rightInner, + Class rightOuter + ) { + Map result = formation.generateByTeam(Team.HAN); + + assertThat(result.get(new Position(0, 1))).isInstanceOf(leftOuter); + assertThat(result.get(new Position(0, 2))).isInstanceOf(leftInner); + assertThat(result.get(new Position(0, 6))).isInstanceOf(rightInner); + assertThat(result.get(new Position(0, 7))).isInstanceOf(rightOuter); + } + + @ParameterizedTest(name = "{0} 차림일 때") + @MethodSource("choFormationProvider") + void 초나라_상차림_기물_순서_테스트( + JanggiFormation formation, + Class leftOuter, + Class leftInner, + Class rightInner, + Class rightOuter + ) { + Map result = formation.generateByTeam(Team.CHO); + + assertThat(result.get(new Position(9, 1))).isInstanceOf(leftOuter); + assertThat(result.get(new Position(9, 2))).isInstanceOf(leftInner); + assertThat(result.get(new Position(9, 6))).isInstanceOf(rightInner); + assertThat(result.get(new Position(9, 7))).isInstanceOf(rightOuter); + } + + @ParameterizedTest(name = "{1}나라 일 때 {0}포메이션에서 기물 수는 총 4개다.") + @CsvSource({ + "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) { + // 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 new file mode 100644 index 0000000000..9148ae7a52 --- /dev/null +++ b/src/test/java/model/board/PositionTest.java @@ -0,0 +1,64 @@ +package model.board; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import model.movement.Displacement; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class PositionTest { + + @ParameterizedTest + @CsvSource({ + "0, 0", // 장기판 최소 좌표 꼭짓점 + "9, 8" // 장기판 최대 좌표 꼭짓점 + }) + void 장기판_위치가_유효한_범위라면_정상적으로_생성할_수_있다(int row, int col) { + // when + Position position = new Position(row, col); + + // then + assertThat(position.row()).isEqualTo(row); + assertThat(position.col()).isEqualTo(col); + } + + @ParameterizedTest + @CsvSource({ + "-1, 0", // 행이 상측으로 벗어난 경우 + "10, 0", // 행이 하측으로 벗어난 경우 + "0, -1", // 열이 좌측으로 벗어난 경우 + "0, 9" // 열이 우측으로 벗어난 경우 + }) + void 장기판_위치가_유효하지_않다면_예외가_발생한다(int row, int col) { + assertThatThrownBy(() -> new Position(row, col)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 두_위치가_주어진다면_두_위치에_대한_변위를_구할_수_있다() { + // given + Position start = new Position(5, 5); + Position end = new Position(3, 7); + + // when + Displacement result = end.minus(start); + + // then + assertThat(result.rowDiff()).isEqualTo(-2); + assertThat(result.colDiff()).isEqualTo(2); + } + + @Test + void 기존_위치에서_추가적인_거리를_통해_다음_위치를_구할_수_있다() { + // given + Position current = new Position(5, 5); + + // when + Position next = current.resolveNext(-1, 2); + + // then + assertThat(next).isEqualTo(new Position(4, 7)); + } +} \ No newline at end of file diff --git a/src/test/java/model/fixture/PieceMovePathFixture.java b/src/test/java/model/fixture/PieceMovePathFixture.java new file mode 100644 index 0000000000..6548acb4cb --- /dev/null +++ b/src/test/java/model/fixture/PieceMovePathFixture.java @@ -0,0 +1,82 @@ +package model.fixture; + +import java.util.List; +import java.util.stream.Stream; +import model.Team; +import model.board.Position; +import org.junit.jupiter.params.provider.Arguments; + +public class PieceMovePathFixture { + + static Stream 사방위_이동_경로_테스트_데이터() { + return Stream.of( + // 1. 여러 칸 이동 (중간 경로 존재) + Arguments.of( + new Position(0, 0), + new Position(0, 5), + List.of(new Position(0, 1), new Position(0, 2), new Position(0, 3), new Position(0, 4)) + ), + // 2. 한 칸 이동 (중간 경로 없음) + Arguments.of( + new Position(3, 4), + new Position(3, 5), + List.of() + ), + // 3. 수직 이동 (행 방향 이동) + Arguments.of( + new Position(0, 0), + new Position(3, 0), + List.of(new Position(1, 0), new Position(2, 0)) + ) + ); + } + + // === 마 이동 데이터 === + static Stream 마_이동_경로_테스트_데이터() { + return Stream.of( + // 1. 세로(rowDiff)로 먼저 움직이는 경우 (rowDiff=2, colDiff=1) + Arguments.of(new Position(5, 4), new Position(7, 3), List.of(new Position(6, 4))), // 북서 + Arguments.of(new Position(5, 4), new Position(7, 5), List.of(new Position(6, 4))), // 북동 + Arguments.of(new Position(5, 4), new Position(3, 3), List.of(new Position(4, 4))), // 남서 + Arguments.of(new Position(5, 4), new Position(3, 5), List.of(new Position(4, 4))), // 남동 + + // 2. 가로(colDiff)로 먼저 움직이는 경우 (rowDiff=1, colDiff=2) + Arguments.of(new Position(5, 4), new Position(6, 6), List.of(new Position(5, 5))), // 동북 + Arguments.of(new Position(5, 4), new Position(4, 6), List.of(new Position(5, 5))), // 동남 + Arguments.of(new Position(5, 4), new Position(6, 2), List.of(new Position(5, 3))), // 서북 + Arguments.of(new Position(5, 4), new Position(4, 2), List.of(new Position(5, 3))) // 서남 + ); + } + + // === 상 이동 데이터 === + static Stream 상_이동_경로_테스트_데이터() { + return Stream.of( + // 1. 세로(rowDiff)로 먼저 움직이는 경우 (rowDiff=3, colDiff=2) + // 북서, 북동, 남서, 남동 순서 + Arguments.of(new Position(5, 4), new Position(8, 2), List.of(new Position(6, 4), new Position(7, 3))), + Arguments.of(new Position(5, 4), new Position(8, 6), List.of(new Position(6, 4), new Position(7, 5))), + Arguments.of(new Position(5, 4), new Position(2, 2), List.of(new Position(4, 4), new Position(3, 3))), + Arguments.of(new Position(5, 4), new Position(2, 6), List.of(new Position(4, 4), new Position(3, 5))), + + // 2. 가로(colDiff)로 먼저 움직이는 경우 (rowDiff=2, colDiff=3) + // 동북, 동남, 서북, 서남 순서 + Arguments.of(new Position(5, 4), new Position(7, 7), List.of(new Position(5, 5), new Position(6, 6))), + Arguments.of(new Position(5, 4), new Position(3, 7), List.of(new Position(5, 5), new Position(4, 6))), + Arguments.of(new Position(5, 4), new Position(7, 1), List.of(new Position(5, 3), new Position(6, 2))), + Arguments.of(new Position(5, 4), new Position(3, 1), List.of(new Position(5, 3), new Position(4, 2))) + ); + } + + static Stream 졸_병_이동_경로_테스트_데이터() { + return 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)), + // 초나라 전진/좌/우 + 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)) + ); + } +} diff --git a/src/test/java/model/fixture/PieceMovePositionFixture.java b/src/test/java/model/fixture/PieceMovePositionFixture.java new file mode 100644 index 0000000000..75c0c05cca --- /dev/null +++ b/src/test/java/model/fixture/PieceMovePositionFixture.java @@ -0,0 +1,127 @@ +package model.fixture; + +import java.util.stream.Stream; +import model.Team; +import model.board.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 증가 + Arguments.of(new Position(5, 5), new Position(3, 7)), // 2. 남동 (SE): rowDiff 감소, colDiff 증가 + Arguments.of(new Position(5, 5), new Position(2, 2)), // 3. 남서 (SW): rowDiff 감소, colDiff 감소 + Arguments.of(new Position(2, 5), new Position(4, 3)) // 4. 북서 (NW): rowDiff 증가, colDiff 감소 + ); + } + + // ============================ + // 馬 + // ============================ + + 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)), // 직선 이동 + Arguments.of(new Position(5, 5), new Position(7, 5)), // 직선 이동 + Arguments.of(new Position(5, 5), new Position(7, 7)), // 정대각선 + Arguments.of(new Position(5, 5), new Position(8, 7)), // 상 이동 (rowDiff+3, colDiff+2) + Arguments.of(new Position(5, 5), new Position(5, 5)) // 제자리 + ); + } + + // ============================ + // 象 + // ============================ + + 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) + Arguments.of(new Position(5, 5), new Position(5, 8)), // 직선 이동 + Arguments.of(new Position(5, 5), new Position(8, 8)), // 정대각선 + Arguments.of(new Position(5, 5), new Position(4, 4)), // 1칸 대각선 + Arguments.of(new Position(5, 5), new Position(5, 5)) // 제자리 + ); + } + + // ============================ + // 兵 & 卒 (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) 불가능 + Stream.of( + Arguments.of(Team.HAN, new Position(3, 2), new Position(2, 2)), // 후퇴 + Arguments.of(Team.HAN, new Position(3, 2), new Position(5, 2)), // 2칸 전진 + Arguments.of(Team.HAN, new Position(3, 2), new Position(4, 3)) // 대각선 + ), + // 초나라 (CHO) 불가능 + Stream.of( + Arguments.of(Team.CHO, new Position(6, 2), new Position(7, 2)), // 후퇴 + Arguments.of(Team.CHO, new Position(6, 2), new Position(4, 2)), // 2칸 전진 + Arguments.of(Team.CHO, new Position(6, 2), new Position(5, 3)) // 대각선 + ) + ); + } +} \ No newline at end of file diff --git a/src/test/java/model/piece/CannonTest.java b/src/test/java/model/piece/CannonTest.java new file mode 100644 index 0000000000..796291c9f3 --- /dev/null +++ b/src/test/java/model/piece/CannonTest.java @@ -0,0 +1,98 @@ +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 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) { + // given + Piece cannon = new Cannon(Team.HAN); + + // when & then + assertThatThrownBy(() -> cannon.validateMove(current, next)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @MethodSource("model.fixture.PieceMovePathFixture#사방위_이동_경로_테스트_데이터") + void 포에_대한_다음_위치까지의_이동_경로를_반환한다(Position current, Position next, List expectedPath) { + // given + Piece chariot = new Cannon(Team.HAN); + + // when + List path = chariot.extractPath(current, next); + + // then + assertThat(path).isEqualTo(expectedPath); + } + + @Test + void 포가_넘어가는_다리에_포가_있으면_예외가_발생한다() { + // given + Piece cannon = new Cannon(Team.HAN); + Piece hurdleCannon = new Cannon(Team.CHO); // 다리가 포인 경우 + List piecesOnPath = List.of(hurdleCannon); + + // when & then + assertThatThrownBy(() -> cannon.validatePathCondition(piecesOnPath)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 포가_넘어가는_다리가_없으면_예외가_발생한다() { + // given + Piece cannon = new Cannon(Team.HAN); + List emptyPath = List.of(); // 다리가 없는 경우 + + // when & then + assertThatThrownBy(() -> cannon.validatePathCondition(emptyPath)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 포가_넘어가는_다리가_두_개_이상이면_예외가_발생한다() { + // given + Piece cannon = new Cannon(Team.HAN); + List manyHurdles = List.of( + new Cannon(Team.CHO), + new Cannon(Team.CHO) + ); + + // when & then + assertThatThrownBy(() -> cannon.validatePathCondition(manyHurdles)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 포가_상대_포를_잡으려_하면_예외가_발생한다() { + // given + Piece cannon = new Cannon(Team.HAN); + Piece targetCannon = new Cannon(Team.CHO); + + // when & then + assertThatThrownBy(() -> cannon.validateTarget(targetCannon)) + .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 new file mode 100644 index 0000000000..c0c228b0e3 --- /dev/null +++ b/src/test/java/model/piece/ChariotTest.java @@ -0,0 +1,62 @@ +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.testdouble.FakePiece; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +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) { + // given + Piece chariot = new Chariot(Team.HAN); + + // when & then + assertThatThrownBy(() -> chariot.validateMove(current, next)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @MethodSource("model.fixture.PieceMovePathFixture#사방위_이동_경로_테스트_데이터") + void 차에_대한_다음_위치까지의_이동_경로를_반환한다(Position current, Position next, List expectedPath) { + // given + Piece chariot = new Chariot(Team.HAN); + + // when + List path = chariot.extractPath(current, next); + + // then + assertThat(path).isEqualTo(expectedPath); + } + + @Test + void 차는_이동_경로에_기물이_있으면_예외가_발생한다() { + // given + Piece chariot = new Chariot(Team.HAN); + List obstacles = List.of(FakePiece.createFake(Team.CHO)); + + // when & then + assertThatThrownBy(() -> chariot.validatePathCondition(obstacles)) + .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 new file mode 100644 index 0000000000..7d14631c14 --- /dev/null +++ b/src/test/java/model/piece/ElephantTest.java @@ -0,0 +1,61 @@ +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.testdouble.FakePiece; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +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) { + // given + Piece elephant = new Elephant(Team.HAN); + + // when & then + assertThatThrownBy(() -> elephant.validateMove(current, next)); + } + + @ParameterizedTest + @MethodSource("model.fixture.PieceMovePathFixture#상_이동_경로_테스트_데이터") + void 상에_대한_다음_위치까지의_이동_경로를_반환한다(Position current, Position next, List expectedPath) { + // given + Piece chariot = new Elephant(Team.HAN); + + // when + List path = chariot.extractPath(current, next); + + // then + assertThat(path).isEqualTo(expectedPath); + } + + @Test + void 상은_이동_경로에_기물이_있으면_예외가_발생한다() { + // given + Piece elephant = new Elephant(Team.HAN); + List obstacles = List.of(FakePiece.createFake(Team.CHO)); + + // when & then + assertThatThrownBy(() -> elephant.validatePathCondition(obstacles)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/model/piece/HorseTest.java b/src/test/java/model/piece/HorseTest.java new file mode 100644 index 0000000000..8dbe1c958f --- /dev/null +++ b/src/test/java/model/piece/HorseTest.java @@ -0,0 +1,62 @@ +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.testdouble.FakePiece; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +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) { + // given + Piece horse = new Horse(Team.HAN); + + // when & then + assertThatThrownBy(() -> horse.validateMove(current, next)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @MethodSource("model.fixture.PieceMovePathFixture#마_이동_경로_테스트_데이터") + void 말에_대한_다음_위치까지의_이동_경로를_반환한다(Position current, Position next, List expectedPath) { + // given + Piece chariot = new Horse(Team.HAN); + + // when + List path = chariot.extractPath(current, next); + + // then + assertThat(path).isEqualTo(expectedPath); + } + + @Test + void 마는_이동_경로에_기물이_있으면_예외가_발생한다() { + // given + Piece horse = new Horse(Team.HAN); + List obstacles = List.of(FakePiece.createFake(Team.CHO)); + + // when & then + assertThatThrownBy(() -> horse.validatePathCondition(obstacles)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/model/piece/SoldierTest.java b/src/test/java/model/piece/SoldierTest.java new file mode 100644 index 0000000000..f229aff1e3 --- /dev/null +++ b/src/test/java/model/piece/SoldierTest.java @@ -0,0 +1,62 @@ +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.testdouble.FakePiece; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +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) { + // given + Piece soldier = new Soldier(team); + + // when & then + assertThatThrownBy(() -> soldier.validateMove(current, next)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest(name = "{0}나라 졸 일 때, {1}에서 {2}로 이동 시 무조건 경로가 비어있다.") + @MethodSource("model.fixture.PieceMovePathFixture#졸_병_이동_경로_테스트_데이터") + void 졸_병_경로_테스트(Team team, Position current, Position next) { + // given + Piece soldier = new Soldier(team); + + // when + List path = soldier.extractPath(current, next); + + // then + assertThat(path).isEmpty(); + } + + @Test + void 졸은_이동_경로에_기물이_있으면_예외가_발생한다() { + // given + Piece soldier = new Soldier(Team.HAN); + List obstacles = List.of(FakePiece.createFake(Team.CHO)); + + // when & then + assertThatThrownBy(() -> soldier.validatePathCondition(obstacles)) + .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 new file mode 100644 index 0000000000..c8b05d087e --- /dev/null +++ b/src/test/java/model/testdouble/FakePiece.java @@ -0,0 +1,33 @@ +package model.testdouble; + +import java.util.List; +import model.Team; +import model.board.Position; +import model.piece.Piece; +import model.piece.PieceType; + +public class FakePiece extends Piece { + + private final boolean movable; + private final List path; + + FakePiece(Team team, PieceType type, boolean movable, List path) { + super(team, type); + this.movable = movable; + this.path = path; + } + + public static FakePiece createFake(Team team) { + return new FakePiece(team, PieceType.SOLDIER, true, List.of()); + } + + @Override + public List extractPath(Position current, Position next) { + return path; + } + + @Override + protected void validateMove(Position current, Position next) { + + } +} diff --git a/src/test/java/model/testdouble/SpyBoard.java b/src/test/java/model/testdouble/SpyBoard.java new file mode 100644 index 0000000000..de1816869a --- /dev/null +++ b/src/test/java/model/testdouble/SpyBoard.java @@ -0,0 +1,38 @@ +package model.testdouble; + +import java.util.Map; +import model.Team; +import model.board.Board; +import model.board.Position; +import model.piece.Horse; +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 static SpyBoard cho() { + return new SpyBoard(new Horse(Team.CHO)); + } + + public static SpyBoard han() { + return new SpyBoard(new Horse(Team.HAN)); + } + + @Override + public void move(Position current, Position next) { + isMoved = true; + } + + @Override + public Piece pickPiece(Position position) { + return piece; + } +}