diff --git a/README.md b/README.md index 9775dda0ae..073cb8b127 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,35 @@ # java-janggi -장기 미션 저장소 +## 1.1 장기판 초기화 + +- [x] 장기판은 9행 * 10열 크기이다. (x축은 가로, y축은 세로이다.) + - [x] x좌표는 0 이상 8 이하의 범위를 가진다. (총 9칸) + - [x] y좌표는 0 이상 9 이하의 범위를 가진다. (총 10칸) + - [x] 장기판의 범위를 벗어난 좌표에 접근할 경우, IllegalArgumentException을 발생시킨다. +- [x] 장기판의 좌측 하단을 (0, 0)으로 정의한다. + +- [x] 장기판을 초기화한다. + - [x] 상차림에 맞도록 기물을 배치한다. + - [x] 왼상차림 : 상마상마 + - [x] 오른상차림 : 마상마상 + - [x] 바깥상차림 : 마상상마 + - [x] 안상차림 : 상마마상 + +## 1.2 기물 이동 + +- [x] `궁`은 왼쪽, 오른쪽, 앞쪽, 뒤쪽으로 1칸씩 움직일 수 있다. + - [x] 가로막는 기물이 적의 것일 경우 잡아내고 그 기물이 있던 곳까지 이동할 수 있다. +- [x] `사`는 왼쪽, 오른쪽, 앞쪽, 뒤쪽으로 1칸씩 움직일 수 있다. + - [x] 가로막는 기물이 적의 것일 경우 잡아내고 그 기물이 있던 곳까지 이동할 수 있다. +- [x] `차`는 장기판 위의 곧은 선을 따라 한 방향으로 원하는 만큼 이동 할 수 있다. + - [x] 단, 다른 기물을 뛰어넘을 수는 없다. + - [x] 가로막는 기물이 있는 지점의 바로 앞까지, 혹은 가로막는 기물이 적의 것일 경우 잡아내고 그 기물이 있던 곳까지 이동할 수 있다. +- [x] `포`는 장기판 위의 곧은 선을 따라 한 방향으로 원하는 만큼 이동 가능하지만, 반드시 기물을 한 개 뛰어넘어야만 그 방향으로 이동이 가능하다. + - [x] 단, 경로에 두 개 이상의 기물이 존재하면 이동할 수 없다. + - [x] 단, 포끼리는 서로 뛰어넘을 수 없다. + - [x] 단, 포끼리는 서로 잡을 수 없다. +- [x] `마`는 수직 또는 수평 방향으로 한 칸 갔다가 45도를 꺾어서 대각선으로 한 칸 더 이동한다. + - [x] 경로에 다른 기물이 있을 경우, 이동할 수 없다. +- [x] `상`은 수직 또는 수평 방향으로 한 칸 갔다가 45도를 꺾어서 대각선으로 두 칸 더 이동한다. + - [x] 경로에 다른 기물이 있을 경우, 이동할 수 없다. +- [x] `졸, 병`은 왼쪽, 오른쪽, 앞쪽으로 1칸씩 움직일 수 있으나, 후퇴할 수 없다. diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 0000000000..54a34d3e83 --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,19 @@ +import controller.GameController; +import domain.board.Board; +import domain.board.BoardInitializer; +import view.InputView; + +public class Application { + public static void main(String[] args) { + Board board = new Board(BoardInitializer.init(InputView.readBoardSetting())); + GameController gameController = new GameController(board); + + while (!board.isGameOver()) { + gameController.printBoard(); + gameController.move(); + } + + gameController.printBoard(); + gameController.printWinner(); + } +} diff --git a/src/main/java/controller/GameController.java b/src/main/java/controller/GameController.java new file mode 100644 index 0000000000..13395a742d --- /dev/null +++ b/src/main/java/controller/GameController.java @@ -0,0 +1,51 @@ + +package controller; + +import domain.board.Board; +import domain.board.Position; +import view.InputView; +import view.OutputView; + +public class GameController { + private final Board board; + + public GameController(Board board) { + this.board = board; + } + + public void move() { + while (true) { + try { + Position departure = parsePosition(InputView.readDeparturePosition()); + Position destination = parsePosition(InputView.readDestinationPosition()); + board.move(departure, destination); + return; + } catch (IllegalArgumentException exception) { + OutputView.printError(exception.getMessage()); + } + } + } + + public void printBoard() { + OutputView.printBoard(board); + } + + public void printWinner() { + OutputView.printWinner(board.winner()); + } + + private Position parsePosition(String value) { + String[] tokens = value.split(","); + if (tokens.length != 2) { + throw new IllegalArgumentException("좌표는 x,y 형식으로 입력해야 합니다."); + } + + try { + int column = Integer.parseInt(tokens[0].trim()); + int row = Integer.parseInt(tokens[1].trim()); + return new Position(column, row); + } catch (NumberFormatException exception) { + throw new IllegalArgumentException("좌표는 숫자로 입력해야 합니다."); + } + } +} diff --git a/src/main/java/domain/board/Board.java b/src/main/java/domain/board/Board.java new file mode 100644 index 0000000000..84747a2c81 --- /dev/null +++ b/src/main/java/domain/board/Board.java @@ -0,0 +1,101 @@ +package domain.board; + +import domain.path.PathInfo; +import domain.piece.Camp; +import domain.piece.Piece; +import domain.piece.PieceType; + +import java.util.List; +import java.util.Map; + +public class Board { + private final Map pieces; + private boolean gameOver; + private Camp winner; + + public Board(Map pieces) { + this.pieces = pieces; + } + + public boolean isExistPieceAt(Position position) { + return pieces.containsKey(position); + } + + public Piece pieceAt(Position position) { + if (!isExistPieceAt(position)) { + throw new IllegalArgumentException("해당 위치에 기물이 존재하지 않습니다."); + } + + return pieces.get(position); + } + + public boolean isGameOver() { + return gameOver; + } + + public Camp winner() { + if (!gameOver) { + throw new IllegalStateException("아직 게임이 종료되지 않았습니다."); + } + + return winner; + } + + public void move(Position departure, Position destination) { + validateGameStatus(); + validateDepartureAndDestinationPosition(departure, destination); + + executeMove(departure, destination); + } + + private void validateGameStatus() { + if (gameOver) { + throw new IllegalStateException("이미 종료된 게임입니다."); + } + } + + private void handleCapture(Piece departurePiece, Position destination) { + Piece destinationPiece = pieceAt(destination); + validateCapture(departurePiece, destinationPiece); + updateGameOver(departurePiece, destinationPiece); + } + + private void validateDepartureAndDestinationPosition(Position departure, Position destination) { + if (departure.equals(destination)) { + throw new IllegalArgumentException("출발 위치와 도착 위치가 같습니다."); + } + } + + private void validateCapture(Piece departurePiece, Piece destinationPiece) { + if (departurePiece.isSameCamp(destinationPiece)) { + throw new IllegalArgumentException("같은 진영의 기물은 잡을 수 없습니다."); + } + } + + private List getPath(Position departure, Position destination, Piece piece) { + return piece.getPath(departure, destination).stream() + .map(pos -> new PathInfo(pos, pieces.get(pos))) + .toList(); + } + + private void updateGameOver(Piece departurePiece, Piece destinationPiece) { + if (destinationPiece.isSameType(PieceType.GENERAL)) { + gameOver = true; + winner = departurePiece.getCamp(); + } + } + + private void executeMove(Position departure, Position destination) { + Piece departurePiece = pieceAt(departure); + List path = getPath(departure, destination, departurePiece); + + departurePiece.validateBlockingPiece(path, destination); + + if (isExistPieceAt(destination)) { + handleCapture(pieceAt(departure), destination); + } + + pieces.remove(departure); + pieces.put(destination, departurePiece); + } +} diff --git a/src/main/java/domain/board/BoardInitializer.java b/src/main/java/domain/board/BoardInitializer.java new file mode 100644 index 0000000000..910fcee925 --- /dev/null +++ b/src/main/java/domain/board/BoardInitializer.java @@ -0,0 +1,99 @@ +package domain.board; + +import domain.piece.Camp; +import domain.piece.Piece; +import domain.piece.PieceType; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BoardInitializer { + private static final int CHO_BASE_ROW = 0; + private static final int CHO_GENERAL_ROW = CHO_BASE_ROW + 1; + private static final int CHO_CANNON_ROW = CHO_BASE_ROW + 2; + private static final int CHO_SOLDIER_ROW = CHO_BASE_ROW + 3; + + private static final int HAN_BASE_ROW = 9; + private static final int HAN_GENERAL_ROW = HAN_BASE_ROW - 1; + private static final int HAN_CANNON_ROW = HAN_BASE_ROW - 2; + private static final int HAN_SOLDIER_ROW = HAN_BASE_ROW - 3; + + private static final int GENERAL_COLUMN = 4; + private static final List GUARD_COLUMNS = List.of(3, 5); + private static final List CHARIOT_COLUMNS = List.of(0, 8); + private static final List CANNON_COLUMNS = List.of(1, 7); + private static final List HORSE_AND_ELEPHANT_COLUMNS = List.of(1, 2, 6, 7); + private static final List SOLDIER_COLUMNS = List.of(0, 2, 4, 6, 8); + + public static Map init(BoardSetting boardSetting) { + Map pieces = new HashMap<>(); + + pieces.putAll(createGeneral()); + pieces.putAll(createGuard()); + pieces.putAll(createChariot()); + pieces.putAll(createCannon()); + pieces.putAll(createHorseAndElephant(boardSetting)); + pieces.putAll(createSoldier()); + + return pieces; + } + + private static Map createGeneral() { + Map pieces = new HashMap<>(); + pieces.put(new Position(GENERAL_COLUMN, CHO_GENERAL_ROW), Piece.of(Camp.CHO, PieceType.GENERAL)); + pieces.put(new Position(GENERAL_COLUMN, HAN_GENERAL_ROW), Piece.of(Camp.HAN, PieceType.GENERAL)); + + return pieces; + } + + private static Map createGuard() { + Map pieces = new HashMap<>(); + for (Integer column : GUARD_COLUMNS) { + pieces.put(new Position(column, CHO_BASE_ROW), Piece.of(Camp.CHO, PieceType.GUARD)); + pieces.put(new Position(column, HAN_BASE_ROW), Piece.of(Camp.HAN, PieceType.GUARD)); + } + + return pieces; + } + + private static Map createChariot() { + Map pieces = new HashMap<>(); + for (Integer column : CHARIOT_COLUMNS) { + pieces.put(new Position(column, CHO_BASE_ROW), Piece.of(Camp.CHO, PieceType.CHARIOT)); + pieces.put(new Position(column, HAN_BASE_ROW), Piece.of(Camp.HAN, PieceType.CHARIOT)); + } + + return pieces; + } + + private static Map createCannon() { + Map pieces = new HashMap<>(); + for (Integer column : CANNON_COLUMNS) { + pieces.put(new Position(column, CHO_CANNON_ROW), Piece.of(Camp.CHO, PieceType.CANNON)); + pieces.put(new Position(column, HAN_CANNON_ROW), Piece.of(Camp.HAN, PieceType.CANNON)); + } + + return pieces; + } + + private static Map createHorseAndElephant(BoardSetting boardSetting) { + Map pieces = new HashMap<>(); + for (int i = 0; i < boardSetting.piecesArrangement().size(); i++) { + pieces.put(new Position(HORSE_AND_ELEPHANT_COLUMNS.get(i), CHO_BASE_ROW), Piece.of(Camp.CHO, boardSetting.piecesArrangement().get(i))); + pieces.put(new Position(HORSE_AND_ELEPHANT_COLUMNS.get(i), HAN_BASE_ROW), Piece.of(Camp.HAN, boardSetting.piecesArrangement().get(i))); + } + + return pieces; + } + + private static Map createSoldier() { + Map pieces = new HashMap<>(); + for (Integer column : SOLDIER_COLUMNS) { + pieces.put(new Position(column, CHO_SOLDIER_ROW), Piece.of(Camp.CHO, PieceType.SOLDIER)); + pieces.put(new Position(column, HAN_SOLDIER_ROW), Piece.of(Camp.HAN, PieceType.SOLDIER)); + } + + return pieces; + } +} diff --git a/src/main/java/domain/board/BoardSetting.java b/src/main/java/domain/board/BoardSetting.java new file mode 100644 index 0000000000..4e86793556 --- /dev/null +++ b/src/main/java/domain/board/BoardSetting.java @@ -0,0 +1,35 @@ +package domain.board; + +import domain.piece.PieceType; + +import java.util.List; + +import static domain.piece.PieceType.ELEPHANT; +import static domain.piece.PieceType.HORSE; + +public enum BoardSetting { + LEFT_ELEPHANT_SET_UP(List.of(ELEPHANT, HORSE, ELEPHANT, HORSE)), + RIGHT_ELEPHANT_SET_UP(List.of(HORSE, ELEPHANT, HORSE, ELEPHANT)), + OUTER_ELEPHANT_SET_UP(List.of(HORSE, ELEPHANT, ELEPHANT, HORSE)), + INNER_ELEPHANT_SET_UP(List.of(ELEPHANT, HORSE, HORSE, ELEPHANT)); + + private final List piecesArrangement; + + BoardSetting(List pieceTypes) { + this.piecesArrangement = pieceTypes; + } + + public List piecesArrangement() { + return piecesArrangement; + } + + public static BoardSetting from(String value) { + return switch (value.trim()) { + case "1", "왼상차림" -> LEFT_ELEPHANT_SET_UP; + case "2", "오른상차림" -> RIGHT_ELEPHANT_SET_UP; + case "3", "바깥상차림" -> OUTER_ELEPHANT_SET_UP; + case "4", "안상차림" -> INNER_ELEPHANT_SET_UP; + default -> throw new IllegalArgumentException("상차림은 1~4 또는 이름으로 입력해야 합니다."); + }; + } +} diff --git a/src/main/java/domain/board/Position.java b/src/main/java/domain/board/Position.java new file mode 100644 index 0000000000..77bec94338 --- /dev/null +++ b/src/main/java/domain/board/Position.java @@ -0,0 +1,38 @@ +package domain.board; + +public record Position(int column, int row) { + private static final int MIN_POSITION = 0; + private static final int MAX_COLUMN = 8; + private static final int MAX_ROW = 9; + + public Position { + validateColumn(column); + validateRow(row); + } + + public Position move(int deltaX, int deltaY) { + return new Position(this.column + deltaX, this.row + deltaY); + } + + public int calculateDeltaX(Position destination) { + return destination.column - this.column; + } + + public int calculateDeltaY(Position destination) { + return destination.row - this.row; + } + + private void validateColumn(int column) { + if (column < MIN_POSITION || column > MAX_COLUMN) { + throw new IllegalArgumentException( + String.format("[ERROR] x좌표는 %d에서 %d 사이입니다.", MIN_POSITION, MAX_COLUMN)); + } + } + + private void validateRow(int row) { + if (row < MIN_POSITION || row > MAX_ROW) { + throw new IllegalArgumentException( + String.format("[ERROR] y좌표는 %d에서 %d 사이입니다.", MIN_POSITION, MAX_ROW)); + } + } +} diff --git a/src/main/java/domain/path/Direction.java b/src/main/java/domain/path/Direction.java new file mode 100644 index 0000000000..66db303969 --- /dev/null +++ b/src/main/java/domain/path/Direction.java @@ -0,0 +1,74 @@ +package domain.path; + +public enum Direction { + UP(0, 1), + DOWN(0, -1), + RIGHT(1, 0), + LEFT(-1, 0), + NORTHEAST(1, 1), + NORTHWEST(-1, 1), + SOUTHEAST(1, -1), + SOUTHWEST(-1, -1); + + private final int deltaX; + private final int deltaY; + + Direction(int deltaX, int deltaY) { + this.deltaX = deltaX; + this.deltaY = deltaY; + } + + public static Direction decideDirection(int deltaX, int deltaY) { + if (deltaX == 0 && deltaY == 0) { + throw new IllegalArgumentException("출발 위치와 도착 위치가 같습니다."); + } + + if (deltaX == 0 || deltaY == 0) { + return decideStraightDirection(deltaX, deltaY); + } + + if (Math.abs(deltaX) == Math.abs(deltaY)) { + return decideDiagonalDirection(deltaX, deltaY); + } + + throw new IllegalArgumentException("이동할 수 없는 방향입니다."); + } + + private static Direction decideStraightDirection(int deltaX, int deltaY) { + if (deltaX > 0) { + return RIGHT; + } + + if (deltaX < 0) { + return LEFT; + } + + if (deltaY > 0) { + return UP; + } + + return DOWN; + } + + private static Direction decideDiagonalDirection(int deltaX, int deltaY) { + if (deltaX > 0) { + if (deltaY > 0) { + return NORTHEAST; + } + return SOUTHEAST; + } + + if (deltaY > 0) { + return NORTHWEST; + } + return SOUTHWEST; + } + + public int getDeltaX() { + return deltaX; + } + + public int getDeltaY() { + return deltaY; + } +} diff --git a/src/main/java/domain/path/PathGenerator.java b/src/main/java/domain/path/PathGenerator.java new file mode 100644 index 0000000000..1dbdaa35ec --- /dev/null +++ b/src/main/java/domain/path/PathGenerator.java @@ -0,0 +1,32 @@ +package domain.path; + +import domain.board.Position; + +import java.util.ArrayList; +import java.util.List; + +public class PathGenerator { + public static List generateStraightPath(Position departure, Position destination, Direction direction) { + List paths = new ArrayList<>(); + + Position current = departure; + while (!current.equals(destination)) { + current = current.move(direction.getDeltaX(), direction.getDeltaY()); + paths.add(current); + } + + return paths; + } + + public static List generateComplexPath(Position departure, List directions) { + List paths = new ArrayList<>(); + + Position current = departure; + for (Direction direction : directions) { + current = current.move(direction.getDeltaX(), direction.getDeltaY()); + paths.add(current); + } + + return paths; + } +} diff --git a/src/main/java/domain/path/PathInfo.java b/src/main/java/domain/path/PathInfo.java new file mode 100644 index 0000000000..8576449fed --- /dev/null +++ b/src/main/java/domain/path/PathInfo.java @@ -0,0 +1,15 @@ +package domain.path; + +import domain.board.Position; +import domain.piece.Piece; +import domain.piece.PieceType; + +public record PathInfo(Position position, Piece piece) { + public boolean hasPiece(){ + return piece != null; + } + + public boolean isPieceType(PieceType type) { + return hasPiece() && piece.isSameType(type); + } +} diff --git a/src/main/java/domain/piece/Camp.java b/src/main/java/domain/piece/Camp.java new file mode 100644 index 0000000000..9ab20a9671 --- /dev/null +++ b/src/main/java/domain/piece/Camp.java @@ -0,0 +1,18 @@ +package domain.piece; + +import domain.path.Direction; + +public enum Camp { + CHO(Direction.UP), + HAN(Direction.DOWN); + + private final Direction forwardDirection; + + Camp(Direction forwardDirection) { + this.forwardDirection = forwardDirection; + } + + public Direction getForwardDirection() { + return forwardDirection; + } +} diff --git a/src/main/java/domain/piece/Piece.java b/src/main/java/domain/piece/Piece.java new file mode 100644 index 0000000000..f26b4bff23 --- /dev/null +++ b/src/main/java/domain/piece/Piece.java @@ -0,0 +1,59 @@ +package domain.piece; + +import domain.path.PathInfo; +import domain.board.Position; +import domain.piece.strategy.MoveStrategy; + +import java.util.List; +import java.util.Objects; + +public class Piece { + private final Camp camp; + private final PieceType pieceType; + private final MoveStrategy moveStrategy; + + public Piece(Camp camp, PieceType pieceType, MoveStrategy moveStrategy) { + this.camp = camp; + this.pieceType = pieceType; + this.moveStrategy = moveStrategy; + } + + public static Piece of(Camp camp, PieceType pieceType) { + return new Piece(camp, pieceType, pieceType.moveStrategy(camp)); + } + + public boolean isSameCamp(Piece otherPiece) { + return otherPiece.camp.equals(camp); + } + + public boolean isSameType(PieceType otherPieceType) { + return pieceType.equals(otherPieceType); + } + + public Camp getCamp() { + return camp; + } + + public PieceType getPieceType() { + return pieceType; + } + + public List getPath(Position departure, Position destination) { + return moveStrategy.getPath(departure, destination); + } + + public void validateBlockingPiece(List pathInfos, Position destination) { + moveStrategy.validateBlockingPiece(pathInfos, destination); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Piece piece)) return false; + return camp == piece.camp && pieceType == piece.pieceType; + } + + @Override + public int hashCode() { + return Objects.hash(camp, pieceType); + } +} diff --git a/src/main/java/domain/piece/PieceType.java b/src/main/java/domain/piece/PieceType.java new file mode 100644 index 0000000000..e865e494f4 --- /dev/null +++ b/src/main/java/domain/piece/PieceType.java @@ -0,0 +1,25 @@ +package domain.piece; + +import domain.piece.strategy.*; + +import java.util.function.Function; + +public enum PieceType { + GENERAL(camp -> new GeneralMoveStrategy()), + GUARD(camp -> new GeneralMoveStrategy()), + CHARIOT(camp -> new ChariotMoveStrategy()), + CANNON(camp -> new CannonMoveStrategy()), + HORSE(camp -> new HorseMoveStrategy()), + ELEPHANT(camp -> new ElephantMoveStrategy()), + SOLDIER(camp -> new SoldierMoveStrategy(camp.getForwardDirection())); + + private final Function strategyFactory; + + PieceType(Function strategyFactory) { + this.strategyFactory = strategyFactory; + } + + public MoveStrategy moveStrategy(Camp camp) { + return strategyFactory.apply(camp); + } +} diff --git a/src/main/java/domain/piece/strategy/CannonMoveStrategy.java b/src/main/java/domain/piece/strategy/CannonMoveStrategy.java new file mode 100644 index 0000000000..67fa53181f --- /dev/null +++ b/src/main/java/domain/piece/strategy/CannonMoveStrategy.java @@ -0,0 +1,34 @@ +package domain.piece.strategy; + +import domain.board.Position; +import domain.path.PathGenerator; +import domain.path.PathInfo; +import domain.path.Direction; +import domain.piece.PieceType; + +import java.util.List; + +public class CannonMoveStrategy implements MoveStrategy { + @Override + public List getPath(Position departure, Position destination) { + int deltaX = departure.calculateDeltaX(destination); + int deltaY = departure.calculateDeltaY(destination); + + if (deltaX != 0 && deltaY != 0) { + throw new IllegalArgumentException("포는 직선 방향으로만 이동할 수 있습니다."); + } + + Direction direction = Direction.decideDirection(deltaX, deltaY); + return PathGenerator.generateStraightPath(departure, destination, direction); + } + + @Override + public void validateBlockingPiece(List pathInfos, Position destination) { + if (pathInfos.size() != 2) { + throw new IllegalArgumentException("포는 반드시 하나의 기물만을 이동할 수 있습니다."); + } + if (pathInfos.stream().anyMatch(pathInfo -> pathInfo.isPieceType(PieceType.CANNON))) { + throw new IllegalArgumentException("포는 포를 넘거나 잡을 수 없습니다."); + } + } +} diff --git a/src/main/java/domain/piece/strategy/ChariotMoveStrategy.java b/src/main/java/domain/piece/strategy/ChariotMoveStrategy.java new file mode 100644 index 0000000000..3edddb544a --- /dev/null +++ b/src/main/java/domain/piece/strategy/ChariotMoveStrategy.java @@ -0,0 +1,34 @@ +package domain.piece.strategy; + +import domain.path.PathGenerator; +import domain.path.PathInfo; +import domain.board.Position; +import domain.path.Direction; + +import java.util.List; + +public class ChariotMoveStrategy implements MoveStrategy { + @Override + public List getPath(Position departure, Position destination) { + int deltaX = departure.calculateDeltaX(destination); + int deltaY = departure.calculateDeltaY(destination); + + if (deltaX != 0 && deltaY != 0) { + throw new IllegalArgumentException("차는 직선 방향으로만 이동할 수 있습니다."); + } + + Direction direction = Direction.decideDirection(deltaX, deltaY); + return PathGenerator.generateStraightPath(departure, destination, direction); + } + + @Override + public void validateBlockingPiece(List pathInfos, Position destination) { + boolean hasBlockingPiece = pathInfos.stream() + .filter(path -> !path.position().equals(destination)) + .anyMatch(PathInfo::hasPiece); + + if (hasBlockingPiece) { + throw new IllegalArgumentException("이동 경로에 있는 다른 기물을 뛰어넘을 수 없습니다."); + } + } +} diff --git a/src/main/java/domain/piece/strategy/ElephantMoveStrategy.java b/src/main/java/domain/piece/strategy/ElephantMoveStrategy.java new file mode 100644 index 0000000000..c03c72a3f6 --- /dev/null +++ b/src/main/java/domain/piece/strategy/ElephantMoveStrategy.java @@ -0,0 +1,54 @@ +package domain.piece.strategy; + +import domain.board.Position; +import domain.path.Direction; +import domain.path.PathGenerator; +import domain.path.PathInfo; + +import java.util.List; + +public class ElephantMoveStrategy implements MoveStrategy { + @Override + public List getPath(Position departure, Position destination) { + int deltaX = departure.calculateDeltaX(destination); + int deltaY = departure.calculateDeltaY(destination); + + if (!isElephantMove(deltaX, deltaY)) { + throw new IllegalArgumentException("상은 직진 후, 대각선 방향으로 두 칸 이동 가능합니다."); + } + + Direction firstDirection = decidefirstDirection(deltaX, deltaY); + Position firstNode = departure.move(firstDirection.getDeltaX(), firstDirection.getDeltaY()); + Direction secondDirection = Direction.decideDirection( + firstNode.calculateDeltaX(destination), + firstNode.calculateDeltaY(destination) + ); + + return PathGenerator.generateComplexPath(departure, List.of(firstDirection, secondDirection, secondDirection)); + } + + @Override + public void validateBlockingPiece(List pathInfos, Position destination) { + boolean hasBlockingPiece = pathInfos.stream() + .filter(path -> !path.position().equals(destination)) + .anyMatch(PathInfo::hasPiece); + + if (hasBlockingPiece) { + throw new IllegalArgumentException("이동 경로에 있는 다른 기물을 뛰어넘을 수 없습니다."); + } + } + + private boolean isElephantMove(int deltaX, int deltaY) { + int absoluteX = Math.abs(deltaX); + int absoluteY = Math.abs(deltaY); + + return (absoluteX == 2 && absoluteY == 3) || (absoluteX == 3 && absoluteY == 2); + } + + private Direction decidefirstDirection(int deltaX, int deltaY) { + if ((Math.abs(deltaX) == 3)) { + return Direction.decideDirection(deltaX, 0); + } + return Direction.decideDirection(0, deltaY); + } +} diff --git a/src/main/java/domain/piece/strategy/GeneralMoveStrategy.java b/src/main/java/domain/piece/strategy/GeneralMoveStrategy.java new file mode 100644 index 0000000000..1f4b167cca --- /dev/null +++ b/src/main/java/domain/piece/strategy/GeneralMoveStrategy.java @@ -0,0 +1,38 @@ +package domain.piece.strategy; + +import domain.board.Position; +import domain.path.PathGenerator; +import domain.path.PathInfo; +import domain.path.Direction; + +import java.util.List; + +public class GeneralMoveStrategy implements MoveStrategy { + @Override + public List getPath(Position departure, Position destination) { + int deltaX = departure.calculateDeltaX(destination); + int deltaY = departure.calculateDeltaY(destination); + + if (deltaX != 0 && deltaY != 0) { + throw new IllegalArgumentException("궁/사는 직선 방향으로만 이동할 수 있습니다."); + } + + if (Math.abs(deltaX) + Math.abs(deltaY) != 1) { + throw new IllegalArgumentException("궁/사는 직선 방향으로 한 칸만 이동 가능합니다."); + } + + Direction direction = Direction.decideDirection(deltaX, deltaY); + return PathGenerator.generateStraightPath(departure, destination, direction); + } + + @Override + public void validateBlockingPiece(List pathInfos, Position destination) { + boolean hasBlockingPiece = pathInfos.stream() + .filter(path -> !path.position().equals(destination)) + .anyMatch(PathInfo::hasPiece); + + if (hasBlockingPiece) { + throw new IllegalArgumentException("이동 경로에 있는 다른 기물을 뛰어넘을 수 없습니다."); + } + } +} diff --git a/src/main/java/domain/piece/strategy/HorseMoveStrategy.java b/src/main/java/domain/piece/strategy/HorseMoveStrategy.java new file mode 100644 index 0000000000..5ec94fb78e --- /dev/null +++ b/src/main/java/domain/piece/strategy/HorseMoveStrategy.java @@ -0,0 +1,54 @@ +package domain.piece.strategy; + +import domain.board.Position; +import domain.path.Direction; +import domain.path.PathGenerator; +import domain.path.PathInfo; + +import java.util.List; + +public class HorseMoveStrategy implements MoveStrategy { + @Override + public List getPath(Position departure, Position destination) { + int deltaX = departure.calculateDeltaX(destination); + int deltaY = departure.calculateDeltaY(destination); + + if (!isHorseMove(deltaX, deltaY)) { + throw new IllegalArgumentException("마는 직진 후, 대각선 방향으로 한 칸 이동 가능합니다."); + } + + Direction firstDirection = decidefirstDirection(deltaX, deltaY); + Position node = departure.move(firstDirection.getDeltaX(), firstDirection.getDeltaY()); + Direction secondDirection = Direction.decideDirection( + node.calculateDeltaX(destination), + node.calculateDeltaY(destination) + ); + + return PathGenerator.generateComplexPath(departure, List.of(firstDirection, secondDirection)); + } + + @Override + public void validateBlockingPiece(List pathInfos, Position destination) { + boolean hasBlockingPiece = pathInfos.stream() + .filter(path -> !path.position().equals(destination)) + .anyMatch(PathInfo::hasPiece); + + if (hasBlockingPiece) { + throw new IllegalArgumentException("이동 경로에 있는 다른 기물을 뛰어넘을 수 없습니다."); + } + } + + private boolean isHorseMove(int deltaX, int deltaY) { + int absoluteX = Math.abs(deltaX); + int absoluteY = Math.abs(deltaY); + + return (absoluteX == 1 && absoluteY == 2) || (absoluteX == 2 && absoluteY == 1); + } + + private Direction decidefirstDirection(int deltaX, int deltaY){ + if((Math.abs(deltaX) == 2)){ + return Direction.decideDirection(deltaX, 0); + } + return Direction.decideDirection(0, deltaY); + } +} diff --git a/src/main/java/domain/piece/strategy/MoveStrategy.java b/src/main/java/domain/piece/strategy/MoveStrategy.java new file mode 100644 index 0000000000..b2979491ce --- /dev/null +++ b/src/main/java/domain/piece/strategy/MoveStrategy.java @@ -0,0 +1,11 @@ +package domain.piece.strategy; + +import domain.path.PathInfo; +import domain.board.Position; + +import java.util.List; + +public interface MoveStrategy { + List getPath(Position departure, Position destination); + void validateBlockingPiece(List pathInfos, Position destination); +} diff --git a/src/main/java/domain/piece/strategy/SoldierMoveStrategy.java b/src/main/java/domain/piece/strategy/SoldierMoveStrategy.java new file mode 100644 index 0000000000..b50558bb76 --- /dev/null +++ b/src/main/java/domain/piece/strategy/SoldierMoveStrategy.java @@ -0,0 +1,50 @@ +package domain.piece.strategy; + +import domain.board.Position; +import domain.path.PathGenerator; +import domain.path.PathInfo; +import domain.path.Direction; + +import java.util.List; + +public class SoldierMoveStrategy implements MoveStrategy { + private final Direction forwardDirection; + + public SoldierMoveStrategy(Direction forwardDirection) { + this.forwardDirection = forwardDirection; + } + + @Override + public List getPath(Position departure, Position destination) { + int deltaX = departure.calculateDeltaX(destination); + int deltaY = departure.calculateDeltaY(destination); + + if (deltaX != 0 && deltaY != 0) { + throw new IllegalArgumentException("졸/병은 직선 방향으로만 이동할 수 있습니다."); + } + + if (Math.abs(deltaX) + Math.abs(deltaY) != 1) { + throw new IllegalArgumentException("졸/병은 직선 방향으로 한 칸만 이동 가능합니다."); + } + + Direction direction = Direction.decideDirection(deltaX, deltaY); + List allowedDirections = List.of(Direction.LEFT, Direction.RIGHT, forwardDirection); + + if (!allowedDirections.contains(direction)) { + throw new IllegalArgumentException("졸/병은 후퇴할 수 없습니다."); + } + + return PathGenerator.generateStraightPath(departure, destination, direction); + } + + @Override + public void validateBlockingPiece(List pathInfos, Position destination) { + boolean hasBlockingPiece = pathInfos.stream() + .filter(path -> !path.position().equals(destination)) + .anyMatch(PathInfo::hasPiece); + + if (hasBlockingPiece) { + throw new IllegalArgumentException("이동 경로에 있는 다른 기물을 뛰어넘을 수 없습니다."); + } + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 0000000000..dade14db10 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,25 @@ +package view; + +import domain.board.BoardSetting; + +import java.util.Scanner; + +public class InputView { + private static final Scanner scanner = new Scanner(System.in); + + public static BoardSetting readBoardSetting() { + System.out.println("장기판 상차림을 선택해주세요."); + System.out.println("1. 왼상차림 2. 오른상차림 3. 바깥상차림 4. 안상차림"); + return BoardSetting.from(scanner.nextLine()); + } + + public static String readDeparturePosition() { + System.out.println("이동시킬 기물이 위치한 좌표를 입력해주세요. (ex) 0,0"); + return scanner.nextLine(); + } + + public static String readDestinationPosition() { + System.out.println("이동할 좌표를 입력해주세요. (ex) 0,0"); + 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..f4fa2dbfbe --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,62 @@ +package view; + +import domain.board.Board; +import domain.board.Position; +import domain.piece.Camp; +import domain.piece.Piece; +import domain.piece.PieceType; + +public class OutputView { + private static final int MAX_ROW = 9; + private static final int MAX_COLUMN = 8; + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_CHO = "\u001B[34m"; + private static final String ANSI_HAN = "\u001B[31m"; + private static final String ANSI_GUIDE = "\u001B[90m"; + + public static void printBoard(Board board) { + System.out.println(ANSI_GUIDE + " 0 1 2 3 4 5 6 7 8" + ANSI_RESET); + + for (int row = MAX_ROW; row >= 0; row--) { + System.out.print(ANSI_GUIDE + row + " |" + ANSI_RESET); + for (int col = 0; col <= MAX_COLUMN; col++) { + if (!board.isExistPieceAt(new Position(col, row))) { + System.out.print(ANSI_GUIDE + "+" + ANSI_RESET); + } else { + Piece piece = board.pieceAt(new Position(col, row)); + System.out.print(colorize(piece.getCamp(), symbolOf(piece.getPieceType(), piece.getCamp()))); + } + if (col < MAX_COLUMN) System.out.print(" "); + } + System.out.println(ANSI_GUIDE + "|" + ANSI_RESET); + } + } + + public static void printError(String message){ + System.out.println(message); + } + + public static void printWinner(Camp winner) { + String winnerName = (winner == Camp.CHO) ? "초" : "한"; + System.out.println(winnerName + "의 승리입니다."); + } + + private static String symbolOf(PieceType pieceType, Camp camp) { + return switch (pieceType) { + case GENERAL -> "궁"; + case GUARD -> "사"; + case CHARIOT -> "차"; + case CANNON -> "포"; + case HORSE -> "마"; + case ELEPHANT -> "상"; + case SOLDIER -> (camp == Camp.CHO) ? "졸" : "병"; + }; + } + + private static String colorize(Camp camp, String symbol) { + if (camp == Camp.CHO) { + return ANSI_CHO + symbol + ANSI_RESET; + } + return ANSI_HAN + symbol + ANSI_RESET; + } +} diff --git a/src/test/java/domain/board/BoardInitializerTest.java b/src/test/java/domain/board/BoardInitializerTest.java new file mode 100644 index 0000000000..b72093e7a6 --- /dev/null +++ b/src/test/java/domain/board/BoardInitializerTest.java @@ -0,0 +1,68 @@ +package domain.board; + +import domain.piece.Camp; +import domain.piece.Piece; +import domain.piece.PieceType; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class BoardInitializerTest { + + @ParameterizedTest + @EnumSource(BoardSetting.class) + void 상차림에_따라_모든_기물이_올바른_위치에_초기화된다(BoardSetting boardSetting) { + Map pieces = BoardInitializer.init(boardSetting); + assertThat(pieces).isEqualTo(createPieces(boardSetting)); + } + + private static Map createPieces(BoardSetting boardSetting) { + Map pieces = new HashMap<>(); + + pieces.put(new Position(4, 1), Piece.of(Camp.CHO, PieceType.GENERAL)); + pieces.put(new Position(4, 8), Piece.of(Camp.HAN, PieceType.GENERAL)); + + pieces.put(new Position(3, 0), Piece.of(Camp.CHO, PieceType.GUARD)); + pieces.put(new Position(5, 0), Piece.of(Camp.CHO, PieceType.GUARD)); + pieces.put(new Position(3, 9), Piece.of(Camp.HAN, PieceType.GUARD)); + pieces.put(new Position(5, 9), Piece.of(Camp.HAN, PieceType.GUARD)); + + pieces.put(new Position(8, 0), Piece.of(Camp.CHO, PieceType.CHARIOT)); + pieces.put(new Position(0, 0), Piece.of(Camp.CHO, PieceType.CHARIOT)); + pieces.put(new Position(0, 9), Piece.of(Camp.HAN, PieceType.CHARIOT)); + pieces.put(new Position(8, 9), Piece.of(Camp.HAN, PieceType.CHARIOT)); + + pieces.put(new Position(1, 2), Piece.of(Camp.CHO, PieceType.CANNON)); + pieces.put(new Position(7, 2), Piece.of(Camp.CHO, PieceType.CANNON)); + pieces.put(new Position(1, 7), Piece.of(Camp.HAN, PieceType.CANNON)); + pieces.put(new Position(7, 7), Piece.of(Camp.HAN, PieceType.CANNON)); + + List horseAndElephantArrangement = boardSetting.piecesArrangement(); + pieces.put(new Position(1, 0), Piece.of(Camp.CHO, horseAndElephantArrangement.get(0))); + pieces.put(new Position(2, 0), Piece.of(Camp.CHO, horseAndElephantArrangement.get(1))); + pieces.put(new Position(6, 0), Piece.of(Camp.CHO, horseAndElephantArrangement.get(2))); + pieces.put(new Position(7, 0), Piece.of(Camp.CHO, horseAndElephantArrangement.get(3))); + pieces.put(new Position(1, 9), Piece.of(Camp.HAN, horseAndElephantArrangement.get(0))); + pieces.put(new Position(2, 9), Piece.of(Camp.HAN, horseAndElephantArrangement.get(1))); + pieces.put(new Position(6, 9), Piece.of(Camp.HAN, horseAndElephantArrangement.get(2))); + pieces.put(new Position(7, 9), Piece.of(Camp.HAN, horseAndElephantArrangement.get(3))); + + pieces.put(new Position(0, 3), Piece.of(Camp.CHO, PieceType.SOLDIER)); + pieces.put(new Position(2, 3), Piece.of(Camp.CHO, PieceType.SOLDIER)); + pieces.put(new Position(4, 3), Piece.of(Camp.CHO, PieceType.SOLDIER)); + pieces.put(new Position(6, 3), Piece.of(Camp.CHO, PieceType.SOLDIER)); + pieces.put(new Position(8, 3), Piece.of(Camp.CHO, PieceType.SOLDIER)); + pieces.put(new Position(0, 6), Piece.of(Camp.HAN, PieceType.SOLDIER)); + pieces.put(new Position(2, 6), Piece.of(Camp.HAN, PieceType.SOLDIER)); + pieces.put(new Position(4, 6), Piece.of(Camp.HAN, PieceType.SOLDIER)); + pieces.put(new Position(6, 6), Piece.of(Camp.HAN, PieceType.SOLDIER)); + pieces.put(new Position(8, 6), Piece.of(Camp.HAN, PieceType.SOLDIER)); + + return pieces; + } +} diff --git a/src/test/java/domain/board/BoardTest.java b/src/test/java/domain/board/BoardTest.java new file mode 100644 index 0000000000..986234412c --- /dev/null +++ b/src/test/java/domain/board/BoardTest.java @@ -0,0 +1,86 @@ +package domain.board; + +import domain.piece.Camp; +import domain.piece.Piece; +import domain.piece.PieceType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.*; + +class BoardTest { + private Board board; + + @BeforeEach + void setUp() { + board = new Board(BoardInitializer.init(BoardSetting.LEFT_ELEPHANT_SET_UP)); + } + + @Test + void 해당_좌표에_기물의_존재_여부를_반환한다() { + assertThat(board.isExistPieceAt(new Position(8, 0))).isTrue(); + assertThat(board.isExistPieceAt(new Position(8, 5))).isFalse(); + } + + @Test + void 해당_좌표에_위치한_기물을_반환한다() { + assertThat(board.pieceAt(new Position(8, 0))).isEqualTo(Piece.of(Camp.CHO, PieceType.CHARIOT)); + } + + @Test + void 이동을_선택한_좌표에_기물이_없을_경우_예외를_던진다() { + assertThatThrownBy(() -> board.pieceAt(new Position(5, 5))).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 출발지와_도착지가_같을_경우_예외를_던진다() { + assertThatThrownBy(() -> board.move(new Position(8, 0), new Position(8, 0))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 도착지에_위치한_기물이_같은_진영의_기물일_경우_예외를_던진다() { + assertThatThrownBy(() -> board.move(new Position(8, 0), new Position(7, 0))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 왕이_잡히면_게임이_종료되고_승자를_반환한다() { + Map pieces = new HashMap<>(); + pieces.put(new Position(4, 8), Piece.of(Camp.CHO, PieceType.CHARIOT)); + pieces.put(new Position(4, 9), Piece.of(Camp.HAN, PieceType.GENERAL)); + Board emptyBoard = new Board(pieces); + + emptyBoard.move(new Position(4, 8), new Position(4, 9)); + + assertThat(emptyBoard.isGameOver()).isTrue(); + assertThat(emptyBoard.winner()).isEqualTo(Camp.CHO); + } + + @Test + void 게임이_종료되면_더_이상_기물을_이동할_수_없다() { + Map pieces = new HashMap<>(); + pieces.put(new Position(4, 8), Piece.of(Camp.CHO, PieceType.CHARIOT)); + pieces.put(new Position(4, 9), Piece.of(Camp.HAN, PieceType.GENERAL)); + Board emptyBoard = new Board(pieces); + + emptyBoard.move(new Position(4, 8), new Position(4, 9)); + + assertThatThrownBy(() -> emptyBoard.move(new Position(4, 9), new Position(4, 8))) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 같은_진영의_기물은_잡을_수_없다() { + Map pieces = new HashMap<>(); + pieces.put(new Position(4, 0), Piece.of(Camp.CHO, PieceType.CHARIOT)); + pieces.put(new Position(4, 1), Piece.of(Camp.CHO, PieceType.SOLDIER)); + Board board = new Board(pieces); + + assertThatThrownBy(() -> board.move(new Position(4, 0), new Position(4, 1))) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/domain/board/PositionTest.java b/src/test/java/domain/board/PositionTest.java new file mode 100644 index 0000000000..2dd2171cb6 --- /dev/null +++ b/src/test/java/domain/board/PositionTest.java @@ -0,0 +1,60 @@ +package domain.board; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + + +class PositionTest { + @Test + void 좌표를_이동시킨다() { + Position position = new Position(0, 0); + assertThat(position.move(1, 2)).isEqualTo(new Position(1, 2)); + } + + @Nested + class 좌표_범위_검증_테스트 { + @Test + void x좌표가_0_미만일_경우_예외를_던진다() { + assertThatThrownBy(() -> new Position(9, 2)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void x좌표가_8을_초과할_경우_예외를_던진다() { + assertThatThrownBy(() -> new Position(-1, 2)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void y좌표가_0_미만일_경우_예외를_던진다() { + assertThatThrownBy(() -> new Position(3, -1)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void y좌표가_9를_초과할_경우_예외를_던진다() { + assertThatThrownBy(() -> new Position(2, 10)) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Nested + class 좌표_차이_계산_테스트 { + Position position = new Position(0, 0); + + @Test + void 두_좌표의_x_증가량을_계산한다() { + assertThat(position.calculateDeltaX(new Position(1, 0))).isEqualTo(1); + } + + @Test + void 두_좌표의_y_증가량을_계산한다() { + assertThat(position.calculateDeltaY(new Position(0, 1))).isEqualTo(1); + + } + } + +} diff --git a/src/test/java/domain/path/DirectionTest.java b/src/test/java/domain/path/DirectionTest.java new file mode 100644 index 0000000000..34d3ae3fd7 --- /dev/null +++ b/src/test/java/domain/path/DirectionTest.java @@ -0,0 +1,89 @@ +package domain.path; + +import domain.board.Position; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DirectionTest { + private Position departure; + + @BeforeEach + void setUp() { + departure = new Position(5, 5); + } + + @Test + void 왼쪽_방향을_판정한다() { + Position destination = new Position(4, 5); + Direction direction = Direction.decideDirection(departure.calculateDeltaX(destination), departure.calculateDeltaY(destination)); + assertThat(direction).isEqualTo(Direction.LEFT); + } + + @Test + void 오른쪽_방향을_판정한다() { + Position destination = new Position(6, 5); + Direction direction = Direction.decideDirection(departure.calculateDeltaX(destination), departure.calculateDeltaY(destination)); + assertThat(direction).isEqualTo(Direction.RIGHT); + } + + @Test + void 위쪽_방향을_판정한다() { + Position destination = new Position(5, 6); + Direction direction = Direction.decideDirection(departure.calculateDeltaX(destination), departure.calculateDeltaY(destination)); + assertThat(direction).isEqualTo(Direction.UP); + } + + @Test + void 아래쪽_방향을_판정한다() { + Position destination = new Position(5, 4); + Direction direction = Direction.decideDirection(departure.calculateDeltaX(destination), departure.calculateDeltaY(destination)); + assertThat(direction).isEqualTo(Direction.DOWN); + } + + @Test + void 북동쪽_방향을_판정한다() { + Position destination = new Position(6, 6); + Direction direction = Direction.decideDirection(departure.calculateDeltaX(destination), departure.calculateDeltaY(destination)); + assertThat(direction).isEqualTo(Direction.NORTHEAST); + } + + @Test + void 북서쪽_방향을_판정한다() { + Position destination = new Position(4, 6); + Direction direction = Direction.decideDirection(departure.calculateDeltaX(destination), departure.calculateDeltaY(destination)); + assertThat(direction).isEqualTo(Direction.NORTHWEST); + } + + @Test + void 남동쪽_방향을_판정한다() { + Position destination = new Position(6, 4); + Direction direction = Direction.decideDirection(departure.calculateDeltaX(destination), departure.calculateDeltaY(destination)); + assertThat(direction).isEqualTo(Direction.SOUTHEAST); + } + + @Test + void 남서쪽_방향을_판정한다() { + Position destination = new Position(4, 4); + Direction direction = Direction.decideDirection(departure.calculateDeltaX(destination), departure.calculateDeltaY(destination)); + assertThat(direction).isEqualTo(Direction.SOUTHWEST); + } + + @Test + void 출발_위치와_도착_위치가_같으면_예외를_던진다() { + assertThatThrownBy(() -> + Direction.decideDirection(departure.calculateDeltaX(departure), + departure.calculateDeltaY(departure))).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 직선_방향과_대각선_방향_모두_아닐_경우_예외를_던진다() { + Position destination = new Position(2, 3); + assertThatThrownBy(() -> + Direction.decideDirection(departure.calculateDeltaX(destination), + departure.calculateDeltaY(destination))).isInstanceOf(IllegalArgumentException.class); + + } +} diff --git a/src/test/java/domain/path/PathGeneratorTest.java b/src/test/java/domain/path/PathGeneratorTest.java new file mode 100644 index 0000000000..0278b833a3 --- /dev/null +++ b/src/test/java/domain/path/PathGeneratorTest.java @@ -0,0 +1,38 @@ +package domain.path; + +import domain.board.Position; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class PathGeneratorTest { + @Test + void 직선_경로를_정상적으로_생성한다() { + Position departure = new Position(0, 0); + Position destination = new Position(0, 2); + Direction direction = Direction.UP; + + List path = PathGenerator.generateStraightPath(departure, destination, direction); + + assertThat(path).containsExactly( + new Position(0, 1), + new Position(0, 2) + ); + } + + @Test + void 복합_경로를_정상적으로_생성한다() { + Position departure = new Position(8, 0); + Position destination = new Position(7, 2); + List directions = List.of(Direction.UP, Direction.NORTHWEST); + + List path = PathGenerator.generateComplexPath(departure, directions); + + assertThat(path).containsExactly( + new Position(8, 1), + new Position(7, 2) + ); + } +} diff --git a/src/test/java/domain/path/PathInfoTest.java b/src/test/java/domain/path/PathInfoTest.java new file mode 100644 index 0000000000..c144a3ab49 --- /dev/null +++ b/src/test/java/domain/path/PathInfoTest.java @@ -0,0 +1,34 @@ +package domain.path; + +import domain.board.Position; +import domain.piece.Camp; +import domain.piece.Piece; +import domain.piece.PieceType; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PathInfoTest { + + @Test + void 해당_위치에_기물이_있는지_반환한다() { + Piece chariot = Piece.of(Camp.CHO, PieceType.CHARIOT); + PathInfo withPiece = new PathInfo(new Position(0, 0), chariot); + assertThat(withPiece.hasPiece()).isTrue(); + + PathInfo empty = new PathInfo(new Position(0, 0), null); + assertThat(empty.hasPiece()).isFalse(); + } + + @Test + void 해당_위치에_존재하는_기물이_같은_종류의_기물인지_반환한다() { + Piece cannon = Piece.of(Camp.CHO, PieceType.CANNON); + PathInfo pathInfo = new PathInfo(new Position(0, 0), cannon); + + assertThat(pathInfo.isPieceType(PieceType.CANNON)).isTrue(); + assertThat(pathInfo.isPieceType(PieceType.CHARIOT)).isFalse(); + + PathInfo empty = new PathInfo(new Position(0, 0), null); + assertThat(empty.isPieceType(PieceType.CANNON)).isFalse(); + } +} diff --git a/src/test/java/domain/piece/PieceTest.java b/src/test/java/domain/piece/PieceTest.java new file mode 100644 index 0000000000..2361b2702b --- /dev/null +++ b/src/test/java/domain/piece/PieceTest.java @@ -0,0 +1,46 @@ +package domain.piece; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PieceTest { + Piece piece; + + @BeforeEach + void setUp() { + piece = Piece.of(Camp.CHO, PieceType.CHARIOT); + } + + @Test + void 같은_진영인지_반환한다() { + assertThat(piece.isSameCamp(Piece.of(Camp.CHO, PieceType.ELEPHANT))).isTrue(); + assertThat(piece.isSameCamp(Piece.of(Camp.HAN, PieceType.SOLDIER))).isFalse(); + } + + @Nested + class 동등성_비교_테스트 { + @Test + void 같은_진영이고_같은_기물_타입이면_동등한_기물로_판단한다() { + assertThat(piece.equals(Piece.of(Camp.CHO, PieceType.CHARIOT))).isTrue(); + } + + @Test + void 진영이_다르면_다른_기물로_판단한다() { + assertThat(piece.equals(Piece.of(Camp.HAN, PieceType.CHARIOT))).isFalse(); + } + + @Test + void 기물_종류가_다르면_다른_기물로_판단한다() { + assertThat(piece.equals(Piece.of(Camp.CHO, PieceType.HORSE))).isFalse(); + } + + @Test + void 동등한_두_객체의_hashcode는_같다() { + assertThat(piece.equals(Piece.of(Camp.CHO, PieceType.CHARIOT))).isTrue(); + assertThat(piece.hashCode()).isEqualTo(Piece.of(Camp.CHO, PieceType.CHARIOT).hashCode()); + } + } +} diff --git a/src/test/java/domain/piece/strategy/CannonMoveStrategyTest.java b/src/test/java/domain/piece/strategy/CannonMoveStrategyTest.java new file mode 100644 index 0000000000..cd0cfecfac --- /dev/null +++ b/src/test/java/domain/piece/strategy/CannonMoveStrategyTest.java @@ -0,0 +1,98 @@ +package domain.piece.strategy; + +import domain.board.Position; +import domain.path.PathInfo; +import domain.piece.Camp; +import domain.piece.Piece; +import domain.piece.PieceType; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CannonMoveStrategyTest { + private final MoveStrategy cannonMoveStrategy = new CannonMoveStrategy(); + + @Test + void 포는_세로_직선_방향의_이동_경로를_가진다() { + Position from = new Position(8, 0); + Position to = new Position(8, 2); + + List path = cannonMoveStrategy.getPath(from, to); + + assertThat(path).containsExactly( + new Position(8, 1), + new Position(8, 2)); + } + + @Test + void 포는_가로_직선_방향의_이동_경로를_가진다() { + Position from = new Position(8, 0); + Position to = new Position(6, 0); + + List path = cannonMoveStrategy.getPath(from, to); + + assertThat(path).containsExactly( + new Position(7, 0), + new Position(6, 0)); + } + + @Test + void 포는_대각선_방향으로_이동할_수_없다() { + Position from = new Position(8, 0); + Position to = new Position(7, 1); + + assertThatThrownBy(() -> cannonMoveStrategy.getPath(from, to)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 포는_경로에_다른_기물이_없으면_이동할_수_없다() { + Position to = new Position(8, 2); + + List pathInfos = new ArrayList<>(); + pathInfos.add(new PathInfo(to, Piece.of(Camp.CHO, PieceType.CHARIOT))); + + assertThatThrownBy(() -> cannonMoveStrategy.validateBlockingPiece(pathInfos, to)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 포는_경로에_여러_개의_기물이_존재하면_이동할_수_없다() { + Position to = new Position(8, 4); + + List pathInfos = new ArrayList<>(); + pathInfos.add(new PathInfo(new Position(8, 2), Piece.of(Camp.CHO, PieceType.SOLDIER))); + pathInfos.add(new PathInfo(new Position(8, 3), Piece.of(Camp.CHO, PieceType.HORSE))); + pathInfos.add(new PathInfo(to, Piece.of(Camp.CHO, PieceType.CHARIOT))); + + assertThatThrownBy(() -> cannonMoveStrategy.validateBlockingPiece(pathInfos, to)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 포는_포를_잡을_수_없다() { + Position to = new Position(8, 2); + + List pathInfos = new ArrayList<>(); + pathInfos.add(new PathInfo(to, Piece.of(Camp.CHO, PieceType.CANNON))); + + assertThatThrownBy(() -> cannonMoveStrategy.validateBlockingPiece(pathInfos, to)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 포는_포를_넘을_수_없다() { + Position to = new Position(8, 2); + + List pathInfos = new ArrayList<>(); + pathInfos.add(new PathInfo(new Position(8, 1), Piece.of(Camp.CHO, PieceType.CANNON))); + pathInfos.add(new PathInfo(to, Piece.of(Camp.CHO, PieceType.CHARIOT))); + + assertThatThrownBy(() -> cannonMoveStrategy.validateBlockingPiece(pathInfos, to)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/domain/piece/strategy/ChariotMoveStrategyTest.java b/src/test/java/domain/piece/strategy/ChariotMoveStrategyTest.java new file mode 100644 index 0000000000..58e9f10dd9 --- /dev/null +++ b/src/test/java/domain/piece/strategy/ChariotMoveStrategyTest.java @@ -0,0 +1,63 @@ +package domain.piece.strategy; + +import domain.board.Position; +import domain.path.PathInfo; +import domain.piece.Camp; +import domain.piece.Piece; +import domain.piece.PieceType; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ChariotMoveStrategyTest { + private final MoveStrategy chariotMoveStrategy = new ChariotMoveStrategy(); + + @Test + void 차는_세로_직선_방향의_이동_경로를_가진다() { + Position from = new Position(8, 0); + Position to = new Position(8, 2); + + List path = chariotMoveStrategy.getPath(from, to); + + assertThat(path).containsExactly( + new Position(8, 1), + new Position(8, 2)); + } + + @Test + void 차는_가로_직선_방향의_이동_경로를_가진다() { + Position from = new Position(8, 0); + Position to = new Position(6, 0); + + List path = chariotMoveStrategy.getPath(from, to); + + assertThat(path).containsExactly( + new Position(7, 0), + new Position(6, 0)); + } + + @Test + void 차는_대각선_방향으로_이동할_수_없다() { + Position from = new Position(8, 0); + Position to = new Position(7, 1); + + assertThatThrownBy(() -> chariotMoveStrategy.getPath(from, to)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 차는_경로에_다른_기물이_있으면_이동할_수_없다() { + Position to = new Position(8, 2); + + List pathInfos = new ArrayList<>(); + pathInfos.add(new PathInfo(new Position(8, 1), Piece.of(Camp.CHO, PieceType.CHARIOT))); + pathInfos.add(new PathInfo(new Position(8, 2), Piece.of(Camp.CHO, PieceType.HORSE))); + + assertThatThrownBy(() -> chariotMoveStrategy.validateBlockingPiece(pathInfos, to)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/domain/piece/strategy/ElephantMoveStrategyTest.java b/src/test/java/domain/piece/strategy/ElephantMoveStrategyTest.java new file mode 100644 index 0000000000..455e48adde --- /dev/null +++ b/src/test/java/domain/piece/strategy/ElephantMoveStrategyTest.java @@ -0,0 +1,52 @@ +package domain.piece.strategy; + +import domain.board.Position; +import domain.path.PathInfo; +import domain.piece.Camp; +import domain.piece.Piece; +import domain.piece.PieceType; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ElephantMoveStrategyTest { + private final MoveStrategy elephantMoveStrategy = new ElephantMoveStrategy(); + + @Test + void 상은_직선으로만_이동할_수_없다() { + Position from = new Position(8, 0); + Position to = new Position(8, 2); + + assertThatThrownBy(() -> elephantMoveStrategy.getPath(from, to)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 상은_직선으로_한_칸_이동_후_대각선으로_두_칸_이동하는_경로를_가진다() { + Position from = new Position(8, 0); + Position to = new Position(6, 3); + + List path = elephantMoveStrategy.getPath(from, to); + + assertThat(path).containsExactly( + new Position(8, 1), + new Position(7, 2), + new Position(6, 3)); + } + + @Test + void 상은_경로에_다른_기물이_있으면_이동할_수_없다() { + Position to = new Position(8, 1); + + List pathInfos = new ArrayList<>(); + pathInfos.add(new PathInfo(new Position(8, 0), Piece.of(Camp.CHO, PieceType.CHARIOT))); + pathInfos.add(new PathInfo(to, Piece.of(Camp.HAN, PieceType.CHARIOT))); + + assertThatThrownBy(() -> elephantMoveStrategy.validateBlockingPiece(pathInfos, to)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/domain/piece/strategy/GeneralMoveStrategyTest.java b/src/test/java/domain/piece/strategy/GeneralMoveStrategyTest.java new file mode 100644 index 0000000000..96c7c606bc --- /dev/null +++ b/src/test/java/domain/piece/strategy/GeneralMoveStrategyTest.java @@ -0,0 +1,51 @@ +package domain.piece.strategy; + +import domain.board.Position; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class GeneralMoveStrategyTest { + private final MoveStrategy generalMoveStrategy = new GeneralMoveStrategy(); + + @Test + void 궁과_사는_두_칸_이상_이동할_수_없다() { + Position from = new Position(8, 0); + Position to = new Position(8, 2); + + assertThatThrownBy(() -> generalMoveStrategy.getPath(from, to)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 궁과_사는_세로_직선_방향으로_한_칸_이동하는_경로를_가진다() { + Position from = new Position(8, 0); + Position to = new Position(8, 1); + + List path = generalMoveStrategy.getPath(from, to); + + assertThat(path).containsExactly(new Position(8, 1)); + } + + @Test + void 궁과_사는_가로_직선_방향으로_한_칸_이동하는_경로를_가진다() { + Position from = new Position(8, 0); + Position to = new Position(7, 0); + + List path = generalMoveStrategy.getPath(from, to); + + assertThat(path).containsExactly(new Position(7, 0)); + } + + @Test + void 궁과_사는_대각선_방향으로_이동할_수_없다() { + Position from = new Position(8, 0); + Position to = new Position(7, 1); + + assertThatThrownBy(() -> generalMoveStrategy.getPath(from, to)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/domain/piece/strategy/HorseMoveStrategyTest.java b/src/test/java/domain/piece/strategy/HorseMoveStrategyTest.java new file mode 100644 index 0000000000..451f6e0cdc --- /dev/null +++ b/src/test/java/domain/piece/strategy/HorseMoveStrategyTest.java @@ -0,0 +1,51 @@ +package domain.piece.strategy; + +import domain.board.Position; +import domain.path.PathInfo; +import domain.piece.Camp; +import domain.piece.Piece; +import domain.piece.PieceType; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class HorseMoveStrategyTest { + private final MoveStrategy horseMoveStrategy = new HorseMoveStrategy(); + + @Test + void 마는_직선으로만_이동할_수_없다() { + Position from = new Position(8, 0); + Position to = new Position(8, 2); + + assertThatThrownBy(() -> horseMoveStrategy.getPath(from, to)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 마는_직선으로_한_칸_이동_후_대각선으로_한_칸_이동하는_경로를_가진다() { + Position from = new Position(8, 0); + Position to = new Position(7, 2); + + List path = horseMoveStrategy.getPath(from, to); + + assertThat(path).containsExactly( + new Position(8, 1), + new Position(7, 2)); + } + + @Test + void 마는_경로에_다른_기물이_있으면_이동할_수_없다() { + Position to = new Position(8, 1); + + List pathInfos = new ArrayList<>(); + pathInfos.add(new PathInfo(new Position(8, 0), Piece.of(Camp.HAN, PieceType.CHARIOT))); + pathInfos.add(new PathInfo(to, Piece.of(Camp.CHO, PieceType.CHARIOT))); + + assertThatThrownBy(() -> horseMoveStrategy.validateBlockingPiece(pathInfos, to)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/domain/piece/strategy/SoldierMoveStrategyTest.java b/src/test/java/domain/piece/strategy/SoldierMoveStrategyTest.java new file mode 100644 index 0000000000..5f955b9d23 --- /dev/null +++ b/src/test/java/domain/piece/strategy/SoldierMoveStrategyTest.java @@ -0,0 +1,61 @@ +package domain.piece.strategy; + +import domain.board.Position; +import domain.piece.Camp; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SoldierMoveStrategyTest { + private final MoveStrategy soldierMoveStrategy = new SoldierMoveStrategy(Camp.CHO.getForwardDirection()); + + @Test + void 졸은_두_칸_이상_이동할_수_없다() { + Position from = new Position(8, 0); + Position to = new Position(8, 2); + + assertThatThrownBy(() -> soldierMoveStrategy.getPath(from, to)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 졸은_세로_직선_방향으로_한_칸_이동하는_경로를_가진다() { + Position from = new Position(8, 0); + Position to = new Position(8, 1); + + List path = soldierMoveStrategy.getPath(from, to); + + assertThat(path).containsExactly(new Position(8, 1)); + } + + @Test + void 졸은_가로_직선_방향으로_한_칸_이동하는_경로를_가진다() { + Position from = new Position(8, 0); + Position to = new Position(7, 0); + + List path = soldierMoveStrategy.getPath(from, to); + + assertThat(path).containsExactly(new Position(7, 0)); + } + + @Test + void 졸은_대각선_방향으로_이동할_수_없다() { + Position from = new Position(8, 0); + Position to = new Position(7, 1); + + assertThatThrownBy(() -> soldierMoveStrategy.getPath(from, to)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 졸은_후퇴할_수_없다() { + Position from = new Position(7, 2); + Position to = new Position(7, 1); + + assertThatThrownBy(() -> soldierMoveStrategy.getPath(from, to)) + .isInstanceOf(IllegalArgumentException.class); + } +}