diff --git a/README.md b/README.md index 9775dda0ae..b00418a764 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,73 @@ # java-janggi 장기 미션 저장소 + +# 기능 + +## 1-1 요구사항 +- [x] 각 플레이어 진형 입력 기능 +- [x] 장기판 초기화 기능 +- [x] 장기판 상태 출력 기능 + +## 1-2 요구사항 +- [x] 이동할 기물 위치 좌표 입력 기능 +- [x] 이동할 목적지 좌표 입력 기능 +- [x] 턴제 시스템 기능 +- [x] 장기 진행 상태 출력 기능 +- [x] 기물 이동 기능 + +## ⚠️ 기능 예외 사항 + +### 입력 검증 +- 포진 선택: 1~4 범위 외 입력 시 예외 +- 좌표 범위: x(1~9), y(1~10) 범위 외 입력 시 예외 + +### 기물 선택 검증 +- 선택한 위치에 기물이 존재하지 않을 경우 예외 +- 선택한 기물이 아군 기물이 아닐 경우 예외 + +### 이동 규칙 검증 +- 목적지가 기물의 이동 규칙에 맞지 않을 경우 예외 +- 목적지에 아군 기물이 존재할 경우 예외 +- 이동 경로에 기물이 존재할 경우 예외 (포 제외) + +### 포 규칙 검증 +- 이동 경로에 포가 존재할 경우 예외 +- 이동 경로에 기물이 정확히 1개가 아닐 경우 예외 +- 포로 포를 잡으려 할 경우 예외 + +--- + +## 📁 프로젝트 구조 + +``` +src/main/java +├── Main.java +├── controller +│ └── JanggiController.java +├── domain +│ ├── Board.java +│ ├── Direction.java +│ ├── Formation.java +│ ├── Position.java +│ ├── Side.java +│ ├── Turn.java +│ ├── piece +│ │ ├── Piece.java +│ │ ├── PieceType.java +│ │ ├── King.java +│ │ ├── Guard.java +│ │ ├── Horse.java +│ │ ├── Elephant.java +│ │ ├── Chariot.java +│ │ ├── Cannon.java +│ │ ├── Soldier.java +│ │ └── Empty.java +│ └── strategy +│ ├── MovementStrategy.java +│ ├── PathMovement.java +│ └── LinearMovement.java +└── view + ├── InputView.java + └── OutputView.java +``` \ No newline at end of file diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/main/java/Main.java b/src/main/java/Main.java new file mode 100644 index 0000000000..119de2f13d --- /dev/null +++ b/src/main/java/Main.java @@ -0,0 +1,10 @@ +import controller.JanggiController; +import view.InputView; +import view.OutputView; + +public class Main { + public static void main(String[] args) { + JanggiController janggiController = new JanggiController(new InputView(), new OutputView()); + janggiController.run(); + } +} diff --git a/src/main/java/constant/BoardSpec.java b/src/main/java/constant/BoardSpec.java new file mode 100644 index 0000000000..6f6bd2392c --- /dev/null +++ b/src/main/java/constant/BoardSpec.java @@ -0,0 +1,13 @@ +package constant; + +import domain.Side; + +public class BoardSpec { + + public static final int MIN_X = 1; + public static final int MAX_X = 9; + public static final int MIN_Y = 1; + public static final int MAX_Y = 10; + + public static final Side DEFAULT_STARTING_SIDE = Side.CHO; +} diff --git a/src/main/java/controller/JanggiController.java b/src/main/java/controller/JanggiController.java new file mode 100644 index 0000000000..7855b27747 --- /dev/null +++ b/src/main/java/controller/JanggiController.java @@ -0,0 +1,98 @@ +package controller; + +import domain.Board; +import domain.BoardFactory; +import domain.Formation; +import domain.Game; +import domain.Position; +import domain.Side; +import util.Parser; +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() { + Game game = initGame(); + processMove(game); + } + + private Game initGame() { + Formation choformation = readChoFormation(); + Formation hanformation = readHanFormation(); + + Board board = BoardFactory.createBoard(choformation, hanformation); + Game game = new Game(board); + outputView.printBoardStatus(game.getBoard()); + return game; + } + + private void processMove(Game game) { + while (!game.isGameEnd()) { + Side currentTurn = game.getCurrentTurn(); + outputView.printCurrentTurn(currentTurn); + Position sourcePosition = readSourcePosition(); + Position targetPosition = readTargetPosition(); + try { + game.move(sourcePosition, targetPosition); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + } + outputView.printBoardStatus(game.getBoard()); + } + } + + private Position readSourcePosition() { + while (true) { + try { + int x = Parser.parseInput(inputView.readSourceXPosition()); + int y = Parser.parseInput(inputView.readSourceYPosition()); + return Position.of(x, y); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + } + } + } + + private Position readTargetPosition() { + while (true) { + try { + int x = Parser.parseInput(inputView.readTargetXPosition()); + int y = Parser.parseInput(inputView.readTargetYPosition()); + return Position.of(x, y); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + } + } + } + + private Formation readChoFormation() { + while (true) { + try { + String choFormation = inputView.readChoFormation(); + return Formation.from(choFormation); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + } + } + } + + private Formation readHanFormation() { + while (true) { + try { + String hanFormation = inputView.readHanFormation(); + return Formation.from(hanFormation); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + } + } + } +} diff --git a/src/main/java/domain/Board.java b/src/main/java/domain/Board.java new file mode 100644 index 0000000000..a090a36d9c --- /dev/null +++ b/src/main/java/domain/Board.java @@ -0,0 +1,49 @@ +package domain; + +import domain.piece.Empty; +import domain.piece.King; +import domain.piece.Piece; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class Board { + + private final Map board; + + public Board(Map board) { + this.board = board; + } + + public void movePiece(Position sourcePosition, Position targetPosition) { + board.put(targetPosition, board.get(sourcePosition)); + board.put(sourcePosition, new Empty()); + } + + public Piece getPiece(Position position) { + return board.get(position); + } + + public List findPiecesOnRoute(List route, Position targetPosition) { + List pieces = new ArrayList<>(); + for (Position position : route) { + if (!position.equals(targetPosition)) { + pieces.add(board.get(position)); + } + } + return pieces; + } + + public boolean hasKing(Side side) { + for (Piece piece : board.values()) { + if (piece instanceof King && piece.isSameSide(side)) { + return false; + } + } + return true; + } + + public Map getBoard() { + return Map.copyOf(board); + } +} diff --git a/src/main/java/domain/BoardFactory.java b/src/main/java/domain/BoardFactory.java new file mode 100644 index 0000000000..f6d9c1c3ce --- /dev/null +++ b/src/main/java/domain/BoardFactory.java @@ -0,0 +1,73 @@ +package domain; + +import constant.BoardSpec; +import domain.piece.Empty; +import domain.piece.Piece; +import domain.piece.PieceType; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BoardFactory { + + private static final int BACK_Y = 0; + private static final int KING_Y = 1; + private static final int CANNON_Y = 2; + private static final int SOLDIER_Y = 3; + + private static final List CANNON_X = List.of(2, 8); + private static final List CHARIOT_X = List.of(1, 9); + private static final List GUARD_X = List.of(4, 6); + private static final List KING_X = List.of(5); + private static final List SOLIDER_X = List.of(1, 3, 5, 7, 9); + private static final List FORMATION_X = List.of(2, 3, 7, 8); + + public static Board createBoard(Formation choFormation, Formation hanFormation) { + Map board = new HashMap<>(); + for (int x = BoardSpec.MIN_X; x <= BoardSpec.MAX_X; x++) { + for (int y = BoardSpec.MIN_Y; y <= BoardSpec.MAX_Y; y++) { + placePiece(board, Position.of(x, y), new Empty()); + } + } + placePieces(board, choFormation, Side.CHO); + placePieces(board, hanFormation, Side.HAN); + + return new Board(board); + } + + private static void placePieces(Map board, Formation formation, Side side) { + List rows = getRowForSide(side); + placePiece(board, CANNON_X, rows.get(CANNON_Y), PieceType.CANNON, side); + placePiece(board, CHARIOT_X, rows.get(BACK_Y), PieceType.CHARIOT, side); + placePiece(board, GUARD_X, rows.get(BACK_Y), PieceType.GUARD, side); + placePiece(board, KING_X, rows.get(KING_Y), PieceType.KING, side); + placePiece(board, SOLIDER_X, rows.get(SOLDIER_Y), PieceType.SOLDIER, side); + placeFormationPiece(board, formation, FORMATION_X, rows.get(BACK_Y), side); + } + + private static List getRowForSide(Side side) { + if (side == Side.HAN) { + return List.of(1, 2, 3, 4); + } + return List.of(10, 9, 8, 7); + } + + private static void placePiece(Map board, Position position, Piece piece) { + board.put(position, piece); + } + + private static void placePiece(Map board, List xPositions, int y, PieceType pieceType, + Side side) { + for (int x : xPositions) { + board.put(Position.of(x, y), pieceType.create(side)); + } + } + + private static void placeFormationPiece(Map board, Formation formation, List xPositions, + int y, Side side) { + List pieceTypes = formation.getPieceTypes(); + for (int i = 0; i < pieceTypes.size(); i++) { + board.put(Position.of(xPositions.get(i), y), pieceTypes.get(i).create(side)); + } + } +} diff --git a/src/main/java/domain/Direction.java b/src/main/java/domain/Direction.java new file mode 100644 index 0000000000..a2f968ef4c --- /dev/null +++ b/src/main/java/domain/Direction.java @@ -0,0 +1,28 @@ +package domain; + +public enum Direction { + UP(0,-1), + DOWN(0, 1), + LEFT(-1, 0), + RIGHT(1, 0), + UP_LEFT(-1, -1), + UP_RIGHT(1, -1), + DOWN_LEFT(-1, 1), + DOWN_RIGHT(1, 1); + + private final int x; + private final int y; + + Direction(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } +} diff --git a/src/main/java/domain/Formation.java b/src/main/java/domain/Formation.java new file mode 100644 index 0000000000..45645d03d9 --- /dev/null +++ b/src/main/java/domain/Formation.java @@ -0,0 +1,38 @@ +package domain; + +import domain.piece.PieceType; +import java.util.List; + +public enum Formation { + 상마마상("1", List.of(PieceType.ELEPHANT, PieceType.HORSE, PieceType.HORSE, PieceType.ELEPHANT)), + 마상상마("2", List.of(PieceType.HORSE, PieceType.ELEPHANT, PieceType.ELEPHANT, PieceType.HORSE)), + 상마상마("3", List.of(PieceType.ELEPHANT, PieceType.HORSE, PieceType.ELEPHANT, PieceType.HORSE)), + 마상마상("4", List.of(PieceType.HORSE, PieceType.ELEPHANT, PieceType.HORSE, PieceType.ELEPHANT)); + + private static final String INVALID_OPTION_RANGE = "번호는 1~4 사이의 숫자여야 합니다."; + + private final String option; + private final List pieceTypes; + + Formation(String option, List pieceTypes) { + this.option = option; + this.pieceTypes = pieceTypes; + } + + public static Formation from(String input) { + for (Formation formation : Formation.values()) { + if (formation.option.equals(input)) { + return formation; + } + } + throw new IllegalArgumentException(INVALID_OPTION_RANGE); + } + + public List getPieceTypes() { + return pieceTypes; + } + + public String toDisplayString() { + return option + ". " + name(); + } +} diff --git a/src/main/java/domain/Game.java b/src/main/java/domain/Game.java new file mode 100644 index 0000000000..3e705b2985 --- /dev/null +++ b/src/main/java/domain/Game.java @@ -0,0 +1,44 @@ +package domain; + +import constant.BoardSpec; +import domain.piece.Piece; +import java.util.List; +import java.util.Map; + +public class Game { + + private final Board board; + private final Turn turn; + + public Game(Board board) { + this.board = board; + this.turn = new Turn(BoardSpec.DEFAULT_STARTING_SIDE); + } + + public void move(Position sourcePosition, Position targetPosition) { + validateMovement(sourcePosition, targetPosition); + board.movePiece(sourcePosition, targetPosition); + turn.next(); + } + + public boolean isGameEnd() { + return board.hasKing(Side.CHO) || board.hasKing(Side.HAN); + } + + public Side getCurrentTurn() { + return turn.current(); + } + + public Map getBoard() { + return board.getBoard(); + } + + private void validateMovement(Position sourcePosition, Position targetPosition) { + Piece sourcePiece = board.getPiece(sourcePosition); + Piece targetPiece = board.getPiece(targetPosition); + sourcePiece.validateMovement(turn.current(), targetPiece); + List route = sourcePiece.findRoute(sourcePosition, targetPosition); + List pieces = board.findPiecesOnRoute(route, targetPosition); + sourcePiece.checkRoute(pieces); + } +} diff --git a/src/main/java/domain/Position.java b/src/main/java/domain/Position.java new file mode 100644 index 0000000000..ee156718ab --- /dev/null +++ b/src/main/java/domain/Position.java @@ -0,0 +1,43 @@ +package domain; + +import constant.BoardSpec; +import java.util.HashMap; +import java.util.Map; + +public class Position { + + private static final String INVALID_POSITION_RANGE = String.format("x 좌표는 %d~%d, y 좌표는 %d~%d, 사이여야 합니다.", + BoardSpec.MIN_X, BoardSpec.MAX_X, BoardSpec.MIN_Y, BoardSpec.MAX_Y); + + private static final Map> CACHE = new HashMap<>(); + private final int x; + private final int y; + + private Position(int x, int y) { + this.x = x; + this.y = y; + } + + public static Position of(int x, int y) { + validateOutOfRange(x, y); + return CACHE + .computeIfAbsent(x, k -> new HashMap<>()) + .computeIfAbsent(y, k -> new Position(x, y)); + } + + public boolean canMove(int dx, int dy) { + int nx = x + dx; + int ny = y + dy; + return BoardSpec.MIN_X <= nx && nx <= BoardSpec.MAX_X && BoardSpec.MIN_Y <= ny && ny <= BoardSpec.MAX_Y; + } + + public Position createPosition(int dx, int dy) { + return Position.of(x + dx, y + dy); + } + + private static void validateOutOfRange(int x, int y) { + if (!((BoardSpec.MIN_X <= x && x <= BoardSpec.MAX_X) && (BoardSpec.MIN_Y <= y && y <= BoardSpec.MAX_Y))) { + throw new IllegalArgumentException(INVALID_POSITION_RANGE); + } + } +} \ No newline at end of file diff --git a/src/main/java/domain/Side.java b/src/main/java/domain/Side.java new file mode 100644 index 0000000000..16ac088c11 --- /dev/null +++ b/src/main/java/domain/Side.java @@ -0,0 +1,13 @@ +package domain; + +public enum Side { + CHO, + HAN; + + public Side opposite() { + if (this == CHO) { + return HAN; + } + return CHO; + } +} diff --git a/src/main/java/domain/Turn.java b/src/main/java/domain/Turn.java new file mode 100644 index 0000000000..b26236a783 --- /dev/null +++ b/src/main/java/domain/Turn.java @@ -0,0 +1,18 @@ +package domain; + +public class Turn { + + private Side current; + + public Turn(Side side) { + this.current = side; + } + + public Side current() { + return current; + } + + public void next() { + current = current.opposite(); + } +} diff --git a/src/main/java/domain/piece/Cannon.java b/src/main/java/domain/piece/Cannon.java new file mode 100644 index 0000000000..cc3e9fbc52 --- /dev/null +++ b/src/main/java/domain/piece/Cannon.java @@ -0,0 +1,57 @@ +package domain.piece; + +import domain.Direction; +import domain.Position; +import domain.Side; +import domain.strategy.MovementStrategy; +import java.util.List; + +public class Cannon extends Piece { + + private static final String CANNOT_JUMP_WITH_CANNON = "포를 넘어갈 수 없습니다."; + private static final String CANNOT_CAPTURE_CANNON_WITH_CANNON = "포는 포끼리 잡을 수 없습니다."; + + private final List> paths = List.of( + List.of(Direction.UP), List.of(Direction.DOWN), List.of(Direction.RIGHT), List.of(Direction.LEFT)); + + public Cannon(Side side, MovementStrategy movementStrategy) { + super(side, movementStrategy); + } + + @Override + public List findRoute(Position sourcePosition, Position targetPosition) { + return movementStrategy.findRoute(paths, sourcePosition, targetPosition); + } + + @Override + public void checkRoute(List pieces) { + int count = 0; + for (Piece piece : pieces) { + if (piece instanceof Cannon) { + throw new IllegalArgumentException(CANNOT_JUMP_WITH_CANNON); + } + if (!(piece instanceof Empty)) { + count++; + } + } + if (count != 1) { + throw new IllegalArgumentException(INVALID_TARGET_POSITION); + } + } + + @Override + public void checkTarget(Piece piece) { + if (piece.isSameSide(side)) { + throw new IllegalArgumentException(CANNOT_CAPTURE_OWN_PIECE); + } + + if (piece instanceof Cannon) { + throw new IllegalArgumentException(CANNOT_CAPTURE_CANNON_WITH_CANNON); + } + } + + @Override + public String getName() { + return "포"; + } +} diff --git a/src/main/java/domain/piece/Chariot.java b/src/main/java/domain/piece/Chariot.java new file mode 100644 index 0000000000..9deb4bb936 --- /dev/null +++ b/src/main/java/domain/piece/Chariot.java @@ -0,0 +1,28 @@ +package domain.piece; + +import domain.Direction; +import domain.Position; +import domain.Side; +import domain.strategy.MovementStrategy; +import java.util.List; + +public class Chariot extends Piece { + + private final List> paths = List.of( + List.of(Direction.UP), List.of(Direction.DOWN), List.of(Direction.RIGHT), List.of(Direction.LEFT) + ); + + public Chariot(Side side, MovementStrategy movementStrategy) { + super(side, movementStrategy); + } + + @Override + public List findRoute(Position sourcePosition, Position targetPosition) { + return movementStrategy.findRoute(paths, sourcePosition, targetPosition); + } + + @Override + public String getName() { + return "차"; + } +} diff --git a/src/main/java/domain/piece/Elephant.java b/src/main/java/domain/piece/Elephant.java new file mode 100644 index 0000000000..7a89db161b --- /dev/null +++ b/src/main/java/domain/piece/Elephant.java @@ -0,0 +1,35 @@ +package domain.piece; + +import domain.Direction; +import domain.Position; +import domain.Side; +import domain.strategy.MovementStrategy; +import java.util.List; + +public class Elephant extends Piece { + + private final List> paths = List.of( + List.of(Direction.UP, Direction.UP_LEFT, Direction.UP_LEFT), + List.of(Direction.UP, Direction.UP_RIGHT, Direction.UP_RIGHT), + List.of(Direction.RIGHT, Direction.UP_RIGHT, Direction.UP_RIGHT), + List.of(Direction.RIGHT, Direction.DOWN_RIGHT, Direction.DOWN_RIGHT), + List.of(Direction.DOWN, Direction.DOWN_LEFT, Direction.DOWN_LEFT), + List.of(Direction.DOWN, Direction.DOWN_RIGHT, Direction.DOWN_RIGHT), + List.of(Direction.LEFT, Direction.UP_LEFT, Direction.UP_LEFT), + List.of(Direction.LEFT, Direction.DOWN_LEFT, Direction.DOWN_LEFT) + ); + + public Elephant(Side side, MovementStrategy movementStrategy) { + super(side, movementStrategy); + } + + @Override + public List findRoute(Position sourcePosition, Position targetPosition) { + return movementStrategy.findRoute(paths, sourcePosition, targetPosition); + } + + @Override + public String getName() { + return "상"; + } +} diff --git a/src/main/java/domain/piece/Empty.java b/src/main/java/domain/piece/Empty.java new file mode 100644 index 0000000000..0a83615af6 --- /dev/null +++ b/src/main/java/domain/piece/Empty.java @@ -0,0 +1,21 @@ +package domain.piece; + +import domain.Position; +import java.util.List; + +public class Empty extends Piece { + + public Empty() { + super(null, null); + } + + @Override + public List findRoute(Position sourcePosition, Position targetPosition) { + throw new IllegalArgumentException(INVALID_TARGET_POSITION); + } + + @Override + public String getName() { + return "."; + } +} diff --git a/src/main/java/domain/piece/Guard.java b/src/main/java/domain/piece/Guard.java new file mode 100644 index 0000000000..b9dfbcd31e --- /dev/null +++ b/src/main/java/domain/piece/Guard.java @@ -0,0 +1,28 @@ +package domain.piece; + +import domain.Direction; +import domain.Position; +import domain.Side; +import domain.strategy.MovementStrategy; +import java.util.List; + +public class Guard extends Piece { + + private final List> paths = List.of( + List.of(Direction.UP), List.of(Direction.DOWN), List.of(Direction.RIGHT), List.of(Direction.LEFT) + ); + + public Guard(Side side, MovementStrategy movementStrategy) { + super(side, movementStrategy); + } + + @Override + public List findRoute(Position sourcePosition, Position targetPosition) { + return movementStrategy.findRoute(paths, sourcePosition, targetPosition); + } + + @Override + public String getName() { + return "사"; + } +} diff --git a/src/main/java/domain/piece/Horse.java b/src/main/java/domain/piece/Horse.java new file mode 100644 index 0000000000..160d221fdb --- /dev/null +++ b/src/main/java/domain/piece/Horse.java @@ -0,0 +1,34 @@ +package domain.piece; + +import domain.Direction; +import domain.Position; +import domain.Side; +import domain.strategy.LinearMovement; +import domain.strategy.MovementStrategy; +import domain.strategy.PathMovement; + +import java.util.List; + +public class Horse extends Piece { + + private final List> paths = List.of( + List.of(Direction.UP, Direction.UP_LEFT), List.of(Direction.UP, Direction.UP_RIGHT), + List.of(Direction.RIGHT, Direction.UP_RIGHT), List.of(Direction.RIGHT, Direction.DOWN_RIGHT), + List.of(Direction.DOWN, Direction.DOWN_LEFT), List.of(Direction.DOWN, Direction.DOWN_RIGHT), + List.of(Direction.LEFT, Direction.UP_LEFT), List.of(Direction.LEFT, Direction.DOWN_LEFT) + ); + + public Horse(Side side, MovementStrategy movementStrategy) { + super(side, movementStrategy); + } + + @Override + public List findRoute(Position sourcePosition, Position targetPosition) { + return movementStrategy.findRoute(paths, sourcePosition, targetPosition); + } + + @Override + public String getName() { + return "마"; + } +} diff --git a/src/main/java/domain/piece/King.java b/src/main/java/domain/piece/King.java new file mode 100644 index 0000000000..ccc7e25e69 --- /dev/null +++ b/src/main/java/domain/piece/King.java @@ -0,0 +1,28 @@ +package domain.piece; + +import domain.Direction; +import domain.Position; +import domain.Side; +import domain.strategy.MovementStrategy; +import java.util.List; + +public class King extends Piece { + + private final List> paths = List.of( + List.of(Direction.UP), List.of(Direction.DOWN), List.of(Direction.RIGHT), List.of(Direction.LEFT) + ); + + public King(Side side, MovementStrategy movementStrategy) { + super(side, movementStrategy); + } + + @Override + public List findRoute(Position sourcePosition, Position targetPosition) { + return movementStrategy.findRoute(paths, sourcePosition, targetPosition); + } + + @Override + public String getName() { + return "왕"; + } +} diff --git a/src/main/java/domain/piece/Piece.java b/src/main/java/domain/piece/Piece.java new file mode 100644 index 0000000000..46906d318d --- /dev/null +++ b/src/main/java/domain/piece/Piece.java @@ -0,0 +1,55 @@ +package domain.piece; + +import domain.Position; +import domain.Side; +import domain.strategy.MovementStrategy; +import java.util.List; + +public abstract class Piece { + + private static final String PIECE_NOT_FOUND = "해당 위치에 기물이 존재하지 않습니다."; + private static final String NOT_OWN_PIECE = "선택한 기물은 아군 기물이 아닙니다."; + protected static final String CANNOT_CAPTURE_OWN_PIECE = "아군 기물은 잡을 수 없습니다."; + protected static final String INVALID_TARGET_POSITION = "이동할 수 없는 목적지입니다."; + + protected final Side side; + protected final MovementStrategy movementStrategy; + + protected Piece(Side side, MovementStrategy movementStrategy) { + this.side = side; + this.movementStrategy = movementStrategy; + } + + public void validateMovement(Side currentTurn, Piece targetPiece) { + if (this instanceof Empty) { + throw new IllegalArgumentException(PIECE_NOT_FOUND); + } + if (!side.equals(currentTurn)) { + throw new IllegalArgumentException(NOT_OWN_PIECE); + } + + checkTarget(targetPiece); + } + + public void checkTarget(Piece piece) { + if (piece.isSameSide(side)) { + throw new IllegalArgumentException(CANNOT_CAPTURE_OWN_PIECE); + } + } + + public void checkRoute(List pieces) { + for (Piece piece : pieces) { + if (!(piece instanceof Empty)) { + throw new IllegalArgumentException(INVALID_TARGET_POSITION); + } + } + } + + public boolean isSameSide(Side side) { + return this.side == side; + } + + public abstract List findRoute(Position sourcePosition, Position targetPosition); + + public abstract String getName(); +} diff --git a/src/main/java/domain/piece/PieceType.java b/src/main/java/domain/piece/PieceType.java new file mode 100644 index 0000000000..96f7ce0402 --- /dev/null +++ b/src/main/java/domain/piece/PieceType.java @@ -0,0 +1,52 @@ +package domain.piece; + +import domain.Side; +import domain.strategy.LinearMovement; +import domain.strategy.PathMovement; + +public enum PieceType { + ELEPHANT { + @Override + public Piece create(Side side) { + return new Elephant(side, new PathMovement()); + } + }, + CANNON { + @Override + public Piece create(Side side) { + return new Cannon(side, new LinearMovement()); + } + }, + CHARIOT { + @Override + public Piece create(Side side) { + return new Chariot(side, new LinearMovement()); + } + }, + GUARD { + @Override + public Piece create(Side side) { + return new Guard(side, new PathMovement()); + } + }, + HORSE { + @Override + public Piece create(Side side) { + return new Horse(side, new PathMovement()); + } + }, + KING { + @Override + public Piece create(Side side) { + return new King(side, new PathMovement()); + } + }, + SOLDIER { + @Override + public Piece create(Side side) { + return new Soldier(side, new PathMovement()); + } + }; + + public abstract Piece create(Side side); +} diff --git a/src/main/java/domain/piece/Soldier.java b/src/main/java/domain/piece/Soldier.java new file mode 100644 index 0000000000..f7bb7d59b9 --- /dev/null +++ b/src/main/java/domain/piece/Soldier.java @@ -0,0 +1,38 @@ +package domain.piece; + +import domain.Direction; +import domain.Position; +import domain.Side; +import domain.strategy.MovementStrategy; +import java.util.List; + +public class Soldier extends Piece { + + private final List> paths; + + public Soldier(Side side, MovementStrategy movementStrategy) { + super(side, movementStrategy); + if (Side.CHO == side) { + paths = List.of( + List.of(Direction.UP), List.of(Direction.RIGHT), List.of(Direction.LEFT) + ); + return; + } + paths = List.of( + List.of(Direction.DOWN), List.of(Direction.RIGHT), List.of(Direction.LEFT) + ); + } + + @Override + public List findRoute(Position sourcePosition, Position targetPosition) { + return movementStrategy.findRoute(paths, sourcePosition, targetPosition); + } + + @Override + public String getName() { + if (isSameSide(Side.CHO)) { + return "졸"; + } + return "병"; + } +} diff --git a/src/main/java/domain/strategy/LinearMovement.java b/src/main/java/domain/strategy/LinearMovement.java new file mode 100644 index 0000000000..ca4e13388a --- /dev/null +++ b/src/main/java/domain/strategy/LinearMovement.java @@ -0,0 +1,27 @@ +package domain.strategy; + +import domain.Direction; +import domain.Position; + +import java.util.ArrayList; +import java.util.List; + +public class LinearMovement extends MovementStrategy { + + @Override + protected List buildRoute(List path, Position sourcePosition, Position targetPosition) { + List route = new ArrayList<>(); + Direction direction = path.getFirst(); + Position current = sourcePosition; + + while (current.canMove(direction.getX(), direction.getY())) { + Position next = current.createPosition(direction.getX(), direction.getY()); + route.add(next); + if (next.equals(targetPosition)) { + return List.copyOf(route); + } + current = next; + } + return List.of(); + } +} diff --git a/src/main/java/domain/strategy/MovementStrategy.java b/src/main/java/domain/strategy/MovementStrategy.java new file mode 100644 index 0000000000..03fd70db4a --- /dev/null +++ b/src/main/java/domain/strategy/MovementStrategy.java @@ -0,0 +1,23 @@ +package domain.strategy; + +import domain.Direction; +import domain.Position; +import java.util.List; + +public abstract class MovementStrategy { + + protected final String INVALID_TARGET_POSITION = "이동할 수 없는 목적지입니다."; + + public List findRoute(List> paths, Position sourcePosition, Position targetPosition) { + for (List path : paths) { + List positions = buildRoute(path, sourcePosition, targetPosition); + if (!positions.isEmpty() && positions.getLast().equals(targetPosition)) { + return positions; + } + } + throw new IllegalArgumentException(INVALID_TARGET_POSITION); + } + + protected abstract List buildRoute(List route, Position sourcePosition, + Position targetPosition); +} diff --git a/src/main/java/domain/strategy/PathMovement.java b/src/main/java/domain/strategy/PathMovement.java new file mode 100644 index 0000000000..5d1a2c5ff0 --- /dev/null +++ b/src/main/java/domain/strategy/PathMovement.java @@ -0,0 +1,25 @@ +package domain.strategy; + +import domain.Direction; +import domain.Position; +import java.util.ArrayList; +import java.util.List; + +public class PathMovement extends MovementStrategy { + + @Override + protected List buildRoute(List path, Position source, Position target) { + List route = new ArrayList<>(); + Position current = source; + + for (Direction direction : path) { + if (!current.canMove(direction.getX(), direction.getY())) { + return List.of(); + } + current = current.createPosition(direction.getX(), direction.getY()); + route.add(current); + } + + return route; + } +} diff --git a/src/main/java/util/Parser.java b/src/main/java/util/Parser.java new file mode 100644 index 0000000000..1b14baf176 --- /dev/null +++ b/src/main/java/util/Parser.java @@ -0,0 +1,14 @@ +package util; + +public class Parser { + + private static final String PARSE_INPUT_MESSAGE = "유효한 입력값이 아닙니다."; + + public static int parseInput(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(PARSE_INPUT_MESSAGE); + } + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 0000000000..5a7287a391 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,57 @@ +package view; + +import constant.BoardSpec; +import domain.Formation; +import java.util.Scanner; + +public class InputView { + + private static final String READ_FORMATION_MESSAGE = "[%s] 포진을 선택해주세요."; + private static final String READ_SOURCE_X_POSITION = String.format("움직일 기물의 x 좌표를 입력해주세요. (x 범위 %d ~ %d)", + BoardSpec.MIN_X, BoardSpec.MAX_X); + private static final String READ_SOURCE_Y_POSITION = String.format("움직일 기물의 y 좌표를 입력해주세요. (y 범위 %d~ %d)", + BoardSpec.MIN_Y, BoardSpec.MAX_Y); + + private final Scanner scanner = new Scanner(System.in); + + public String readChoFormation() { + System.out.printf(READ_FORMATION_MESSAGE, "초"); + System.out.println(); + return readFormation(); + } + + public String readHanFormation() { + System.out.printf(READ_FORMATION_MESSAGE, "한"); + System.out.println(); + return readFormation(); + } + + public String readSourceXPosition() { + System.out.println(); + System.out.println(READ_SOURCE_X_POSITION); + return scanner.nextLine(); + } + + public String readSourceYPosition() { + System.out.println(); + return scanner.nextLine(); + } + + public String readTargetXPosition() { + System.out.println(); + System.out.println(READ_SOURCE_X_POSITION); + return scanner.nextLine(); + } + + public String readTargetYPosition() { + System.out.println(READ_SOURCE_Y_POSITION); + return scanner.nextLine(); + } + + private String readFormation() { + for (Formation formation : Formation.values()) { + System.out.println(formation.toDisplayString()); + } + return scanner.nextLine(); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 0000000000..7796bb4c63 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,71 @@ +package view; + +import constant.BoardSpec; +import domain.Position; +import domain.Side; +import domain.piece.Piece; +import java.util.Map; + +public class OutputView { + + private static final String RED = "\u001B[31m"; + private static final String BLUE = "\u001B[34m"; + private static final String RESET = "\u001B[0m"; + private static final String ERROR_PREFIX = "[ERROR] "; + private static final String BOARD_HEADER = " 1 2 3 4 5 6 7 8 9"; + private static final String CHO_TURN_MESSAGE = "초의 차례입니다."; + private static final String HAN_TURN_MESSAGE = "한의 차례입니다."; + private static final String SINGLE_DIGIT_ROW_FORMAT = " %d "; + private static final String DOUBLE_DIGIT_ROW_FORMAT = "%d "; + private static final String PIECE_FORMAT = "%s%s "; + + public void printBoardStatus(Map board) { + System.out.println(); + System.out.println(BOARD_HEADER); + + for (int y = BoardSpec.MIN_Y; y <= BoardSpec.MAX_Y; y++) { + printRowNumber(y); + for (int x = 1; x <= 9; x++) { + Piece piece = board.get(Position.of(x, y)); + printPiece(piece); + } + System.out.println(RESET); + } + } + + public void printCurrentTurn(Side currentSide) { + System.out.println(); + if (currentSide == Side.CHO) { + System.out.println(CHO_TURN_MESSAGE); + return; + } + System.out.println(HAN_TURN_MESSAGE); + } + + public void printErrorMessage(String errorMessage) { + System.out.println(ERROR_PREFIX + errorMessage); + } + + private void printRowNumber(int y) { + if (y < BoardSpec.MAX_Y) { + System.out.printf(SINGLE_DIGIT_ROW_FORMAT, y); + } else { + System.out.printf(DOUBLE_DIGIT_ROW_FORMAT, y); + } + } + + private void printPiece(Piece piece) { + String color = getColor(piece); + System.out.printf(PIECE_FORMAT, color, piece.getName()); + } + + private String getColor(Piece piece) { + if (piece.isSameSide(Side.HAN)) { + return RED; + } + if (piece.isSameSide(Side.CHO)) { + return BLUE; + } + return RESET; + } +} diff --git a/src/test/java/.gitkeep b/src/test/java/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/test/java/BoardFactoryTest.java b/src/test/java/BoardFactoryTest.java new file mode 100644 index 0000000000..9b79fa4f80 --- /dev/null +++ b/src/test/java/BoardFactoryTest.java @@ -0,0 +1,70 @@ +import static org.assertj.core.api.Assertions.assertThat; + +import domain.Board; +import domain.BoardFactory; +import domain.Formation; +import domain.Position; +import domain.Side; +import domain.piece.Elephant; +import domain.piece.Horse; +import domain.piece.Piece; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class BoardFactoryTest { + + @Nested + @DisplayName("장기판 생성") + class CreateBoard { + + @DisplayName("장기판의 모든 좌표를 초기화한다.") + @Test + void 장기판의_모든_좌표를_초기화한다() { + Board board = BoardFactory.createBoard(Formation.from("1"), Formation.from("1")); + Map pieces = board.getBoard(); + + assertThat(pieces).hasSize(90); + for (int x = 1; x <= 9; x++) { + for (int y = 1; y <= 10; y++) { + assertThat(pieces.get(Position.of(x, y))).isNotNull(); + } + } + } + + @DisplayName("초나라 포진을 상마마상으로 배치한다.") + @Test + void 초나라_포진을_상마마상으로_배치한다() { + Board board = BoardFactory.createBoard(Formation.from("1"), Formation.from("2")); + Map pieces = board.getBoard(); + + assertThat(pieces.get(Position.of(2, 10))).isInstanceOf(Elephant.class); + assertThat(pieces.get(Position.of(3, 10))).isInstanceOf(Horse.class); + assertThat(pieces.get(Position.of(7, 10))).isInstanceOf(Horse.class); + assertThat(pieces.get(Position.of(8, 10))).isInstanceOf(Elephant.class); + } + + @DisplayName("한나라 포진을 마상마상으로 배치한다.") + @Test + void 한나라_포진을_마상마상으로_배치한다() { + Board board = BoardFactory.createBoard(Formation.from("1"), Formation.from("4")); + Map pieces = board.getBoard(); + + assertThat(pieces.get(Position.of(2, 1))).isInstanceOf(Horse.class); + assertThat(pieces.get(Position.of(3, 1))).isInstanceOf(Elephant.class); + assertThat(pieces.get(Position.of(7, 1))).isInstanceOf(Horse.class); + assertThat(pieces.get(Position.of(8, 1))).isInstanceOf(Elephant.class); + } + + @DisplayName("각 궁이 올바른 진영에 배치된다.") + @Test + void 각_궁이_올바른_진영에_배치된다() { + Board board = BoardFactory.createBoard(Formation.from("1"), Formation.from("2")); + Map pieces = board.getBoard(); + + assertThat(pieces.get(Position.of(5, 2)).isSameSide(Side.HAN)).isTrue(); + assertThat(pieces.get(Position.of(5, 9)).isSameSide(Side.CHO)).isTrue(); + } + } +} diff --git a/src/test/java/BoardTest.java b/src/test/java/BoardTest.java new file mode 100644 index 0000000000..ee78670a5d --- /dev/null +++ b/src/test/java/BoardTest.java @@ -0,0 +1,43 @@ +import static org.assertj.core.api.Assertions.assertThat; + +import domain.Board; +import domain.BoardFactory; +import domain.Formation; +import domain.Position; +import domain.piece.Chariot; +import domain.piece.Empty; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class BoardTest { + + @Nested + @DisplayName("기물 이동") + class MovePiece { + + @DisplayName("기물을 이동하면 목적지에 기물이 위치한다.") + @Test + void 기물을_이동하면_목적지에_기물이_위치한다() { + Board board = BoardFactory.createBoard(Formation.from("1"), Formation.from("1")); + Position source = Position.of(1, 1); + Position target = Position.of(1, 3); + + board.movePiece(source, target); + + assertThat(board.getBoard().get(target)).isInstanceOf(Chariot.class); + } + + @DisplayName("기물을 이동하면 출발지는 빈칸이 된다.") + @Test + void 기물을_이동하면_출발지는_빈칸이_된다() { + Board board = BoardFactory.createBoard(Formation.from("1"), Formation.from("1")); + Position source = Position.of(1, 1); + Position target = Position.of(1, 3); + + board.movePiece(source, target); + + assertThat(board.getBoard().get(source)).isInstanceOf(Empty.class); + } + } +} diff --git a/src/test/java/GameTest.java b/src/test/java/GameTest.java new file mode 100644 index 0000000000..cb2046818e --- /dev/null +++ b/src/test/java/GameTest.java @@ -0,0 +1,431 @@ +import static org.assertj.core.api.Assertions.assertThat; + +import domain.Board; +import domain.BoardFactory; +import domain.Formation; +import domain.Game; +import domain.Position; +import domain.Side; +import domain.piece.Cannon; +import domain.piece.Chariot; +import domain.piece.Elephant; +import domain.piece.Empty; +import domain.piece.Guard; +import domain.piece.Horse; +import domain.piece.King; +import domain.piece.Soldier; +import domain.piece.PieceType; +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; + +public class GameTest { + + @DisplayName("초 턴에 한 기물을 선택한 경우 이동할 수 없다.") + @Test + void 초_턴에_한_기물을_선택한_경우_이동할_수_없다() { + Game game = createGame("1", "1"); + Position sourcePosition = Position.of(1, 4); + Position targetPosition = Position.of(1, 1); + + Assertions.assertThatThrownBy(() -> game.move(sourcePosition, targetPosition)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("선택한 기물은 아군 기물이 아닙니다."); + } + + @DisplayName("한 턴에 초 기물을 선택한 경우 이동할 수 없다.") + @Test + void 한_턴에_초_기물을_선택한_경우_이동할_수_없다() { + Game game = createGame("1", "1"); + moveBySide(game, Side.CHO, Position.of(1, 7), Position.of(1, 6)); + + Assertions.assertThatThrownBy(() -> game.move(Position.of(3, 7), Position.of(3, 6))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("선택한 기물은 아군 기물이 아닙니다."); + } + + @DisplayName("출발지에 기물이 존재하지 않는 경우 이동할 수 없다.") + @Test + void 기물이_존재하지_않는_경우_이동할_수_없다() { + Game game = createGame("1", "1"); + Position sourcePosition = Position.of(5, 5); + Position targetPosition = Position.of(1, 1); + + Assertions.assertThatThrownBy(() -> game.move(sourcePosition, targetPosition)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("해당 위치에 기물이 존재하지 않습니다."); + } + + @DisplayName("양측 궁이 모두 존재하면 게임이 종료되지 않는다.") + @Test + void 양측_궁이_모두_존재하면_게임이_종료되지_않는다() { + Game game = createGame("1", "1"); + + assertThat(game.isGameEnd()).isFalse(); + } + + @DisplayName("한쪽 궁이 없으면 게임이 종료된다.") + @Test + void 한쪽_궁이_없으면_게임이_종료된다() { + Board board = new Board(Map.of( + Position.of(5, 2), PieceType.KING.create(Side.HAN) + )); + Game game = new Game(board); + + assertThat(game.isGameEnd()).isTrue(); + } + + @Nested + @DisplayName("기물 이동") + class MovePiece { + + @Nested + @DisplayName("차") + class ChariotMove { + + @Test + @DisplayName("차가 세로로 이동한다.") + void 차가_세로로_이동한다() { + Game game = createGame("1", "1"); + Position source = Position.of(1, 1); + Position target = Position.of(1, 3); + + moveBySide(game, Side.HAN, source, target); + + assertThat(game.getBoard().get(target)).isInstanceOf(Chariot.class); + assertThat(game.getBoard().get(source)).isInstanceOf(Empty.class); + } + + @Test + @DisplayName("차가 가로로 이동한다.") + void 차가_가로로_이동한다() { + Game game = createGame("1", "1"); + Position source = Position.of(1, 1); + Position mid = Position.of(1, 2); + Position target = Position.of(3, 2); + + moveBySide(game, Side.HAN, source, mid); + moveBySide(game, Side.HAN, mid, target); + + assertThat(game.getBoard().get(target)).isInstanceOf(Chariot.class); + } + + @Test + @DisplayName("차가 대각선으로 이동하면 예외가 발생한다.") + void 차가_대각선으로_이동하면_예외가_발생한다() { + Game game = createGame("1", "1"); + Position source = Position.of(1, 1); + Position target = Position.of(3, 3); + + Assertions.assertThatThrownBy(() -> moveBySide(game, Side.HAN, source, target)) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Nested + @DisplayName("상") + class ElephantMove { + + @Test + @DisplayName("상이 대각선 방향으로 이동한다.") + void 상이_대각선_방향으로_이동한다() { + Game game = createGame("1", "1"); + Position source = Position.of(2, 1); + Position target = Position.of(4, 4); + + moveBySide(game, Side.HAN, source, target); + + assertThat(game.getBoard().get(target)).isInstanceOf(Elephant.class); + assertThat(game.getBoard().get(source)).isInstanceOf(Empty.class); + } + + @Test + @DisplayName("상이 이동 불가능한 위치로 이동하면 예외가 발생한다.") + void 상이_이동_불가능한_위치로_이동하면_예외가_발생한다() { + Game game = createGame("1", "1"); + Position source = Position.of(2, 1); + Position target = Position.of(3, 2); + + Assertions.assertThatThrownBy(() -> moveBySide(game, Side.HAN, source, target)) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Nested + @DisplayName("마") + class HorseMove { + + @Test + @DisplayName("마가 날 일자로 이동한다.") + void 마가_날_일자로_이동한다() { + Game game = createGame("1", "4"); + Position source = Position.of(2, 1); + Position target = Position.of(3, 3); + + moveBySide(game, Side.HAN, source, target); + + assertThat(game.getBoard().get(target)).isInstanceOf(Horse.class); + assertThat(game.getBoard().get(source)).isInstanceOf(Empty.class); + } + + @Test + @DisplayName("마가 이동 불가능한 위치로 이동하면 예외가 발생한다.") + void 마가_이동_불가능한_위치로_이동하면_예외가_발생한다() { + Game game = createGame("1", "4"); + Position source = Position.of(2, 1); + Position target = Position.of(4, 4); + + Assertions.assertThatThrownBy(() -> moveBySide(game, Side.HAN, source, target)) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Nested + @DisplayName("포") + class CannonMove { + + @Test + @DisplayName("포가 기물을 하나 뛰어넘어 이동한다.") + void 포가_기물을_하나_뛰어넘어_이동한다() { + Game game = createGame("1", "1"); + + moveBySide(game, Side.HAN, Position.of(4, 1), Position.of(4, 2)); + moveBySide(game, Side.HAN, Position.of(4, 2), Position.of(4, 3)); + + moveBySide(game, Side.HAN, Position.of(2, 3), Position.of(5, 3)); + + assertThat(game.getBoard().get(Position.of(5, 3))).isInstanceOf(Cannon.class); + } + + @Test + @DisplayName("포가 뛰어넘을 기물이 없으면 예외가 발생한다.") + void 포가_뛰어넘을_기물이_없으면_예외가_발생한다() { + Game game = createGame("1", "1"); + Position source = Position.of(2, 3); + Position target = Position.of(5, 3); + + Assertions.assertThatThrownBy(() -> moveBySide(game, Side.HAN, source, target)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("포가 포를 뛰어넘으면 예외가 발생한다.") + void 포가_포를_뛰어넘으면_예외가_발생한다() { + Game game = createGame("1", "1"); + + moveBySide(game, Side.HAN, Position.of(6, 1), Position.of(6, 2)); + moveBySide(game, Side.HAN, Position.of(6, 2), Position.of(6, 3)); + moveBySide(game, Side.HAN, Position.of(8, 3), Position.of(5, 3)); + + Assertions.assertThatThrownBy( + () -> moveBySide(game, Side.HAN, Position.of(2, 3), Position.of(7, 3))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("포를 넘어갈 수 없습니다."); + } + } + + @Nested + @DisplayName("사") + class GuardMove { + + @Test + @DisplayName("사가 한 칸 이동한다.") + void 사가_한_칸_이동한다() { + Game game = createGame("1", "1"); + Position source = Position.of(4, 1); + Position target = Position.of(5, 1); + + moveBySide(game, Side.HAN, source, target); + + assertThat(game.getBoard().get(target)).isInstanceOf(Guard.class); + assertThat(game.getBoard().get(source)).isInstanceOf(Empty.class); + } + + @Test + @DisplayName("사가 두 칸 이상 이동하면 예외가 발생한다.") + void 사가_두_칸_이상_이동하면_예외가_발생한다() { + Game game = createGame("1", "1"); + Position source = Position.of(4, 1); + Position target = Position.of(4, 3); + + Assertions.assertThatThrownBy(() -> moveBySide(game, Side.HAN, source, target)) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Nested + @DisplayName("궁") + class KingMove { + + @Test + @DisplayName("궁이 한 칸 이동한다.") + void 궁이_한_칸_이동한다() { + Game game = createGame("1", "1"); + Position source = Position.of(5, 2); + Position target = Position.of(5, 1); + + moveBySide(game, Side.HAN, source, target); + + assertThat(game.getBoard().get(target)).isInstanceOf(King.class); + assertThat(game.getBoard().get(source)).isInstanceOf(Empty.class); + } + + @Test + @DisplayName("궁이 두 칸 이상 이동하면 예외가 발생한다.") + void 궁이_두_칸_이상_이동하면_예외가_발생한다() { + Game game = createGame("1", "1"); + Position source = Position.of(5, 2); + Position target = Position.of(5, 4); + + Assertions.assertThatThrownBy(() -> moveBySide(game, Side.HAN, source, target)) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Nested + @DisplayName("졸/병") + class SoldierMove { + + @Test + @DisplayName("졸이 앞으로 한 칸 이동한다.") + void 졸이_앞으로_한_칸_이동한다() { + Game game = createGame("1", "1"); + Position source = Position.of(1, 7); + Position target = Position.of(1, 6); + + moveBySide(game, Side.CHO, source, target); + + assertThat(game.getBoard().get(target)).isInstanceOf(Soldier.class); + assertThat(game.getBoard().get(source)).isInstanceOf(Empty.class); + } + + @Test + @DisplayName("병이 앞으로 한 칸 이동한다.") + void 병이_앞으로_한_칸_이동한다() { + Game game = createGame("1", "1"); + Position source = Position.of(1, 4); + Position target = Position.of(1, 5); + + moveBySide(game, Side.HAN, source, target); + + assertThat(game.getBoard().get(target)).isInstanceOf(Soldier.class); + assertThat(game.getBoard().get(source)).isInstanceOf(Empty.class); + } + + @Test + @DisplayName("졸이 옆으로 한 칸 이동한다.") + void 졸이_옆으로_한_칸_이동한다() { + Game game = createGame("1", "1"); + Position source = Position.of(1, 7); + Position target = Position.of(2, 7); + + moveBySide(game, Side.CHO, source, target); + + assertThat(game.getBoard().get(target)).isInstanceOf(Soldier.class); + } + + @Test + @DisplayName("졸이 뒤로 이동하면 예외가 발생한다.") + void 졸이_뒤로_이동하면_예외가_발생한다() { + Game game = createGame("1", "1"); + Position source = Position.of(1, 7); + Position target = Position.of(1, 8); + + Assertions.assertThatThrownBy(() -> moveBySide(game, Side.CHO, source, target)) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Nested + @DisplayName("적 기물 잡기") + class CapturePiece { + + @Test + @DisplayName("적 기물을 잡을 수 있다.") + void 적_기물을_잡을_수_있다() { + Game game = createGame("1", "1"); + + moveBySide(game, Side.HAN, Position.of(1, 4), Position.of(2, 4)); + moveBySide(game, Side.HAN, Position.of(1, 1), Position.of(1, 7)); + + assertThat(game.getBoard().get(Position.of(1, 7))).isInstanceOf(Chariot.class); + assertThat(game.getBoard().get(Position.of(1, 7)).isSameSide(Side.HAN)).isTrue(); + } + + @Test + @DisplayName("아군 기물을 잡으면 예외가 발생한다.") + void 아군_기물을_잡으면_예외가_발생한다() { + Game game = createGame("1", "1"); + Position source = Position.of(1, 1); + Position target = Position.of(1, 4); + + Assertions.assertThatThrownBy(() -> moveBySide(game, Side.HAN, source, target)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("아군 기물은 잡을 수 없습니다."); + } + } + } + + private Game createGame(String choFormation, String hanFormation) { + Board board = BoardFactory.createBoard(Formation.from(choFormation), Formation.from(hanFormation)); + return new Game(board); + } + + private void moveBySide(Game game, Side side, Position source, Position target) { + if (game.getCurrentTurn() != side) { + passCurrentTurn(game); + } + game.move(source, target); + } + + private void passCurrentTurn(Game game) { + if (game.getCurrentTurn() == Side.CHO) { + moveOneOf(game, new MoveCandidate[] { + new MoveCandidate(9, 7, 9, 6), + new MoveCandidate(9, 6, 8, 6), + new MoveCandidate(8, 6, 8, 5), + new MoveCandidate(8, 5, 7, 5), + new MoveCandidate(7, 7, 7, 6), + new MoveCandidate(5, 7, 5, 6), + new MoveCandidate(3, 7, 3, 6), + new MoveCandidate(1, 7, 1, 6) + }); + return; + } + + moveOneOf(game, new MoveCandidate[] { + new MoveCandidate(9, 4, 9, 5), + new MoveCandidate(9, 5, 8, 5), + new MoveCandidate(8, 5, 8, 6), + new MoveCandidate(8, 6, 7, 6), + new MoveCandidate(7, 4, 7, 5), + new MoveCandidate(5, 4, 5, 5), + new MoveCandidate(3, 4, 3, 5), + new MoveCandidate(1, 4, 1, 5) + }); + } + + private void moveOneOf(Game game, MoveCandidate[] candidates) { + for (MoveCandidate candidate : candidates) { + try { + game.move(candidate.source, candidate.target); + return; + } catch (IllegalArgumentException ignored) { + } + } + throw new IllegalStateException("턴 전환을 위한 이동 가능한 수가 없습니다."); + } + + private static class MoveCandidate { + + private final Position source; + private final Position target; + + private MoveCandidate(int sourceX, int sourceY, int targetX, int targetY) { + this.source = Position.of(sourceX, sourceY); + this.target = Position.of(targetX, targetY); + } + } +} diff --git a/src/test/java/PositionTest.java b/src/test/java/PositionTest.java new file mode 100644 index 0000000000..8b0d78196c --- /dev/null +++ b/src/test/java/PositionTest.java @@ -0,0 +1,59 @@ +import domain.Position; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +public class PositionTest { + + @Nested + class FromTest { + + @ParameterizedTest + @CsvSource({ + "0, 10", + "1, 11" + }) + void 목적지_좌표가_가로_1_9_세로_1_10을_벗어나면_예외가_발생한다(int x, int y) { + // when & then + Assertions.assertThatThrownBy(() -> Position.of(x, y)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("x 좌표는 1~9, y 좌표는 1~10, 사이여야 합니다."); + } + } + + @Nested + class CamMoveTest { + + @ParameterizedTest + @CsvSource({ + "1, 1, 0, 1", + "5, 5, 1, 0", + "9, 10, -1, -1" + }) + void 이동_가능한_좌표이면_true를_반환한다(int x, int y, int dx, int dy) { + // given + Position position = Position.of(x, y); + // when + boolean result = position.canMove(dx, dy); + // then + Assertions.assertThat(result).isTrue(); + } + + @ParameterizedTest + @CsvSource({ + "1, 1, -1, 0", + "1, 1, 0, -1", + "9, 10, 1, 0", + "9, 10, 0, 1" + }) + void 이동_불가능한_좌표이면_false를_반환한다(int x, int y, int dx, int dy) { + // given + Position position = Position.of(x, y); + // when + boolean result = position.canMove(dx, dy); + // then + Assertions.assertThat(result).isFalse(); + } + } +} diff --git a/src/test/java/domain/piece/CannonTest.java b/src/test/java/domain/piece/CannonTest.java new file mode 100644 index 0000000000..b798f2edab --- /dev/null +++ b/src/test/java/domain/piece/CannonTest.java @@ -0,0 +1,81 @@ +package domain.piece; + +import domain.Position; +import domain.Side; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CannonTest { + + @DisplayName("포는 이동한 경로를 반환한다.") + @Test + void 포는_이동한_경로를_반환한다() { + // given + Position sourcePosition = Position.of(1, 1); + Position targetPosition = Position.of(1, 5); + Piece piece = PieceType.CANNON.create(Side.HAN); + + // when + List positions = piece.findRoute(sourcePosition, targetPosition); + + // then + List expected = List.of(Position.of(1, 2), Position.of(1, 3), Position.of(1, 4)); + for (int i = 0; i < 3; i++) { + Assertions.assertThat(positions.get(i)).isEqualTo(expected.get(i)); + } + } + + @DisplayName("포는 목적지까지 이동할 수 없으면 예외를 발생한다.") + @Test + void 포는_목적지까지_이동할_수_없으면_예외를_발생한다() { + // given + Position sourcePosition = Position.of(1, 1); + Position targetPosition = Position.of(2, 5); + Piece piece = PieceType.CANNON.create(Side.CHO); + + // when & then + Assertions.assertThatThrownBy(() -> piece.findRoute(sourcePosition, targetPosition)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이동할 수 없는 목적지입니다."); + } + + @DisplayName("포 이동 경로에 포를 제외한 기물이 하나 존재해야 한다.") + @Test + void 포_이동_경로에_포를_제외한_기물이_하나_존재해야_한다() { + // given + List pieces = List.of(new Empty(), PieceType.HORSE.create(Side.CHO), new Empty()); + // when + Piece cannon = PieceType.CANNON.create(Side.CHO); + Assertions.assertThatCode(() -> cannon.checkRoute(pieces)) + .doesNotThrowAnyException(); + } + + @DisplayName("포 이동 경로에 포가 존재하면 예외를 발생한다.") + @Test + void 포_이동_경로에_포가_존재하면_예외를_발생한다() { + // given + List pieces = List.of(new Empty(), PieceType.CANNON.create(Side.CHO), new Empty()); + Piece cannon = PieceType.CANNON.create(Side.CHO); + + // when & then + Assertions.assertThatThrownBy(() -> cannon.checkRoute(pieces)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("포를 넘어갈 수 없습니다."); + } + + @DisplayName("포 이동 경로에 기물이 두 개 이상 존재하면 예외를 발생한다.") + @Test + void 포_이동_경로에_기물이_두_개_이상_존재하면_예외를_발생한다() { + // given + List pieces = List.of(new Empty(), PieceType.HORSE.create(Side.CHO), + PieceType.ELEPHANT.create(Side.CHO)); + Piece cannon = PieceType.CANNON.create(Side.HAN); + + // when & then + Assertions.assertThatThrownBy(() -> cannon.checkRoute(pieces)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이동할 수 없는 목적지입니다."); + } +} diff --git a/src/test/java/domain/piece/ChariotTest.java b/src/test/java/domain/piece/ChariotTest.java new file mode 100644 index 0000000000..5874ab9018 --- /dev/null +++ b/src/test/java/domain/piece/ChariotTest.java @@ -0,0 +1,45 @@ +package domain.piece; + +import domain.Position; +import domain.Side; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ChariotTest { + + private static final Side IRRELEVANT_SIDE = Side.CHO; + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "2, 3, 2, 7", + "8, 3, 2, 3" + }) + @DisplayName("차가 이동 가능한 위치로 이동하면 경로를 반환한다") + void 차가_이동_가능한_위치로_이동하면_경로를_반환한다( + int sourceX, int sourceY, int targetX, int targetY) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.CHARIOT.create(IRRELEVANT_SIDE); + + Assertions.assertThatCode(() -> piece.findRoute(source, target)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "2, 3, 4, 6", + "8, 3, 5, 9" + }) + @DisplayName("차가 이동 불가능한 위치로 이동하면 예외가 발생한다") + void 차가_이동_불가능한_위치로_이동하면_예외가_발생한다( + int sourceX, int sourceY, int targetX, int targetY) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.CHARIOT.create(IRRELEVANT_SIDE); + + Assertions.assertThatThrownBy(() -> piece.findRoute(source, target)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/domain/piece/ElephantTest.java b/src/test/java/domain/piece/ElephantTest.java new file mode 100644 index 0000000000..9b0a5b1a67 --- /dev/null +++ b/src/test/java/domain/piece/ElephantTest.java @@ -0,0 +1,47 @@ +package domain.piece; + +import domain.Position; +import domain.Side; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ElephantTest { + + private static final Side IRRELEVANT_SIDE = Side.HAN; + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "3, 1, 5, 4", + "3, 1, 1, 4" + }) + @DisplayName("상이 이동 가능한 위치로 이동하면 경로를 반환한다") + void 상이_이동_가능한_위치로_이동하면_경로를_반환한다( + int sourceX, int sourceY, int targetX, int targetY + ) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.ELEPHANT.create(IRRELEVANT_SIDE); + + Assertions.assertThatCode(() -> piece.findRoute(source, target)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "3, 1, 3, 2", + "3, 1, 4, 1" + }) + @DisplayName("상이 이동 불가능한 위치로 이동하면 예외가 발생한다") + void 상이_이동_불가능한_위치로_이동하면_예외가_발생한다( + int sourceX, int sourceY, int targetX, int targetY + ) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.ELEPHANT.create(IRRELEVANT_SIDE); + + Assertions.assertThatThrownBy(() -> piece.findRoute(source, target)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/domain/piece/GuardTest.java b/src/test/java/domain/piece/GuardTest.java new file mode 100644 index 0000000000..e0e723593e --- /dev/null +++ b/src/test/java/domain/piece/GuardTest.java @@ -0,0 +1,48 @@ +package domain.piece; + +import domain.Position; +import domain.Side; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class GuardTest { + + private static final Side IRRELEVANT_SIDE = Side.HAN; + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "4, 1, 4, 2", + "6, 1, 5, 1", + "6, 2, 6, 1" + }) + @DisplayName("사가 이동 가능한 위치로 이동하면 경로를 반환한다.") + void 상이_이동_가능한_위치로_이동하면_경로를_반환한다( + int sourceX, int sourceY, int targetX, int targetY + ) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.GUARD.create(IRRELEVANT_SIDE); + + Assertions.assertThatCode(() -> piece.findRoute(source, target)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "4, 1, 4, 3", + "6, 1, 4, 1" + }) + @DisplayName("사가 이동 불가능한 위치로 이동하면 예외가 발생한다.") + void 상이_이동_불가능한_위치로_이동하면_예외가_발생한다( + int sourceX, int sourceY, int targetX, int targetY + ) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.GUARD.create(IRRELEVANT_SIDE); + + Assertions.assertThatThrownBy(() -> piece.findRoute(source, target)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/domain/piece/HorseTest.java b/src/test/java/domain/piece/HorseTest.java new file mode 100644 index 0000000000..c151bdfe38 --- /dev/null +++ b/src/test/java/domain/piece/HorseTest.java @@ -0,0 +1,45 @@ +package domain.piece; + +import domain.Position; +import domain.Side; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class HorseTest { + + private static final Side IRRELEVANT_SIDE = Side.HAN; + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "3, 2, 1, 1", + "3, 2, 5, 1" + }) + @DisplayName("마가 이동 가능한 위치로 이동하면 경로를 반환한다") + void 마가_이동_가능한_위치로_이동하면_경로를_반환한다( + int sourceX, int sourceY, int targetX, int targetY) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.HORSE.create(IRRELEVANT_SIDE); + + Assertions.assertThatCode(() -> piece.findRoute(source, target)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "3, 2, 1, 5", + "3, 2, 3, 5" + }) + @DisplayName("마가 이동 불가능한 위치로 이동하면 예외가 발생한다") + void 마가_이동_불가능한_위치로_이동하면_예외가_발생한다( + int sourceX, int sourceY, int targetX, int targetY) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.HORSE.create(IRRELEVANT_SIDE); + + Assertions.assertThatThrownBy(() -> piece.findRoute(source, target)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/domain/piece/KingTest.java b/src/test/java/domain/piece/KingTest.java new file mode 100644 index 0000000000..e2c8875f58 --- /dev/null +++ b/src/test/java/domain/piece/KingTest.java @@ -0,0 +1,46 @@ +package domain.piece; + +import domain.Position; +import domain.Side; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class KingTest { + + private static final Side IRRELEVANT_SIDE = Side.HAN; + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "5, 2, 5, 3", + "5, 2, 5, 1", + "5, 2, 4, 2", + "5, 2, 6, 2" + }) + @DisplayName("궁이 이동 가능한 위치로 이동하면 경로를 반환한다") + void 궁이_이동_가능한_위치로_이동하면_경로를_반환한다( + int sourceX, int sourceY, int targetX, int targetY) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.KING.create(IRRELEVANT_SIDE); + + Assertions.assertThatCode(() -> piece.findRoute(source, target)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "5, 2, 5, 4" + }) + @DisplayName("궁이 이동 불가능한 위치로 이동하면 예외가 발생한다") + void 궁이_이동_불가능한_위치로_이동하면_예외가_발생한다( + int sourceX, int sourceY, int targetX, int targetY) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.KING.create(IRRELEVANT_SIDE); + + Assertions.assertThatThrownBy(() -> piece.findRoute(source, target)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/domain/piece/PieceTest.java b/src/test/java/domain/piece/PieceTest.java new file mode 100644 index 0000000000..f3278dc739 --- /dev/null +++ b/src/test/java/domain/piece/PieceTest.java @@ -0,0 +1,41 @@ +package domain.piece; + +import domain.Position; +import domain.Side; +import domain.strategy.PathMovement; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PieceTest { + + static class TestPiece extends Piece { + + public TestPiece() { + super(Side.CHO, new PathMovement()); + } + + @Override + public List findRoute(Position sourcePosition, Position targetPosition) { + return List.of(); + } + + @Override + public String getName() { + return ""; + } + } + + @DisplayName("기물 이동 경로는 모두 비어있어야한다.") + @Test + void 기물_이동_경로는_모두_비어있어야한다() { + // given + List pieces = List.of(new Empty(), new Empty()); + // when + Piece piece = new TestPiece(); + Assertions.assertThatCode(() -> piece.checkRoute(pieces)) + .doesNotThrowAnyException(); + // then + } +} diff --git a/src/test/java/domain/piece/SoldierTest.java b/src/test/java/domain/piece/SoldierTest.java new file mode 100644 index 0000000000..07f1ec6a1a --- /dev/null +++ b/src/test/java/domain/piece/SoldierTest.java @@ -0,0 +1,77 @@ +package domain.piece; + +import domain.Position; +import domain.Side; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class SoldierTest { + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "5, 7, 5, 6", + "5, 7, 4, 7", + "5, 7, 6, 7" + }) + @DisplayName("초 병이 이동 가능한 위치로 이동하면 경로를 반환한다") + void 초_병이_이동_가능한_위치로_이동하면_경로를_반환한다( + int sourceX, int sourceY, int targetX, int targetY) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.SOLDIER.create(Side.CHO); + + Assertions.assertThatCode(() -> piece.findRoute(source, target)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "5, 7, 5, 8", + "5, 7, 5, 5" + }) + @DisplayName("초 병이 이동 불가능한 위치로 이동하면 예외가 발생한다") + void 초_병이_이동_불가능한_위치로_이동하면_예외가_발생한다( + int sourceX, int sourceY, int targetX, int targetY) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.SOLDIER.create(Side.CHO); + + Assertions.assertThatThrownBy(() -> piece.findRoute(source, target)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "5, 4, 5, 5", + "5, 4, 4, 4", + "5, 4, 6, 4" + }) + @DisplayName("한 졸이 이동 가능한 위치로 이동하면 경로를 반환한다") + void 한_졸이_이동_가능한_위치로_이동하면_경로를_반환한다( + int sourceX, int sourceY, int targetX, int targetY) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.SOLDIER.create(Side.HAN); + + Assertions.assertThatCode(() -> piece.findRoute(source, target)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest(name = "[{index}] ({0},{1}) -> ({2},{3})") + @CsvSource({ + "5, 4, 5, 3", + "5, 4, 5, 2" + }) + @DisplayName("한 졸이 이동 불가능한 위치로 이동하면 예외가 발생한다") + void 한_졸이_이동_불가능한_위치로_이동하면_예외가_발생한다( + int sourceX, int sourceY, int targetX, int targetY) { + Position source = Position.of(sourceX, sourceY); + Position target = Position.of(targetX, targetY); + Piece piece = PieceType.SOLDIER.create(Side.HAN); + + Assertions.assertThatThrownBy(() -> piece.findRoute(source, target)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/util/ParserTest.java b/src/test/java/util/ParserTest.java new file mode 100644 index 0000000000..dfc1b15b0d --- /dev/null +++ b/src/test/java/util/ParserTest.java @@ -0,0 +1,18 @@ +package util; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ParserTest { + + @ParameterizedTest + @ValueSource(strings = {"a", "10000000000000000000000", "ㅁ"}) + void Integer로_변환되지_않는_값은_예외를_발생한다(String input) { + + // when & then + Assertions.assertThatThrownBy(() -> Parser.parseInput(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("유효한 입력값이 아닙니다."); + } +} \ No newline at end of file