diff --git a/README.md b/README.md index 9775dda0ae..22c5974033 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,62 @@ # java-janggi -장기 미션 저장소 +## 프로그램 흐름 +1. **[출력] 장기 게임 시작 문구를 출력한다.** +2. **[중간 과정] 장기판을 초기화한다.** +3. **[출력] 초기화된 장기판을 출력한다.** +4. **[입력] 움직일 기물 좌표를 입력받는다.** + + [예외처리] 좌표를 쉼표로 구분하지 않았을 경우 IllegalArgmentException 예외를 발생시킨다. + + [예외처리] 좌표를 숫자로 입력하지 않았을 경우 IllegalArgmentException 예외를 발생시킨다. + + [예외처리] 장기판 범위 밖의 좌표를 입력했을 경우 IllegalArgmentException 예외를 발생시킨다. + + [예외처리] 입력한 좌표에 기물이 없을 경우 IllegalArgmentException 예외를 발생시킨다. +5. **[입력] 목적지 좌표를 입력받는다.** + + [예외처리] 좌표를 쉼표로 구분하지 않았을 경우 IllegalArgmentException 예외를 발생시킨다. + + [예외처리] 좌표를 숫자로 입력하지 않았을 경우 IllegalArgmentException 예외를 발생시킨다. + + [예외처리] 장기판 범위 밖의 좌표를 입력했을 경우 IllegalArgmentException 예외를 발생시킨다. + + [예외처리] 입력한 좌표 또는 이동 경로에 아군 기물이 존재할 경우 IllegalArgmentException 예외를 발생시킨다. +6. **[중간 과정] 기물을 이동시킨다.** +7. **[출력] 갱신된 장기판을 출력한다.** + +## 출력예시 +``` +장기 게임을 시작합니다. + +   1 2 3 4 5 6 7 8 9  +10 차 마 상 사 . 사 상 마 차  + 9 . . . . 궁 . . . .  + 8 . 포 . . . . . 포 .  + 7 졸 . 졸 . 졸 . 졸 . 졸  + 6 . . . . . . . . .  + 5 . . . . . . . . .  + 4 졸 . 졸 . 졸 . 졸 . 졸  + 3 . 포 . . . . . 포 .  + 2 . . . . 궁 . . . .  + 1 차 마 상 사 . 사 상 마 차  +초나라의 차례입니다. +움직일 기물의 좌표를 입력해주세요. (ex. 1,3) +1,1 +차의 목적 좌표를 입력해주세요. (ex. 1,3) +1,3 +   1 2 3 4 5 6 7 8 9  +10 차 마 상 사 . 사 상 마 차  + 9 . . . . 궁 . . . .  + 8 . 포 . . . . . 포 .  + 7 졸 . 졸 . 졸 . 졸 . 졸  + 6 . . . . . . . . .  + 5 . . . . . . . . .  + 4 졸 . 졸 . 졸 . 졸 . 졸  + 3 차 포 . . . . . 포 .  + 2 . . . . 궁 . . . .  + 1 . 마 상 사 . 사 상 마 차  +한나라의 차례입니다. +움직일 기물의 좌표를 입력해주세요. (ex. 1,3) + +``` 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/janggi/Application.java b/src/main/java/janggi/Application.java new file mode 100644 index 0000000000..b8b3cedc9b --- /dev/null +++ b/src/main/java/janggi/Application.java @@ -0,0 +1,9 @@ +package janggi; + +public class Application { + + public static void main(String[] args) { + JanggiRunner janggiRunner = new JanggiRunner(); + janggiRunner.execute(); + } +} diff --git a/src/main/java/janggi/JanggiRunner.java b/src/main/java/janggi/JanggiRunner.java new file mode 100644 index 0000000000..333c0ed083 --- /dev/null +++ b/src/main/java/janggi/JanggiRunner.java @@ -0,0 +1,50 @@ +package janggi; + +import janggi.domain.JanggiGame; +import janggi.domain.Position; +import janggi.util.DelimiterParser; +import janggi.util.ActionExecutor; +import janggi.view.InputView; +import janggi.view.OutputView; +import java.util.List; + +public class JanggiRunner { + + private final JanggiGame janggiGame; + + public JanggiRunner() { + this.janggiGame = JanggiGame.createInitialJanggiGame(); + } + + public void execute() { + OutputView.printStartMessage(); + + while (true) { + OutputView.printBoard(janggiGame.makeCurrentTurnBoardSnapShot()); + Position startPosition = ActionExecutor.retryUntilSuccess(this::readValidStartPosition); + Position endPosition = ActionExecutor.retryUntilSuccess(() -> readValidEndPosition(startPosition)); + janggiGame.doGame(startPosition, endPosition); + } + } + + private Position readValidStartPosition() { + OutputView.printTurnNotice(janggiGame.getCurrentTurnTeamName()); + OutputView.printAskPiecePosition(); + Position startPosition = createPosition(); + janggiGame.validatePieceExists(startPosition); + return startPosition; + } + + private Position readValidEndPosition(Position startPosition) { + OutputView.printAskMovePosition(janggiGame.getPieceName(startPosition)); + Position endPosition = createPosition(); + janggiGame.validateValidEndPosition(startPosition, endPosition); + return endPosition; + } + + private static Position createPosition() { + String rawPiecePosition = InputView.readLine(); + List parsedPiecePosition = DelimiterParser.parse(rawPiecePosition); + return Position.makePosition(parsedPiecePosition); + } +} diff --git a/src/main/java/janggi/domain/Board.java b/src/main/java/janggi/domain/Board.java new file mode 100644 index 0000000000..da2b9f239f --- /dev/null +++ b/src/main/java/janggi/domain/Board.java @@ -0,0 +1,143 @@ +package janggi.domain; + +import janggi.domain.piece.Piece; +import janggi.domain.side.Chu; +import janggi.domain.side.Han; +import janggi.domain.side.Team; +import janggi.domain.side.TeamType; +import janggi.dto.BoardSpots; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class Board { + + private static final int FIRST_INDEX = 1; + private static final int LAST_X_INDEX = 9; + private static final int LAST_Y_INDEX = 10; + + private final Team chu; + private final Team han; + + private Board(Team chu, Team han) { + this.chu = chu; + this.han = han; + } + + public static Board createInitialBoard() { + return new Board(Chu.createInitialChu(), Han.createInitialHan()); + } + + public BoardSpots makeSnapShot() { + Map allPieces = new HashMap<>(chu.getPieces()); + allPieces.putAll(han.getPieces()); + return BoardSpots.from(allPieces); + } + + public boolean isPieceExists(Position position, TeamType currentTeamType) { + validateRange(position); + Team currentTeam = currentTeam(currentTeamType); + return currentTeam.isPieceExists(position); + } + + public String getPieceName(Position position, TeamType currentTeamType) { + Piece piece = findTeamPiece(position, currentTeam(currentTeamType)); + return piece.name(); + } + + public void validateCanMove(Position start, Position end, TeamType currentTeamType) { + validateRange(end); + checkSamePosition(start, end); + validateTeamPieceExistsAtEnd(currentTeam(currentTeamType), end); + validatePiecesInPath(start, end, currentTeam(currentTeamType)); + } + + public Optional findPiece(Position position) { + Optional chuPiece = chu.findPiece(position); + if (chuPiece.isPresent()) { + return chuPiece; + } + return han.findPiece(position); + } + + public Board move(Position start, Position end, TeamType currentTeamType) { + Team updatedCurrentTeam = currentTeam(currentTeamType).move(start, end); + Team updatedOpponentTeam = removeOpponentPiece(currentTeamType, end); + return createMovedBoard(currentTeamType, updatedCurrentTeam, updatedOpponentTeam); + } + + private void validateRange(Position inputPosition) { + int x = inputPosition.getX(); + int y = inputPosition.getY(); + if (isNotInRange(FIRST_INDEX, LAST_X_INDEX, x) || isNotInRange(FIRST_INDEX, LAST_Y_INDEX, y)) { + throw new IllegalArgumentException("입력한 좌표가 장기판 범위 밖입니다."); + } + } + + private boolean isNotInRange(int start, int last, int index) { + return !isInRange(start, last, index); + } + + private boolean isInRange(int start, int last, int index) { + return index >= start && index <= last; + } + + private void checkSamePosition(Position start, Position end) { + if (start.isSamePosition(end)) { + throw new IllegalArgumentException("출발지와 목적지가 동일합니다."); + } + } + + private void validateTeamPieceExistsAtEnd(Team team, Position end) { + if (team.isPieceExists(end)) { + throw new IllegalArgumentException("아군이 존재하는 좌표로는 이동할 수 없습니다."); + } + } + + private Team currentTeam(TeamType currentTeamType) { + if (currentTeamType == TeamType.CHU) { + return chu; + } + return han; + } + + private void validatePiecesInPath(Position start, Position end, Team currentTeam) { + Piece piece = findTeamPiece(start, currentTeam); + List piecePositionsInPath = piece.getPiecePositionsInPath(start, end); + List piecesInPath = piecePositionsInPath.stream() + .map(this::findPiece) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + piece.validateCanMove(piecesInPath); + } + + private Piece findTeamPiece(Position position, Team currentTeam) { + return currentTeam.findPiece(position) + .orElseThrow(() -> new IllegalArgumentException("입력한 위치에 기물이 없습니다.")); + } + + private Team removeOpponentPiece(TeamType currentTeamType, Position end) { + Team opponentTeam = opponentTeam(currentTeamType); + if (opponentTeam.findPiece(end).isEmpty()) { + return opponentTeam; + } + return opponentTeam.remove(end); + } + + private Team opponentTeam(TeamType currentTeamType) { + if (currentTeamType == TeamType.CHU) { + return han; + } + return chu; + } + + private Board createMovedBoard(TeamType currentTeamType, Team updatedCurrentTeam, Team updatedOpponentTeam) { + if (currentTeamType == TeamType.CHU) { + return new Board(updatedCurrentTeam, updatedOpponentTeam); + } + return new Board(updatedOpponentTeam, updatedCurrentTeam); + } +} diff --git a/src/main/java/janggi/domain/Delta.java b/src/main/java/janggi/domain/Delta.java new file mode 100644 index 0000000000..ac4ded8320 --- /dev/null +++ b/src/main/java/janggi/domain/Delta.java @@ -0,0 +1,30 @@ +package janggi.domain; + +public enum Delta { + + UP(0, 1), + DOWN(0, -1), + RIGHT(1, 0), + LEFT(-1, 0), + RIGHT_UP(1, 1), + LEFT_UP(-1, 1), + RIGHT_DOWN(1, -1), + LEFT_DOWN(-1, -1), + ; + + private final int dx; + private final int dy; + + Delta(int dx, int dy) { + this.dx = dx; + this.dy = dy; + } + + public int getDx() { + return dx; + } + + public int getDy() { + return dy; + } +} diff --git a/src/main/java/janggi/domain/JanggiGame.java b/src/main/java/janggi/domain/JanggiGame.java new file mode 100644 index 0000000000..41cf45b190 --- /dev/null +++ b/src/main/java/janggi/domain/JanggiGame.java @@ -0,0 +1,55 @@ +package janggi.domain; + +import janggi.dto.BoardSpots; + +import java.util.ArrayList; +import java.util.List; + +public class JanggiGame { + + private final List turns; + + private JanggiGame(List turns) { + this.turns = new ArrayList<>(turns); + } + + public static JanggiGame createInitialJanggiGame() { + return new JanggiGame(List.of(Turn.createInitialTurn())); + } + + public BoardSpots makeCurrentTurnBoardSnapShot() { + Turn lastTurn = getLastTurn(); + return lastTurn.makeBoardSnapShot(); + } + + public String getCurrentTurnTeamName() { + Turn lastTurn = getLastTurn(); + return lastTurn.nextTurnTeam(); + } + + public void validatePieceExists(Position position) { + Turn lastTurn = getLastTurn(); + if (!lastTurn.isNextTurnTeamPieceExists(position)) { + throw new IllegalArgumentException("기물이 존재하는 좌표가 아닙니다."); + } + } + + public String getPieceName(Position position) { + return getLastTurn().getPieceName(position); + } + + public void validateValidEndPosition(Position start, Position end) { + Turn lastTurn = getLastTurn(); + lastTurn.validateCanMove(start, end); + } + + public void doGame(Position start, Position end) { + Turn lastTurn = getLastTurn(); + Turn newTurn = lastTurn.move(start, end); + turns.add(newTurn); + } + + private Turn getLastTurn() { + return turns.getLast(); + } +} diff --git a/src/main/java/janggi/domain/MovePath.java b/src/main/java/janggi/domain/MovePath.java new file mode 100644 index 0000000000..9d09192e79 --- /dev/null +++ b/src/main/java/janggi/domain/MovePath.java @@ -0,0 +1,76 @@ +package janggi.domain; + +import java.util.ArrayList; +import java.util.List; + +public class MovePath { + + private final List path; + + public MovePath(List path) { + this.path = path; + } + + public boolean matches(int dx, int dy) { + return totalDx() == dx && totalDy() == dy; + } + + public boolean matchesDirection(int dx, int dy) { + if (path.size() != 1) { + return false; + } + Delta delta = path.getFirst(); + if (delta.getDx() == 0) { + return dx == 0 && Integer.signum(dy) == Integer.signum(delta.getDy()) && dy != 0; + } + if (delta.getDy() == 0) { + return dy == 0 && Integer.signum(dx) == Integer.signum(delta.getDx()) && dx != 0; + } + return Math.abs(dx) == Math.abs(dy) + && Integer.signum(dx) == Integer.signum(delta.getDx()) + && Integer.signum(dy) == Integer.signum(delta.getDy()); + } + + public List createRoute(Position start, Position end) { + List route = new ArrayList<>(); + Position current = start; + if (path.size() == 1) { + return createStraightRoute(end, route, current); + } + for (Delta delta : path) { + current = current.move(delta); + route.add(current); + } + return route; + } + + public List intermediatePositions(Position start, Position end) { + List route = createRoute(start, end); + if (route.isEmpty()) { + return route; + } + route.removeLast(); + return route; + } + + private int totalDx() { + return path.stream() + .mapToInt(Delta::getDx) + .sum(); + } + + private int totalDy() { + return path.stream() + .mapToInt(Delta::getDy) + .sum(); + } + + private List createStraightRoute(Position end, List route, Position current) { + Delta delta = path.getFirst(); + while (!current.equals(end)) { + current = current.move(delta); + route.add(current); + } + return route; + } +} diff --git a/src/main/java/janggi/domain/Position.java b/src/main/java/janggi/domain/Position.java new file mode 100644 index 0000000000..ecd48b61be --- /dev/null +++ b/src/main/java/janggi/domain/Position.java @@ -0,0 +1,70 @@ +package janggi.domain; + +import java.util.List; +import java.util.Objects; + +public class Position { + + private static final int POSITION_SIZE = 2; + + private final int x; + private final int y; + + public Position(int x, int y) { + this.x = x; + this.y = y; + } + + public static Position makePosition(List parsedPiecePosition) { + if (parsedPiecePosition.size() != POSITION_SIZE) { + throw new IllegalArgumentException("기물의 좌표는 두 개로 입력해야 합니다."); + } + try { + int parsedX = Integer.parseInt(parsedPiecePosition.getFirst()); + int parsedY = Integer.parseInt(parsedPiecePosition.getLast()); + return new Position(parsedX, parsedY); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("좌표는 숫자가 입력되어야 합니다."); + } + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public Position move(Delta delta) { + return new Position(x + delta.getDx(), y + delta.getDy()); + } + + public boolean isSamePosition(Position position) { + return x == position.getX() && y == position.getY(); + } + + public int deltaX(Position position) { + return position.getX() - x; + } + + public int deltaY(Position position) { + return position.getY() - y; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof Position position)) { + return false; + } + return x == position.x && y == position.y; + } + + @Override + public int hashCode() { + return Objects.hash(x, y); + } +} diff --git a/src/main/java/janggi/domain/Turn.java b/src/main/java/janggi/domain/Turn.java new file mode 100644 index 0000000000..36c4a13f32 --- /dev/null +++ b/src/main/java/janggi/domain/Turn.java @@ -0,0 +1,53 @@ +package janggi.domain; + +import janggi.domain.side.TeamType; +import janggi.dto.BoardSpots; + +public class Turn { + + private final TeamType movedTeam; + private final Board board; + + private Turn(TeamType movedTeam, Board board) { + this.movedTeam = movedTeam; + this.board = board; + } + + public static Turn createInitialTurn() { + return new Turn(TeamType.HAN, Board.createInitialBoard()); + } + + public BoardSpots makeBoardSnapShot() { + return board.makeSnapShot(); + } + + public String nextTurnTeam() { + TeamType teamType = opponentTeamType(); + return teamType.getName(); + } + + public boolean isNextTurnTeamPieceExists(Position position) { + return board.isPieceExists(position, opponentTeamType()); + } + + public String getPieceName(Position position) { + return board.getPieceName(position, opponentTeamType()); + } + + public void validateCanMove(Position start, Position end) { + board.validateCanMove(start, end, opponentTeamType()); + } + + public Turn move(Position start, Position end) { + TeamType currentTeamType = opponentTeamType(); + Board movedBoard = board.move(start, end, currentTeamType); + return new Turn(currentTeamType, movedBoard); + } + + private TeamType opponentTeamType() { + if (movedTeam == TeamType.CHU) { + return TeamType.HAN; + } + return TeamType.CHU; + } +} diff --git a/src/main/java/janggi/domain/piece/Cha.java b/src/main/java/janggi/domain/piece/Cha.java new file mode 100644 index 0000000000..38ca7fb616 --- /dev/null +++ b/src/main/java/janggi/domain/piece/Cha.java @@ -0,0 +1,42 @@ +package janggi.domain.piece; + +import janggi.domain.Delta; +import janggi.domain.MovePath; +import janggi.domain.side.TeamType; + +import java.util.List; + +public class Cha extends SlidingPiece { + + private static final List PATHS = List.of( + new MovePath(List.of(Delta.UP)), + new MovePath(List.of(Delta.DOWN)), + new MovePath(List.of(Delta.LEFT)), + new MovePath(List.of(Delta.RIGHT)) + ); + + public Cha(TeamType teamType) { + super(teamType, PieceType.CHA); + } + + + @Override + protected List getPaths() { + return PATHS; + } + + @Override + public void validateCanMove(List piecesInPath) { + if (!piecesInPath.isEmpty()) { + throw new IllegalArgumentException("이동 경로에 기물이 존재하여 이동할 수 없습니다."); + } + } + +// @Override +// protected void validatePieceInPath(MovePath movePath, Position start, Position end, Board board) { +// if (movePath.intermediatePositions(start, end).stream() +// .anyMatch(board::hasPiece)) { +// throw new IllegalArgumentException("이동 경로에 기물이 존재하여 이동할 수 없습니다."); +// } +// } +} diff --git a/src/main/java/janggi/domain/piece/Gung.java b/src/main/java/janggi/domain/piece/Gung.java new file mode 100644 index 0000000000..50873f2393 --- /dev/null +++ b/src/main/java/janggi/domain/piece/Gung.java @@ -0,0 +1,30 @@ +package janggi.domain.piece; + +import janggi.domain.Delta; +import janggi.domain.MovePath; +import janggi.domain.side.TeamType; + +import java.util.List; + +public class Gung extends SteppingPiece { + + private static final List PATHS = List.of( + new MovePath(List.of(Delta.UP)), + new MovePath(List.of(Delta.DOWN)), + new MovePath(List.of(Delta.RIGHT)), + new MovePath(List.of(Delta.LEFT)), + new MovePath(List.of(Delta.RIGHT_UP)), + new MovePath(List.of(Delta.RIGHT_DOWN)), + new MovePath(List.of(Delta.LEFT_UP)), + new MovePath(List.of(Delta.LEFT_DOWN)) + ); + + public Gung(TeamType teamType) { + super(teamType, PieceType.GUNG); + } + + @Override + protected List getPaths() { + return PATHS; + } +} diff --git a/src/main/java/janggi/domain/piece/Jol.java b/src/main/java/janggi/domain/piece/Jol.java new file mode 100644 index 0000000000..cef9e1a898 --- /dev/null +++ b/src/main/java/janggi/domain/piece/Jol.java @@ -0,0 +1,34 @@ +package janggi.domain.piece; + +import janggi.domain.Delta; +import janggi.domain.MovePath; +import janggi.domain.side.TeamType; + +import java.util.List; + +public class Jol extends SteppingPiece { + + private static final List CHU_PATHS = List.of( + new MovePath(List.of(Delta.UP)), + new MovePath(List.of(Delta.LEFT)), + new MovePath(List.of(Delta.RIGHT)) + ); + + private static final List HAN_PATHS = List.of( + new MovePath(List.of(Delta.DOWN)), + new MovePath(List.of(Delta.LEFT)), + new MovePath(List.of(Delta.RIGHT)) + ); + + public Jol(TeamType teamType) { + super(teamType, PieceType.JOL); + } + + @Override + protected List getPaths() { + if (getTeamType() == TeamType.CHU) { + return CHU_PATHS; + } + return HAN_PATHS; + } +} diff --git a/src/main/java/janggi/domain/piece/LeapingPiece.java b/src/main/java/janggi/domain/piece/LeapingPiece.java new file mode 100644 index 0000000000..d055a0d96e --- /dev/null +++ b/src/main/java/janggi/domain/piece/LeapingPiece.java @@ -0,0 +1,37 @@ +package janggi.domain.piece; + +import janggi.domain.MovePath; +import janggi.domain.Position; +import janggi.domain.side.TeamType; + +import java.util.List; + +public abstract class LeapingPiece extends Piece{ + + public LeapingPiece(TeamType teamType, PieceType pieceType) { + super(teamType, pieceType); + } + + @Override + public void validateCanMove(List piecesInPath) { + if (!piecesInPath.isEmpty()) { + throw new IllegalArgumentException("이동 경로에 기물이 존재하여 이동할 수 없습니다."); + } + } + + @Override + public List getPiecePositionsInPath(Position start, Position end) { + MovePath movePath = findMovePath(start, end); + return movePath.intermediatePositions(start, end); + } + + private MovePath findMovePath(Position start, Position end) { + int dx = start.deltaX(end); + int dy = start.deltaY(end); + + return getPaths().stream() + .filter(path -> path.matches(dx, dy)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("이동할 수 없는 위치입니다.")); + } +} diff --git a/src/main/java/janggi/domain/piece/Ma.java b/src/main/java/janggi/domain/piece/Ma.java new file mode 100644 index 0000000000..4aeba13839 --- /dev/null +++ b/src/main/java/janggi/domain/piece/Ma.java @@ -0,0 +1,30 @@ +package janggi.domain.piece; + +import janggi.domain.Delta; +import janggi.domain.MovePath; +import janggi.domain.side.TeamType; + +import java.util.List; + +public class Ma extends LeapingPiece { + + private static final List PATHS = List.of( + new MovePath(List.of(Delta.UP, Delta.RIGHT_UP)), + new MovePath(List.of(Delta.UP, Delta.LEFT_UP)), + new MovePath(List.of(Delta.DOWN, Delta.RIGHT_DOWN)), + new MovePath(List.of(Delta.DOWN, Delta.LEFT_DOWN)), + new MovePath(List.of(Delta.LEFT, Delta.LEFT_UP)), + new MovePath(List.of(Delta.LEFT, Delta.LEFT_DOWN)), + new MovePath(List.of(Delta.RIGHT, Delta.RIGHT_UP)), + new MovePath(List.of(Delta.RIGHT, Delta.RIGHT_DOWN)) + ); + + public Ma(TeamType teamType) { + super(teamType, PieceType.MA); + } + + @Override + protected List getPaths() { + return PATHS; + } +} diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java new file mode 100644 index 0000000000..fdbbb0785a --- /dev/null +++ b/src/main/java/janggi/domain/piece/Piece.java @@ -0,0 +1,36 @@ +package janggi.domain.piece; + +import janggi.domain.MovePath; +import janggi.domain.Position; +import janggi.domain.side.TeamType; + +import java.util.List; + +public abstract class Piece { + + private final TeamType teamType; + private final PieceType pieceType; + + public Piece(TeamType teamType, PieceType pieceType) { + this.teamType = teamType; + this.pieceType = pieceType; + } + + public String name() { + return pieceType.getName(); + } + + public PieceType getPieceType() { + return pieceType; + } + + public TeamType getTeamType() { + return teamType; + } + + public abstract void validateCanMove(List piecesInPath); + + protected abstract List getPaths(); + + public abstract List getPiecePositionsInPath(Position start, Position end); +} diff --git a/src/main/java/janggi/domain/piece/PieceType.java b/src/main/java/janggi/domain/piece/PieceType.java new file mode 100644 index 0000000000..983016a8af --- /dev/null +++ b/src/main/java/janggi/domain/piece/PieceType.java @@ -0,0 +1,22 @@ +package janggi.domain.piece; + +public enum PieceType { + CHA("차"), + GUNG("궁"), + JOL("졸"), + MA("마"), + PO("포"), + SA("사"), + SANG("상"), + ; + + PieceType(String name) { + this.name = name; + } + + private final String name; + + public String getName() { + return name; + } +} diff --git a/src/main/java/janggi/domain/piece/Po.java b/src/main/java/janggi/domain/piece/Po.java new file mode 100644 index 0000000000..9e240b780f --- /dev/null +++ b/src/main/java/janggi/domain/piece/Po.java @@ -0,0 +1,39 @@ +package janggi.domain.piece; + +import janggi.domain.Delta; +import janggi.domain.MovePath; +import janggi.domain.side.TeamType; + +import java.util.List; + +public class Po extends SlidingPiece { + + private static final List PATHS = List.of( + new MovePath(List.of(Delta.UP)), + new MovePath(List.of(Delta.DOWN)), + new MovePath(List.of(Delta.LEFT)), + new MovePath(List.of(Delta.RIGHT)) + ); + + public Po(TeamType teamType) { + super(teamType, PieceType.PO); + } + + @Override + protected List getPaths() { + return PATHS; + } + + @Override + public void validateCanMove(List piecesInPath) { + if (piecesInPath.isEmpty()) { + throw new IllegalArgumentException("이동 경로에 기물이 존재하지 않아 이동할 수 없습니다."); + } + if (piecesInPath.size() > 1) { + throw new IllegalArgumentException("이동 경로에 기물이 2개 이상 존재합니다."); + } + if (piecesInPath.getFirst().getPieceType() == PieceType.PO) { + throw new IllegalArgumentException("포는 포를 넘을 수 없습니다."); + } + } +} diff --git a/src/main/java/janggi/domain/piece/Sa.java b/src/main/java/janggi/domain/piece/Sa.java new file mode 100644 index 0000000000..b4ceb8c3fa --- /dev/null +++ b/src/main/java/janggi/domain/piece/Sa.java @@ -0,0 +1,30 @@ +package janggi.domain.piece; + +import janggi.domain.Delta; +import janggi.domain.MovePath; +import janggi.domain.side.TeamType; + +import java.util.List; + +public class Sa extends SteppingPiece { + + private static final List PATHS = List.of( + new MovePath(List.of(Delta.UP)), + new MovePath(List.of(Delta.DOWN)), + new MovePath(List.of(Delta.LEFT)), + new MovePath(List.of(Delta.RIGHT)), + new MovePath(List.of(Delta.RIGHT_UP)), + new MovePath(List.of(Delta.RIGHT_DOWN)), + new MovePath(List.of(Delta.LEFT_UP)), + new MovePath(List.of(Delta.LEFT_DOWN)) + ); + + public Sa(TeamType teamType) { + super(teamType, PieceType.SA); + } + + @Override + protected List getPaths() { + return PATHS; + } +} diff --git a/src/main/java/janggi/domain/piece/Sang.java b/src/main/java/janggi/domain/piece/Sang.java new file mode 100644 index 0000000000..410220d17a --- /dev/null +++ b/src/main/java/janggi/domain/piece/Sang.java @@ -0,0 +1,30 @@ +package janggi.domain.piece; + +import janggi.domain.Delta; +import janggi.domain.MovePath; +import janggi.domain.side.TeamType; + +import java.util.List; + +public class Sang extends LeapingPiece { + + private static final List PATHS = List.of( + new MovePath(List.of(Delta.UP, Delta.RIGHT_UP, Delta.RIGHT_UP)), + new MovePath(List.of(Delta.UP, Delta.LEFT_UP, Delta.LEFT_UP)), + new MovePath(List.of(Delta.DOWN, Delta.RIGHT_DOWN, Delta.RIGHT_DOWN)), + new MovePath(List.of(Delta.DOWN, Delta.LEFT_DOWN, Delta.LEFT_DOWN)), + new MovePath(List.of(Delta.LEFT, Delta.LEFT_UP, Delta.LEFT_UP)), + new MovePath(List.of(Delta.LEFT, Delta.LEFT_DOWN, Delta.LEFT_DOWN)), + new MovePath(List.of(Delta.RIGHT, Delta.RIGHT_UP, Delta.RIGHT_UP)), + new MovePath(List.of(Delta.RIGHT, Delta.RIGHT_DOWN, Delta.RIGHT_DOWN)) + ); + + public Sang(TeamType teamType) { + super(teamType, PieceType.SANG); + } + + @Override + protected List getPaths() { + return PATHS; + } +} diff --git a/src/main/java/janggi/domain/piece/SlidingPiece.java b/src/main/java/janggi/domain/piece/SlidingPiece.java new file mode 100644 index 0000000000..fc9c054a8a --- /dev/null +++ b/src/main/java/janggi/domain/piece/SlidingPiece.java @@ -0,0 +1,30 @@ +package janggi.domain.piece; + +import janggi.domain.MovePath; +import janggi.domain.Position; +import janggi.domain.side.TeamType; + +import java.util.List; + +public abstract class SlidingPiece extends Piece { + + public SlidingPiece(TeamType teamType, PieceType pieceType) { + super(teamType, pieceType); + } + + @Override + public List getPiecePositionsInPath(Position start, Position end) { + MovePath movePath = findMovePath(start, end); + return movePath.intermediatePositions(start, end); + } + + private MovePath findMovePath(Position start, Position end) { + int dx = start.deltaX(end); + int dy = start.deltaY(end); + + return getPaths().stream() + .filter(path -> path.matchesDirection(dx, dy)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("이동할 수 없는 위치입니다.")); + } +} diff --git a/src/main/java/janggi/domain/piece/SteppingPiece.java b/src/main/java/janggi/domain/piece/SteppingPiece.java new file mode 100644 index 0000000000..3dbeaee437 --- /dev/null +++ b/src/main/java/janggi/domain/piece/SteppingPiece.java @@ -0,0 +1,37 @@ +package janggi.domain.piece; + +import janggi.domain.Position; +import janggi.domain.side.TeamType; + +import java.util.Collections; +import java.util.List; + +public abstract class SteppingPiece extends Piece{ + + public SteppingPiece(TeamType teamType, PieceType pieceType) { + super(teamType, pieceType); + } + + @Override + public void validateCanMove(List piecesInPath) { + if (!piecesInPath.isEmpty()) { + throw new IllegalArgumentException("이동 경로에 기물이 존재하여 이동할 수 없습니다."); + } + } + + @Override + public List getPiecePositionsInPath(Position start, Position end) { + checkMovePath(start, end); + return Collections.emptyList(); + } + + private void checkMovePath(Position start, Position end) { + int dx = start.deltaX(end); + int dy = start.deltaY(end); + + getPaths().stream() + .filter(path -> path.matches(dx, dy)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("이동할 수 없는 위치입니다.")); + } +} diff --git a/src/main/java/janggi/domain/side/Chu.java b/src/main/java/janggi/domain/side/Chu.java new file mode 100644 index 0000000000..1a09e3256a --- /dev/null +++ b/src/main/java/janggi/domain/side/Chu.java @@ -0,0 +1,47 @@ +package janggi.domain.side; + +import janggi.domain.Position; +import janggi.domain.piece.Piece; + +import java.util.HashMap; +import java.util.Map; + +public class Chu extends Team { + + public Chu(Map pieces) { + super(pieces); + } + + public static Chu createInitialChu() { + return new Chu(initializePieces()); + } + + @Override + public Team move(Position start, Position end) { + Map pieces = getPieces(); + Piece piece = pieces.get(start); + Map updatedPieces = new HashMap<>(pieces); + updatedPieces.remove(start); + updatedPieces.put(end, piece); + return new Chu(updatedPieces); + } + + @Override + public Team remove(Position position) { + Map updatedPieces = new HashMap<>(getPieces()); + updatedPieces.remove(position); + return new Chu(updatedPieces); + } + + private static Map initializePieces() { + Map pieces = new HashMap<>(); + createChas(pieces, 1, TeamType.CHU); + createMas(pieces, 1, TeamType.CHU); + createSangs(pieces, 1, TeamType.CHU); + createSas(pieces, 1, TeamType.CHU); + createGung(pieces, 2, TeamType.CHU); + createPos(pieces, 3, TeamType.CHU); + createJols(pieces, 4, TeamType.CHU); + return pieces; + } +} diff --git a/src/main/java/janggi/domain/side/Han.java b/src/main/java/janggi/domain/side/Han.java new file mode 100644 index 0000000000..9df9d5cb92 --- /dev/null +++ b/src/main/java/janggi/domain/side/Han.java @@ -0,0 +1,47 @@ +package janggi.domain.side; + +import janggi.domain.Position; +import janggi.domain.piece.*; + +import java.util.HashMap; +import java.util.Map; + +public class Han extends Team { + + public Han(Map pieces) { + super(pieces); + } + + public static Han createInitialHan() { + return new Han(initializePieces()); + } + + @Override + public Team move(Position start, Position end) { + Map pieces = getPieces(); + Piece piece = pieces.get(start); + Map updatedPieces = new HashMap<>(pieces); + updatedPieces.remove(start); + updatedPieces.put(end, piece); + return new Han(updatedPieces); + } + + @Override + public Team remove(Position position) { + Map updatedPieces = new HashMap<>(getPieces()); + updatedPieces.remove(position); + return new Han(updatedPieces); + } + + private static Map initializePieces() { + Map pieces = new HashMap<>(); + createChas(pieces, 10, TeamType.HAN); + createMas(pieces, 10, TeamType.HAN); + createSangs(pieces, 10, TeamType.HAN); + createSas(pieces, 10, TeamType.HAN); + createGung(pieces, 9, TeamType.HAN); + createPos(pieces, 8, TeamType.HAN); + createJols(pieces, 7, TeamType.HAN); + return pieces; + } +} diff --git a/src/main/java/janggi/domain/side/Team.java b/src/main/java/janggi/domain/side/Team.java new file mode 100644 index 0000000000..60a440c153 --- /dev/null +++ b/src/main/java/janggi/domain/side/Team.java @@ -0,0 +1,78 @@ +package janggi.domain.side; + +import janggi.domain.Position; +import janggi.domain.piece.*; +import janggi.dto.BoardSpot; + +import java.util.*; + +public abstract class Team { + + private final Map pieces; + + public Team(Map pieces) { + this.pieces = pieces; + } + + public Map getPieces() { + return Collections.unmodifiableMap(pieces); + } + + public Map makeSnapShot() { + Map snapShot = new HashMap<>(); + for (Map.Entry entry : pieces.entrySet()) { + Position position = entry.getKey(); + Piece piece = entry.getValue(); + snapShot.put(position, new BoardSpot(piece.name(), piece.getTeamType())); + } + return snapShot; + } + + public boolean isPieceExists(Position position) { + return pieces.containsKey(position); + } + + public Optional findPiece(Position position) { + return Optional.ofNullable(pieces.get(position)); + } + + protected static void createChas(Map pieces, int indexY, TeamType teamType) { + pieces.put(new Position(1, indexY), new Cha(teamType)); + pieces.put(new Position(9, indexY), new Cha(teamType)); + } + + protected static void createMas(Map pieces, int indexY, TeamType teamType) { + pieces.put(new Position(2, indexY), new Ma(teamType)); + pieces.put(new Position(8, indexY), new Ma(teamType)); + } + + protected static void createSangs(Map pieces, int indexY, TeamType teamType) { + pieces.put(new Position(7, indexY), new Sang(teamType)); + pieces.put(new Position(10 - 7, indexY), new Sang(teamType)); + } + + protected static void createSas(Map pieces, int indexY, TeamType teamType) { + pieces.put(new Position(4, indexY), new Sa(teamType)); + pieces.put(new Position(10 - 4, indexY), new Sa(teamType)); + } + + protected static void createGung(Map pieces, int indexY, TeamType teamType) { + pieces.put(new Position(5, indexY), new Gung(teamType)); + } + + protected static void createPos(Map pieces, int indexY, TeamType teamType) { + pieces.put(new Position(2, indexY), new Po(teamType)); + pieces.put(new Position(10 - 2, indexY), new Po(teamType)); + } + + protected static void createJols(Map pieces, int indexY, TeamType teamType) { + for (int i = 1; i < 10; i += 2) { + pieces.put(new Position(i, indexY), new Jol(teamType)); + } + } + + public abstract Team move(Position start, Position end); + + public abstract Team remove(Position position); + +} diff --git a/src/main/java/janggi/domain/side/TeamType.java b/src/main/java/janggi/domain/side/TeamType.java new file mode 100644 index 0000000000..6d581c390f --- /dev/null +++ b/src/main/java/janggi/domain/side/TeamType.java @@ -0,0 +1,17 @@ +package janggi.domain.side; + +public enum TeamType { + CHU("초나라"), + HAN("한나라"), + ; + + private final String name; + + TeamType(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/janggi/dto/BoardSpot.java b/src/main/java/janggi/dto/BoardSpot.java new file mode 100644 index 0000000000..681cb6e104 --- /dev/null +++ b/src/main/java/janggi/dto/BoardSpot.java @@ -0,0 +1,11 @@ +package janggi.dto; + +import janggi.domain.piece.Piece; +import janggi.domain.side.TeamType; + +public record BoardSpot(String pieceName, TeamType teamType) { + + public static BoardSpot from(Piece piece) { + return new BoardSpot(piece.name(), piece.getTeamType()); + } +} diff --git a/src/main/java/janggi/dto/BoardSpots.java b/src/main/java/janggi/dto/BoardSpots.java new file mode 100644 index 0000000000..45fc96e0aa --- /dev/null +++ b/src/main/java/janggi/dto/BoardSpots.java @@ -0,0 +1,22 @@ +package janggi.dto; + +import janggi.domain.Position; +import janggi.domain.piece.Piece; + +import java.util.HashMap; +import java.util.Map; + +public record BoardSpots(Map value) { + + public static BoardSpots from(Map pieces) { + Map boardSpots = new HashMap<>(); + for (Map.Entry entry : pieces.entrySet()) { + Position position = entry.getKey(); + Piece piece = entry.getValue(); + + BoardSpot boardSpot = BoardSpot.from(piece); + boardSpots.put(position, boardSpot); + } + return new BoardSpots(boardSpots); + } +} diff --git a/src/main/java/janggi/util/ActionExecutor.java b/src/main/java/janggi/util/ActionExecutor.java new file mode 100644 index 0000000000..601c89d24e --- /dev/null +++ b/src/main/java/janggi/util/ActionExecutor.java @@ -0,0 +1,17 @@ +package janggi.util; + +import janggi.view.OutputView; +import java.util.function.Supplier; + +public class ActionExecutor { + + public static T retryUntilSuccess(Supplier supplier) { + while (true) { + try { + return supplier.get(); + } catch (IllegalArgumentException e) { + OutputView.printMessage(e.getMessage()); + } + } + } +} diff --git a/src/main/java/janggi/util/DelimiterParser.java b/src/main/java/janggi/util/DelimiterParser.java new file mode 100644 index 0000000000..ce99d0f27c --- /dev/null +++ b/src/main/java/janggi/util/DelimiterParser.java @@ -0,0 +1,15 @@ +package janggi.util; + +import java.util.Arrays; +import java.util.List; + +public class DelimiterParser { + + private static final String DELIMITER = ","; + + public static List parse(String input) { + return Arrays.stream(input.split(DELIMITER)) + .map(String::trim) + .toList(); + } +} diff --git a/src/main/java/janggi/view/InputView.java b/src/main/java/janggi/view/InputView.java new file mode 100644 index 0000000000..4963d38ae1 --- /dev/null +++ b/src/main/java/janggi/view/InputView.java @@ -0,0 +1,12 @@ +package janggi.view; + +import java.util.Scanner; + +public class InputView { + + private static final Scanner scanner = new Scanner(System.in); + + public static String readLine() { + return scanner.nextLine(); + } +} diff --git a/src/main/java/janggi/view/OutputView.java b/src/main/java/janggi/view/OutputView.java new file mode 100644 index 0000000000..33cf9a7785 --- /dev/null +++ b/src/main/java/janggi/view/OutputView.java @@ -0,0 +1,120 @@ +package janggi.view; + +import janggi.domain.Position; +import janggi.domain.side.TeamType; +import janggi.dto.BoardSpot; +import janggi.dto.BoardSpots; +import java.util.Map; + +public class OutputView { + + private static final String RED = "\u001B[31m"; + private static final String GREEN = "\u001B[32m"; + private static final String RESET = "\u001B[0m"; + private static final String EMPTY = "\uFF0E"; + private static final String FULL_WIDTH_SPACE = "\u3000"; + private static final String FULL_WIDTH_ZERO = "\uFF10"; + private static final String FULL_WIDTH_ONE = "\uFF11"; + private static final String FULL_WIDTH_TWO = "\uFF12"; + private static final String FULL_WIDTH_THREE = "\uFF13"; + private static final String FULL_WIDTH_FOUR = "\uFF14"; + private static final String FULL_WIDTH_FIVE = "\uFF15"; + private static final String FULL_WIDTH_SIX = "\uFF16"; + private static final String FULL_WIDTH_SEVEN = "\uFF17"; + private static final String FULL_WIDTH_EIGHT = "\uFF18"; + private static final String FULL_WIDTH_NINE = "\uFF19"; + + public static void printMessage(String message) { + System.out.println(message); + } + + public static void printSameLine(String message) { + System.out.print(message); + } + + public static void printNewLine() { + System.out.println(); + } + + public static void printStartMessage() { + printMessage("장기 게임을 시작합니다."); + printNewLine(); + } + + public static void printBoard(BoardSpots boardSpots) { + Map boardSpotsMap = boardSpots.value(); + printHeader(); + for (int y = 10; y >= 1; y--) { + printRow(boardSpotsMap, y); + } + } + + private static void printHeader() { + printSameLine(" "); + printSameLine(FULL_WIDTH_SPACE.repeat(2)); + for (int x = 1; x <= 9; x++) { + printSameLine(toFullWidthNumber(x) + FULL_WIDTH_SPACE); + } + printNewLine(); + } + + private static void printRow(Map boardSpotMap, int y) { + printSameLine(formatRowNumber(y) + FULL_WIDTH_SPACE); + for (int x = 1; x <= 9; x++) { + printSameLine(makeCell(x, y, boardSpotMap) + FULL_WIDTH_SPACE); + } + printNewLine(); + } + + private static String makeCell(int x, int y, Map boardSpotMap) { + Position position = new Position(x, y); + if (boardSpotMap.containsKey(position)) { + BoardSpot spot = boardSpotMap.get(position); + return getColor(spot.teamType()) + spot.pieceName() + RESET; + } + return EMPTY; + } + + private static String formatRowNumber(int number) { + if (number < 10) { + return FULL_WIDTH_SPACE + toFullWidthNumber(number); + } + return toFullWidthNumber(number); + } + + private static String toFullWidthNumber(int number) { + return String.valueOf(number) + .replace("0", FULL_WIDTH_ZERO) + .replace("1", FULL_WIDTH_ONE) + .replace("2", FULL_WIDTH_TWO) + .replace("3", FULL_WIDTH_THREE) + .replace("4", FULL_WIDTH_FOUR) + .replace("5", FULL_WIDTH_FIVE) + .replace("6", FULL_WIDTH_SIX) + .replace("7", FULL_WIDTH_SEVEN) + .replace("8", FULL_WIDTH_EIGHT) + .replace("9", FULL_WIDTH_NINE); + } + + private static String getColor(TeamType teamType) { + if (teamType == TeamType.CHU) { + return GREEN; + } + if (teamType == TeamType.HAN) { + return RED; + } + return RESET; + } + + public static void printTurnNotice(String nowTurn) { + printMessage(nowTurn + "의 차례입니다."); + } + + public static void printAskPiecePosition() { + printMessage("움직일 기물의 좌표를 입력해주세요. (ex. 1,3)"); + } + + public static void printAskMovePosition(String pieceName) { + printMessage(pieceName + "의 목적 좌표를 입력해주세요. (ex. 1,3)"); + } +} 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/janggi/domain/BoardTest.java b/src/test/java/janggi/domain/BoardTest.java new file mode 100644 index 0000000000..2e90169524 --- /dev/null +++ b/src/test/java/janggi/domain/BoardTest.java @@ -0,0 +1,67 @@ +package janggi.domain; + +import janggi.domain.side.TeamType; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class BoardTest { + + private Board board; + + @BeforeEach + void setUp() { + board = Board.createInitialBoard(); + } + + @Test + @DisplayName("도착 좌표가 장기판 범위 밖일 경우 예외 발생") + void cannotMoveWhenEndPositionIsOutOfRange() { + // given + Position outOfBound = new Position(1, 11); + + // when & then + assertThatThrownBy(() -> board.validateCanMove(new Position(1, 4), outOfBound, TeamType.CHU)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("입력한 좌표가 장기판 범위 밖입니다."); + } + + @Test + @DisplayName("출발지와 목적지가 동일할 경우 예외 발생") + void checkSamePosition() { + // given + Position start = new Position(4, 4); + Position end = new Position(4, 4); + + // when & then + assertThatThrownBy(() -> board.validateCanMove(start, end, TeamType.CHU)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("출발지와 목적지가 동일합니다."); + } + + @Test + @DisplayName("입력한 좌표에 기물이 없을 경우 false 반환") + void cannotMoveWhenStartPositionHasNoCurrentTeamPiece() { + // given + Position notExistPosition = new Position(2, 2); + + // when & then + boolean pieceExists = board.isPieceExists(notExistPosition, TeamType.CHU); + Assertions.assertThat(pieceExists).isFalse(); + } + + @Test + @DisplayName("목적 좌표에 아군 기물이 있을 경우 예외 발생") + void cannotMoveToSameTeamPiecePosition() { + // given + Position currentTeamPosition = new Position(2, 1); + + // when & then + assertThatThrownBy(() -> board.validateCanMove(new Position(1, 1), currentTeamPosition, TeamType.CHU)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("아군이 존재하는 좌표로는 이동할 수 없습니다."); + } +} diff --git a/src/test/java/janggi/domain/PositionTest.java b/src/test/java/janggi/domain/PositionTest.java new file mode 100644 index 0000000000..96891b529a --- /dev/null +++ b/src/test/java/janggi/domain/PositionTest.java @@ -0,0 +1,68 @@ +package janggi.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PositionTest { + + @Test + @DisplayName("좌표 문자열 두 개를 받아 Position 생성 성공") + void makePosition() { + // given + List rawPosition = List.of("3", "7"); + + // when + Position position = Position.makePosition(rawPosition); + + // then + assertAll( + () -> assertThat(position.getX()).isEqualTo(3), + () -> assertThat(position.getY()).isEqualTo(7) + ); + } + + @Test + @DisplayName("좌표 입력이 두 개가 아닐 경우 예외 발생") + void throwWhenInputSizeIsNotTwo() { + // given + List rawPosition = List.of("3"); + + // when & then + assertThatThrownBy(() -> Position.makePosition(rawPosition)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("기물의 좌표는 두 개로 입력해야 합니다."); + } + + @Test + @DisplayName("좌표 입력이 숫자가 아닐 경우 예외 발생") + void throwWhenInputContainsNonNumericValue() { + // given + List rawPosition = List.of("a", "7"); + + // when & then + assertThatThrownBy(() -> Position.makePosition(rawPosition)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("좌표는 숫자가 입력되어야 합니다."); + } + + @Test + @DisplayName("델타만큼 이동한 새 Position을 반환한다.") + void move() { + // given + Position position = new Position(4, 4); + + // when + Position movedPosition = position.move(Delta.RIGHT_UP); + + // then + assertAll( + () -> assertThat(movedPosition).isEqualTo(new Position(5, 5)), + () -> assertThat(position).isEqualTo(new Position(4, 4)) + ); + } +} diff --git a/src/test/java/janggi/domain/piece/ChaTest.java b/src/test/java/janggi/domain/piece/ChaTest.java new file mode 100644 index 0000000000..155c5fcc3a --- /dev/null +++ b/src/test/java/janggi/domain/piece/ChaTest.java @@ -0,0 +1,86 @@ +package janggi.domain.piece; + +import janggi.domain.Position; +import janggi.domain.side.TeamType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +class ChaTest { + + private Cha cha; + + @BeforeEach + void setUp() { + cha = new Cha(TeamType.HAN); + } + + @Test + @DisplayName("차는 이동 경로에 장애물이 없다면 이동할 수 있다.") + void validateCanMove_Success() { + // given + List pieces = Collections.emptyList(); + + // when & then + assertThatCode(() -> cha.validateCanMove(pieces)) + .doesNotThrowAnyException(); + } + + /** + * 제자리 테스트는 Board에서 + */ +// @Test +// @DisplayName("제자리로 이동할 경우 예외 발생") +// void validateCanMove_Fail_Same_Position() { +// // given +// Position start = new Position(4, 4); +// Position sameEnd = new Position(4, 4); +// +// // when & then +// assertThatThrownBy(() -> cha.validateCanMove(start, sameEnd, board)) +// .isInstanceOf(IllegalArgumentException.class) +// .hasMessage("출발지와 목적지가 동일합니다."); +// } + + /** + * SlidingPiece에서 검증 + */ + @Test + @DisplayName("차가 이동할 수 없는 위치일 경우 예외 발생") + void validateCanMove_Fail_Invalid_Position() { + // given + Position start = new Position(4, 4); + Position diagonalEnd = new Position(5, 5); + Position knightEnd = new Position(5, 6); + + // when & then + assertAll( + () -> assertThatThrownBy(() -> cha.getPiecePositionsInPath(start, diagonalEnd)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다."), + () -> assertThatThrownBy(() -> cha.getPiecePositionsInPath(start, knightEnd)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다.") + ); + } + + @Test + @DisplayName("이동 경로 중간에 기물이 존재할 경우 예외 발생") + void validateCanMove_Fail_Exist_Pieces_In_Path() { + // given + List piecesInPath = new ArrayList<>(List.of(new Gung(TeamType.CHU))); + + // when & then + assertThatThrownBy(() -> cha.validateCanMove(piecesInPath)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동 경로에 기물이 존재하여 이동할 수 없습니다."); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/piece/GungTest.java b/src/test/java/janggi/domain/piece/GungTest.java new file mode 100644 index 0000000000..fb88e4f8d8 --- /dev/null +++ b/src/test/java/janggi/domain/piece/GungTest.java @@ -0,0 +1,86 @@ +/** + * gung은 steppingPiece 테스트로 검증 가능 + */ +package janggi.domain.piece; + +import janggi.domain.Position; +import janggi.domain.side.TeamType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class GungTest { + + private Gung gung; + + @BeforeEach + void setUp() { + gung = new Gung(TeamType.HAN); + } + + @ParameterizedTest + @DisplayName("한나라 궁은 궁성 내에서 상하좌우 및 대각선으로 한 칸 이동할 수 있다.") + @CsvSource({ + "5, 9, 5, 10", + "5, 9, 5, 8", + "5, 9, 4, 9", + "5, 9, 6, 9", + "5, 9, 4, 10", + "5, 9, 6, 10", + "5, 9, 4, 8", + "5, 9, 6, 8" + }) + void validateCanMove_Success(int startX, int startY, int endX, int endY) { + // given + Position start = createPosition(startX, startY); + Position end = createPosition(endX, endY); + + // when & then + assertThatCode(() -> gung.getPiecePositionsInPath(start, end)) + .doesNotThrowAnyException(); + } + +// @Test +// @DisplayName("이동 경로에 기물이 존재하는 경우 예외 발생") +// void validateCanMove_Fail_Piece_Exists_In_Path() { +// List = new ArrayList<>(new Gung(TeamType.CHU)); +// } + + /** + * 제자리 이동은 Board로 이전 + */ +// @Test +// @DisplayName("제자리로 이동할 경우 예외 발생") +// void validateCanMove_Fail_Same_Position() { +// // given +// Position start = new Position(4, 4); +// Position sameEnd = new Position(4, 4); +// +// // when & then +// assertThatThrownBy(() -> gung.validateCanMove(start, sameEnd, board)) +// .isInstanceOf(IllegalArgumentException.class) +// .hasMessage("출발지와 목적지가 동일합니다."); +// } + + @Test + @DisplayName("궁이 이동할 수 없는 패턴(예: 마의 행마)인 경우 예외 발생") + void validateCanMove_Fail_InvalidPattern() { + // given + Position start = createPosition(5, 9); + Position knightEnd = createPosition(6, 7); + + // when & then + assertThatThrownBy(() -> gung.getPiecePositionsInPath(start, knightEnd)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다."); + } + + private Position createPosition(int x, int y) { + return new Position(x, y); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/piece/JolTest.java b/src/test/java/janggi/domain/piece/JolTest.java new file mode 100644 index 0000000000..4e26948b45 --- /dev/null +++ b/src/test/java/janggi/domain/piece/JolTest.java @@ -0,0 +1,104 @@ +/** + * 졸은 steppingPiece에서 테스트 가능 + * 다만 이동 경로에 대해서는 검증 필요할지도 + * 기물마다 다른 것은 각각 테스트해줘야 할듯 + */ +package janggi.domain.piece; + +import janggi.domain.Position; +import janggi.domain.side.TeamType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +class JolTest { + + private Jol chuJol; + private Jol hanJol; + + @BeforeEach + void setUp() { + chuJol = new Jol(TeamType.CHU); + hanJol = new Jol(TeamType.HAN); + } + + @ParameterizedTest + @DisplayName("초나라 졸은 위, 왼쪽, 오른쪽으로 한 칸씩 이동할 수 있다.") + @CsvSource({ + "1, 4, 1, 5", + "3, 4, 2, 4", + "3, 4, 4, 4" + }) + void validateCanMove_ChuJol_Success(int startX, int startY, int endX, int endY) { + // given + Position start = new Position(startX, startY); + Position end = new Position(endX, endY); + + // when & then + assertThatCode(() -> chuJol.getPiecePositionsInPath(start, end)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @DisplayName("한나라 졸은 아래, 왼쪽, 오른쪽으로 한 칸씩 이동할 수 있다.") + @CsvSource({ + "1, 7, 1, 6", + "3, 7, 2, 7", + "3, 7, 4, 7" + }) + void validateCanMove_HanJol_Success(int startX, int startY, int endX, int endY) { + // given + Position start = new Position(startX, startY); + Position end = new Position(endX, endY); + + // when & then + assertThatCode(() -> hanJol.getPiecePositionsInPath(start, end)) + .doesNotThrowAnyException(); + } + + /** + * 제자리는 Board에서 + */ +// @Test +// @DisplayName("제자리로 이동할 경우 예외 발생") +// void validateCanMove_Fail_Same_Position() { +// // given +// Position start = new Position(4, 4); +// Position sameEnd = new Position(4, 4); +// +// // when & then +// assertThatThrownBy(() -> chuJol.validateCanMove(start, sameEnd, board)) +// .isInstanceOf(IllegalArgumentException.class) +// .hasMessage("출발지와 목적지가 동일합니다."); +// } + + @Test + @DisplayName("졸이 이동할 수 없는 위치일 경우 예외 발생") + void validateCanMove_Fail_Invalid_Position() { + // given + Position chuJolStart = new Position(4, 4); + Position chuJolInvalidEnd = new Position(4, 3); + Position hanJolStart = new Position(4, 4); + Position hanJolInvalidEnd = new Position(4, 5); + Position diagonalEnd = new Position(5, 5); + + // when & then + assertAll( + () -> assertThatThrownBy(() -> chuJol.getPiecePositionsInPath(chuJolStart, chuJolInvalidEnd)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다."), + () -> assertThatThrownBy(() -> hanJol.getPiecePositionsInPath(hanJolStart, hanJolInvalidEnd)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다."), + () -> assertThatThrownBy(() -> chuJol.getPiecePositionsInPath(chuJolStart, diagonalEnd)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다.") + ); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/piece/LeapingPieceTest.java b/src/test/java/janggi/domain/piece/LeapingPieceTest.java new file mode 100644 index 0000000000..99b5e4b398 --- /dev/null +++ b/src/test/java/janggi/domain/piece/LeapingPieceTest.java @@ -0,0 +1,26 @@ +package janggi.domain.piece; + +import janggi.domain.side.TeamType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LeapingPieceTest { + + @Test + @DisplayName("이동 경로에 기물이 존재할 경우 예외 발생") + void validateCanMove_Fail_Exist_Piece_In_Path() { + // given + List piecesInPath = new ArrayList<>(List.of(new Jol(TeamType.CHU))); + Ma ma = new Ma(TeamType.CHU); + + // when & then + assertThatThrownBy(() -> ma.validateCanMove(piecesInPath)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동 경로에 기물이 존재하여 이동할 수 없습니다."); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/piece/MaTest.java b/src/test/java/janggi/domain/piece/MaTest.java new file mode 100644 index 0000000000..53c4eddff8 --- /dev/null +++ b/src/test/java/janggi/domain/piece/MaTest.java @@ -0,0 +1,93 @@ +/** + * 마는 LeapingPiece에서 테스트해줘야 + * 근데 각 기물 이동 규칙은 여기서 테스트해야 함 + */ +package janggi.domain.piece; + +import janggi.domain.Position; +import janggi.domain.side.TeamType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class MaTest { + + private Ma ma; + + @BeforeEach + void setUp() { + ma = new Ma(TeamType.HAN); + } + + @ParameterizedTest + @DisplayName("마는 이동 경로(멱)에 장애물이 없다면 L자 모양으로 이동할 수 있다.") + @CsvSource({ + "4, 4, 5, 6", + "4, 4, 3, 6", + "4, 4, 5, 2", + "4, 4, 3, 2" + }) + void validateCanMove_Success(int startX, int startY, int endX, int endY) { + // given + Position start = new Position(startX, startY); + Position end = new Position(endX, endY); + + // when & then + assertThatCode(() -> ma.getPiecePositionsInPath(start, end)) + .doesNotThrowAnyException(); + } + + /** + * Board에서 진행 + */ +// @Test +// @DisplayName("제자리로 이동할 경우 예외 발생") +// void validateCanMove_Fail_Same_Position() { +// // given +// Position start = new Position(4, 4); +// Position sameEnd = new Position(4, 4); +// +// // when & then +// assertThatThrownBy(() -> ma.validateCanMove(start, sameEnd, board)) +// .isInstanceOf(IllegalArgumentException.class) +// .hasMessage("출발지와 목적지가 동일합니다."); +// } + + @Test + @DisplayName("마가 이동할 수 없는 위치일 경우 예외 발생") + void validateCanMove_Fail_Invalid_Position() { + // given + Position start = new Position(4, 4); + Position diagonalEnd = new Position(5, 5); + + // when & then + assertThatThrownBy(() -> ma.getPiecePositionsInPath(start, diagonalEnd)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다."); + } + + // LeapingPiece에서 테스트 +// @ParameterizedTest +// @DisplayName("이동 경로 중간에 기물이 존재할 경우 예외 발생") +// @CsvSource({ +// "4, 4, 6, 5", +// "4, 4, 6, 3", +// "4, 4, 2, 5", +// "4, 4, 2, 3" +// }) +// void validateCanMove_Fail_ObstacleExist(int startX, int startY, int endX, int endY) { +// // given +// Position start = new Position(startX, startY); +// Position end = new Position(endX, endY); +// +// // when & then +// assertThatThrownBy(() -> ma.getPiecePositionsInPath(start, end)) +// .isInstanceOf(IllegalArgumentException.class) +// .hasMessage("이동 경로에 기물이 존재하여 이동할 수 없습니다."); +// } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/piece/PoTest.java b/src/test/java/janggi/domain/piece/PoTest.java new file mode 100644 index 0000000000..7f292cdb6f --- /dev/null +++ b/src/test/java/janggi/domain/piece/PoTest.java @@ -0,0 +1,111 @@ +package janggi.domain.piece; + +import janggi.domain.Position; +import janggi.domain.side.TeamType; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PoTest { + + private Po po; + + @BeforeEach + void setUp() { + po = new Po(TeamType.HAN); + } + + @Test + @DisplayName("포는 경로 사이에 기물이 정확히 하나(포 제외) 있을 때만 넘어갈 수 있다.") + void validateCanMove_Success() { + // given +// Position start = new Position(startX, startY); +// Position end = new Position(endX, endY); + List piecesInPath = new ArrayList<>(List.of(new Jol(TeamType.CHU))); + + // when & then + assertThatCode(() -> po.validateCanMove(piecesInPath)) + .doesNotThrowAnyException(); + } + + /** + * 이건 Board 테스트에서 + */ +// @Test +// @DisplayName("제자리로 이동할 경우 예외 발생") +// void validateCanMove_Fail_Same_Position() { +// // given +// Position start = new Position(4, 4); +// Position sameEnd = new Position(4, 4); +// +// // when & then +// assertThatThrownBy(() -> po.validateCanMove(start, sameEnd, board)) +// .isInstanceOf(IllegalArgumentException.class) +// .hasMessage("출발지와 목적지가 동일합니다."); +// } + + /** + * 이건 SlidingPiece에서 테스트 + */ + @Test + @DisplayName("포가 이동할 수 없는 위치일 경우 예외 발생") + void validateCanMove_Fail_InvalidPattern() { + // given + Position start = new Position(4, 4); + Position invalidEnd = new Position(5, 5); + + // when & then + assertThatThrownBy(() -> po.getPiecePositionsInPath(start, invalidEnd)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("이동 경로에 기물이 하나도 없을 경우 예외 발생") + void validateCanMove_Fail_NoObstacle() { +// Position start = new Position(4, 4); +// Position end = new Position(4, 6); + List pieces = Collections.emptyList(); + + // when & then + assertThatThrownBy(() -> po.validateCanMove(pieces)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동 경로에 기물이 존재하지 않아 이동할 수 없습니다."); + } + + @Test + @DisplayName("이동 경로에 기물이 2개 이상일 경우 예외 발생") + void validateCanMove_Fail_TooManyObstacles() { +// Position start = new Position(2, 10); +// Position end = new Position(2, 1); + + List pieces = new ArrayList<>(List.of(new Cha(TeamType.CHU), new Gung(TeamType.HAN))); + + // when & then + assertThatThrownBy(() -> po.validateCanMove(pieces)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동 경로에 기물이 2개 이상 존재합니다."); + } + + @Test + @DisplayName("포의 이동 경로에 포가 존재할 경우 예외 발생") + void validateCanMove_Fail_PoAsObstacle() { +// Position start = new Position(2, 9); +// Position end = new Position(2, 7); + List pieces = new ArrayList<>(List.of(new Po(TeamType.CHU))); + + // when & then + assertThatThrownBy(() -> po.validateCanMove(pieces)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("포는 포를 넘을 수 없습니다."); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/piece/SaTest.java b/src/test/java/janggi/domain/piece/SaTest.java new file mode 100644 index 0000000000..b2afb6394f --- /dev/null +++ b/src/test/java/janggi/domain/piece/SaTest.java @@ -0,0 +1,84 @@ +/** + * SteppingPiece에서 테스트 + */ +package janggi.domain.piece; + +import janggi.domain.Position; +import janggi.domain.side.TeamType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +class SaTest { + + private Sa sa; + + @BeforeEach + void setUp() { + sa = new Sa(TeamType.HAN); + } + + @ParameterizedTest + @DisplayName("사는 상하좌우 및 대각선으로 한 칸 이동할 수 있다.") + @CsvSource({ + "4, 1, 4, 2", + "4, 1, 5, 1", + "4, 1, 3, 1", + "4, 1, 5, 2", + "4, 1, 3, 2" + }) + void validateCanMove_Success(int startX, int startY, int endX, int endY) { + // given + Position start = new Position(startX, startY); + Position end = new Position(endX, endY); + + // when & then + assertThatCode(() -> sa.getPiecePositionsInPath(start, end)) + .doesNotThrowAnyException(); + } + + /** + * 제자리 테스트는 Board에서 + */ +// @Test +// @DisplayName("제자리로 이동할 경우 예외 발생") +// void validateCanMove_Fail_Same_Position() { +// // given +// Position start = new Position(4, 4); +// Position sameEnd = new Position(4, 4); +// +// // when & then +// assertThatThrownBy(() -> sa.validateCanMove(start, sameEnd, board)) +// .isInstanceOf(IllegalArgumentException.class) +// .hasMessage("출발지와 목적지가 동일합니다."); +// } + + @Test + @DisplayName("사가 이동할 수 없는 위치일 경우 예외 발생") + void validateCanMove_Fail_Invalid_Position() { + // given + Position start = new Position(4, 1); + Position moveTwoSteps = new Position(4, 3); + Position moveLongDiagonal = new Position(6, 3); + Position knightMove = new Position(5, 3); + + // when & then + assertAll( + () -> assertThatThrownBy(() -> sa.getPiecePositionsInPath(start, moveTwoSteps)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다."), + () -> assertThatThrownBy(() -> sa.getPiecePositionsInPath(start, moveLongDiagonal)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다."), + () -> assertThatThrownBy(() -> sa.getPiecePositionsInPath(start, knightMove)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다.") + ); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/piece/SangTest.java b/src/test/java/janggi/domain/piece/SangTest.java new file mode 100644 index 0000000000..c917425740 --- /dev/null +++ b/src/test/java/janggi/domain/piece/SangTest.java @@ -0,0 +1,96 @@ +/** + * LeapingPiece에서 테스트 + */ +package janggi.domain.piece; + +import janggi.domain.Position; +import janggi.domain.side.TeamType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +class SangTest { + + private Sang sang; + + @BeforeEach + void setUp() { + sang = new Sang(TeamType.HAN); + } + + @ParameterizedTest + @DisplayName("상은 경로상에 장애물이 없다면 '1칸 직선 + 2칸 대각선'으로 이동할 수 있다.") + @CsvSource({ + "4, 4, 6, 7", + "4, 4, 2, 7", + "4, 5, 6, 2", + "4, 4, 2, 1" + }) + void validateCanMove_Success(int startX, int startY, int endX, int endY) { + // given + Position start = new Position(startX, startY); + Position end = new Position(endX, endY); + + // when & then + assertThatCode(() -> sang.getPiecePositionsInPath(start, end)) + .doesNotThrowAnyException(); + } + + /** + * Board에서 진행 + */ +// @Test +// @DisplayName("제자리로 이동할 경우 예외 발생") +// void validateCanMove_Fail_Same_Position() { +// // given +// Position start = new Position(4, 4); +// Position sameEnd = new Position(4, 4); +// +// // when & then +// assertThatThrownBy(() -> sang.validateCanMove(start, sameEnd, board)) +// .isInstanceOf(IllegalArgumentException.class) +// .hasMessage("출발지와 목적지가 동일합니다."); +// } + + @Test + @DisplayName("상이 이동할 수 없는 위치일 경우 예외 발생") + void validateCanMove_Fail_Invalid_Position() { + Position start = new Position(4, 4); + + assertAll( + () -> assertThatThrownBy(() -> sang.getPiecePositionsInPath(start, new Position(4, 7))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다."), + () -> assertThatThrownBy(() -> sang.getPiecePositionsInPath(start, new Position(5, 6))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다.") + ); + } +/** + * LeapingPiece에서 테스트 + */ +// @ParameterizedTest +// @DisplayName("이동 경로 중간에 기물이 존재할 경우 예외 발생") +// @CsvSource({ +// "4, 4, 7, 6", +// "4, 4, 1, 6", +// "4, 4, 7, 2", +// "4, 4, 1, 2" +// }) +// void validateCanMove_Fail_ObstacleExist(int startX, int startY, int endX, int endY) { +// // given +// Position start = new Position(startX, startY); +// Position end = new Position(endX, endY); +// +// // when & then +// assertThatThrownBy(() -> sang.validateCanMove(start, end, board)) +// .isInstanceOf(IllegalArgumentException.class) +// .hasMessage("이동 경로에 기물이 존재하여 이동할 수 없습니다."); +// } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/piece/SteppingPieceTest.java b/src/test/java/janggi/domain/piece/SteppingPieceTest.java new file mode 100644 index 0000000000..5e58b12cb9 --- /dev/null +++ b/src/test/java/janggi/domain/piece/SteppingPieceTest.java @@ -0,0 +1,28 @@ +package janggi.domain.piece; + +import janggi.domain.side.TeamType; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SteppingPieceTest { + + + @Test + @DisplayName("이동 경로에 기물이 존재할 경우 예외 발생") + void validateCanMove_Fail_Exist_Pieces_In_Path() { + // given + List piecesInPath = new ArrayList<>(List.of(new Jol(TeamType.CHU))); + Gung gung = new Gung(TeamType.CHU); + + // when & then + assertThatThrownBy(() -> gung.validateCanMove(piecesInPath)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동 경로에 기물이 존재하여 이동할 수 없습니다."); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/side/ChuTest.java b/src/test/java/janggi/domain/side/ChuTest.java new file mode 100644 index 0000000000..442a0d2ed1 --- /dev/null +++ b/src/test/java/janggi/domain/side/ChuTest.java @@ -0,0 +1,85 @@ +package janggi.domain.side; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import janggi.domain.Position; +import janggi.domain.piece.Cha; +import janggi.domain.piece.Gung; +import janggi.domain.piece.Jol; +import janggi.domain.piece.Ma; +import janggi.domain.piece.Po; +import janggi.domain.piece.Sa; +import janggi.domain.piece.Sang; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ChuTest { + + @Test + @DisplayName("초나라 팀은 초기 배치대로 기물을 가진다.") + void createInitialChu() { + // given + Chu chu = Chu.createInitialChu(); + + // when & then + assertAll( + () -> assertThat(chu.makeSnapShot()).hasSize(16), + () -> assertThat(chu.findPiece(new Position(1, 1))).get().isInstanceOf(Cha.class), + () -> assertThat(chu.findPiece(new Position(9, 1))).get().isInstanceOf(Cha.class), + () -> assertThat(chu.findPiece(new Position(2, 1))).get().isInstanceOf(Ma.class), + () -> assertThat(chu.findPiece(new Position(8, 1))).get().isInstanceOf(Ma.class), + () -> assertThat(chu.findPiece(new Position(3, 1))).get().isInstanceOf(Sang.class), + () -> assertThat(chu.findPiece(new Position(7, 1))).get().isInstanceOf(Sang.class), + () -> assertThat(chu.findPiece(new Position(4, 1))).get().isInstanceOf(Sa.class), + () -> assertThat(chu.findPiece(new Position(6, 1))).get().isInstanceOf(Sa.class), + () -> assertThat(chu.findPiece(new Position(5, 2))).get().isInstanceOf(Gung.class), + () -> assertThat(chu.findPiece(new Position(2, 3))).get().isInstanceOf(Po.class), + () -> assertThat(chu.findPiece(new Position(8, 3))).get().isInstanceOf(Po.class), + () -> assertThat(chu.findPiece(new Position(1, 4))).get().isInstanceOf(Jol.class), + () -> assertThat(chu.findPiece(new Position(3, 4))).get().isInstanceOf(Jol.class), + () -> assertThat(chu.findPiece(new Position(5, 4))).get().isInstanceOf(Jol.class), + () -> assertThat(chu.findPiece(new Position(7, 4))).get().isInstanceOf(Jol.class), + () -> assertThat(chu.findPiece(new Position(9, 4))).get().isInstanceOf(Jol.class), + () -> assertThat(chu.findPiece(new Position(5, 5))).isEmpty() + ); + } + + @Test + @DisplayName("기물을 이동한 새 팀 상태를 반환한다.") + void move() { + // given + Team chu = Chu.createInitialChu(); + Position chuJolPosition = new Position(1, 4); + Position movedChuJolPosition = new Position(1, 5); + + // when + Team movedChu = chu.move(chuJolPosition, movedChuJolPosition); + + // then + assertAll( + () -> assertThat(movedChu.findPiece(chuJolPosition)).isEmpty(), + () -> assertThat(movedChu.findPiece(movedChuJolPosition)).get().isInstanceOf(Jol.class), + () -> assertThat(chu.findPiece(movedChuJolPosition)).isEmpty(), + () -> assertThat(chu.findPiece(chuJolPosition)).get().isInstanceOf(Jol.class) + ); + } + + @Test + @DisplayName("기물을 제거하면 새 팀 상태를 반환하여 원본은 유지한다.") + void remove() { + // given + Team chu = Chu.createInitialChu(); + Position chuJolPosition = new Position(1, 4); + + // when + Team removedChu = chu.remove(chuJolPosition); + + // then + assertAll( + () -> assertThat(removedChu.findPiece(chuJolPosition)).isEmpty(), + () -> assertThat(removedChu.makeSnapShot()).hasSize(15), + () -> assertThat(chu.findPiece(chuJolPosition)).get().isInstanceOf(Jol.class) + ); + } +} diff --git a/src/test/java/janggi/domain/side/HanTest.java b/src/test/java/janggi/domain/side/HanTest.java new file mode 100644 index 0000000000..5be46b543f --- /dev/null +++ b/src/test/java/janggi/domain/side/HanTest.java @@ -0,0 +1,81 @@ +package janggi.domain.side; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import janggi.domain.Position; +import janggi.domain.piece.Cha; +import janggi.domain.piece.Gung; +import janggi.domain.piece.Jol; +import janggi.domain.piece.Ma; +import janggi.domain.piece.Po; +import janggi.domain.piece.Sa; +import janggi.domain.piece.Sang; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HanTest { + + @Test + @DisplayName("한나라 팀은 초기 배치대로 기물을 가진다.") + void createInitialHan() { + // given + Han han = Han.createInitialHan(); + + // when & then + assertAll( + () -> assertThat(han.makeSnapShot()).hasSize(16), + () -> assertThat(han.findPiece(new Position(1, 10))).get().isInstanceOf(Cha.class), + () -> assertThat(han.findPiece(new Position(9, 10))).get().isInstanceOf(Cha.class), + () -> assertThat(han.findPiece(new Position(2, 10))).get().isInstanceOf(Ma.class), + () -> assertThat(han.findPiece(new Position(8, 10))).get().isInstanceOf(Ma.class), + () -> assertThat(han.findPiece(new Position(3, 10))).get().isInstanceOf(Sang.class), + () -> assertThat(han.findPiece(new Position(7, 10))).get().isInstanceOf(Sang.class), + () -> assertThat(han.findPiece(new Position(4, 10))).get().isInstanceOf(Sa.class), + () -> assertThat(han.findPiece(new Position(6, 10))).get().isInstanceOf(Sa.class), + () -> assertThat(han.findPiece(new Position(5, 9))).get().isInstanceOf(Gung.class), + () -> assertThat(han.findPiece(new Position(2, 8))).get().isInstanceOf(Po.class), + () -> assertThat(han.findPiece(new Position(8, 8))).get().isInstanceOf(Po.class), + () -> assertThat(han.findPiece(new Position(1, 7))).get().isInstanceOf(Jol.class), + () -> assertThat(han.findPiece(new Position(3, 7))).get().isInstanceOf(Jol.class), + () -> assertThat(han.findPiece(new Position(5, 7))).get().isInstanceOf(Jol.class), + () -> assertThat(han.findPiece(new Position(7, 7))).get().isInstanceOf(Jol.class), + () -> assertThat(han.findPiece(new Position(9, 7))).get().isInstanceOf(Jol.class), + () -> assertThat(han.findPiece(new Position(5, 6))).isEmpty() + ); + } + + @Test + @DisplayName("기물을 이동한 새 팀 상태를 반환한다.") + void move() { + // given + Team han = Han.createInitialHan(); + + // when + Team movedHan = han.move(new Position(1, 7), new Position(1, 6)); + + // then + assertAll( + () -> assertThat(movedHan.findPiece(new Position(1, 7))).isEmpty(), + () -> assertThat(movedHan.findPiece(new Position(1, 6))).get().isInstanceOf(Jol.class), + () -> assertThat(han.findPiece(new Position(1, 7))).get().isInstanceOf(Jol.class) + ); + } + + @Test + @DisplayName("기물을 제거하면 새 팀 상태를 반환하여 원본은 유지한다.") + void remove() { + // given + Team han = Han.createInitialHan(); + + // when + Team removedHan = han.remove(new Position(1, 7)); + + // then + assertAll( + () -> assertThat(removedHan.findPiece(new Position(1, 7))).isEmpty(), + () -> assertThat(removedHan.makeSnapShot()).hasSize(15), + () -> assertThat(han.findPiece(new Position(1, 7))).get().isInstanceOf(Jol.class) + ); + } +}