diff --git a/.gitmessage.txt b/.gitmessage.txt new file mode 100644 index 00000000000..18e39abc3d9 --- /dev/null +++ b/.gitmessage.txt @@ -0,0 +1,4 @@ + + +Co-authored-by: Hyunguk Ryu + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..8ad0f4d0ad9 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,63 @@ +## 프로그램 흐름 + +- 사용자로부터 게임 시작 여부를 입력받는다. + - `start`를 입력받으면 게임을 생성하고 초기 체스판의 상태를 출력한다. + - `end`를 입력받으면 프로그램을 종료한다. +- 기물 이동 여부를 입력받는다. + - `move b2 b4`와 같이 입력받으면 `b2`에 존재하는 기물을 `b4`로 이동시킨다. + - `end`를 입력받으면 프로그램을 종료한다. + +## 도메인별 기능 + +### Piece + +- [x] 출발지 & 목적지 정보를 토대로 기물이 이동할 수 있는지 검사한다. +- [x] 입력받은 색상을 토대로 자신의 적 여부를 반환한다. + +#### Rook + +- 상/하/좌/우 거리 제한 없이 이동 가능 +- 비었거나 적 기물이 존재하는 칸으로만 이동 가능 +- 이동 경로에 기물이 존재하면 이동 불가 + +#### Bishop + +- 대각선 방향 거리 제한 없이 이동 가능 +- 비었거나 적 기물이 존재하는 칸으로만 이동 가능 +- 이동 경로에 기물이 존재하면 이동 불가 + +#### Queen + +- 상/하/좌/우, 대각선 방향 거리 제한 없이 이동 가능 +- 비었거나 적 기물이 존재하는 칸으로만 이동 가능 +- 이동 경로에 기물이 존재하면 이동 불가 + +#### King + +- 상/하/좌/우, 대각선 방향 중 한 칸만 이동 가능 +- 비었거나 적 기물이 존재하는 칸으로만 이동 가능 + +#### Knight + +- 상/하/좌/우 중 한 칸 이동 후 전진 방향의 대각석으로 한 칸 이동 가능 +- 비었거나 적 기물이 존재하는 칸으로만 이동 가능 +- 이동 경로에 기물이 존재해도 이동 가능 + +#### Pawn + +- 특정 조건에 따라 이동 선택 + - 기본적으로 한 칸 전진 가능 + - 출발지가 초기 위치일 경우 두 칸 전진 가능 + - 전진 경로 혹은 목적지에 기물이 존재하면 이동 불가 + - 전진 방향 대각선 한 칸에 적 기물이 존재시 이동 가능 + +## Board + +- [x] 특정 색상의 기물 이동 + - `출발지` == `도착지`인 경우 예외 처리 + - 출발지에 기물이 존재하지 않으면 예외 처리 + - 상대 색상의 기물을 이동시키려 할 시 예외 처리 + +## ChessGame + +- [x] `흰색` 플레이어와 `검은색` 플레이어의 게임 순서 관리 및 `Board`에 기물 이동 요청 전달 diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000000..ff3accb42d4 --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,12 @@ +import controller.GameController; +import domain.game.ChessGame; +import view.InputView; +import view.OutputView; + +public class Application { + public static void main(String[] args) { + ChessGame chessGame = ChessGame.initGame(); + GameController gameController = new GameController(new InputView(), new OutputView(), chessGame); + gameController.run(); + } +} diff --git a/src/main/java/controller/GameController.java b/src/main/java/controller/GameController.java new file mode 100644 index 00000000000..605348a46cf --- /dev/null +++ b/src/main/java/controller/GameController.java @@ -0,0 +1,62 @@ +package controller; + +import domain.game.ChessGame; +import domain.game.GameCommand; +import domain.game.GameCommandType; +import dto.BoardDto; +import view.InputView; +import view.OutputView; + +public class GameController { + private final InputView inputView; + private final OutputView outputView; + private final ChessGame chessGame; + + public GameController(final InputView inputView, final OutputView outputView, final ChessGame chessGame) { + this.inputView = inputView; + this.outputView = outputView; + this.chessGame = chessGame; + } + + public void run() { + initGame(); + + if (chessGame.isGameRunning()) { + gameStart(); + } + } + + private void initGame() { + try { + GameCommand gameCommand = inputCommand(); + gameCommand.execute(chessGame); + } catch (Exception e) { + outputView.printErrorMessage(e.getMessage()); + initGame(); + } + } + + private GameCommand inputCommand() { + String[] inputValues = inputView.inputCommand().split(" "); + return GameCommandType.of(inputValues); + } + + private void gameStart() { + outputView.printWelcomeMessage(); + while (chessGame.isGameRunning()) { + BoardDto boardDto = BoardDto.from(chessGame.piecePositions()); + outputView.printBoard(boardDto); + playTurn(); + } + } + + private void playTurn() { + try { + GameCommand gameCommand = inputCommand(); + gameCommand.execute(chessGame); + } catch (Exception e) { + outputView.printErrorMessage(e.getMessage()); + playTurn(); + } + } +} diff --git a/src/main/java/domain/board/Board.java b/src/main/java/domain/board/Board.java new file mode 100644 index 00000000000..fd6885878f9 --- /dev/null +++ b/src/main/java/domain/board/Board.java @@ -0,0 +1,52 @@ +package domain.board; + +import domain.piece.Piece; +import domain.piece.PieceColor; + +import java.util.Collections; +import java.util.Map; + +public class Board { + private final Map piecePositions; + + public Board(final Map piecePositions) { + this.piecePositions = piecePositions; + } + + public void movePiece(final PieceColor pieceColor, final Position source, final Position destination) { + validatePosition(pieceColor, source, destination); + + Piece targetPiece = piecePositions.get(source); + targetPiece.move(source, destination, this); + + piecePositions.put(destination, targetPiece); + piecePositions.remove(source); + } + + private void validatePosition(final PieceColor pieceColor, final Position source, final Position destination) { + if (source.equals(destination)) { + throw new IllegalArgumentException("출발지와 목적지가 같을 수 없습니다."); + } + + if (!piecePositions.containsKey(source)) { + throw new IllegalArgumentException("출발지에 기물이 존재하지 않습니다."); + } + + if (!piecePositions.get(source).isTeam(pieceColor)) { + throw new IllegalArgumentException("상대방의 기물을 이동시킬 수 없습니다."); + } + } + + public boolean existPiece(final Position position) { + return piecePositions.containsKey(position); + } + + public boolean existTeamColor(final Position position, final PieceColor teamColor) { + return piecePositions.get(position).isTeam(teamColor); + + } + + public Map piecePositions() { + return Collections.unmodifiableMap(piecePositions); + } +} diff --git a/src/main/java/domain/board/BoardInitializer.java b/src/main/java/domain/board/BoardInitializer.java new file mode 100644 index 00000000000..967329d6e62 --- /dev/null +++ b/src/main/java/domain/board/BoardInitializer.java @@ -0,0 +1,87 @@ +package domain.board; + +import domain.piece.*; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static domain.board.File.D; +import static domain.board.File.E; +import static domain.board.File.A; +import static domain.board.File.B; +import static domain.board.File.C; +import static domain.board.File.F; +import static domain.board.File.G; +import static domain.board.File.H; +import static domain.board.Rank.ONE; +import static domain.board.Rank.TWO; +import static domain.board.Rank.SEVEN; +import static domain.board.Rank.EIGHT; +import static domain.piece.PieceColor.BLACK; +import static domain.piece.PieceColor.WHITE; + +public class BoardInitializer { + public static Board initBoard() { + return new Board(new HashMap<>(initPieces())); + } + + private static Map initPieces() { + final Map piecePositions = new HashMap<>(); + initKing(piecePositions); + initQueen(piecePositions); + initBishop(piecePositions); + initKnight(piecePositions); + initRook(piecePositions); + initBlackPawns(piecePositions); + initWhitePawns(piecePositions); + + return piecePositions; + } + + private static void initKing(final Map piecePositions) { + piecePositions.put(position(E, ONE), new King(WHITE)); + piecePositions.put(position(E, EIGHT), new King(BLACK)); + } + + private static void initQueen(final Map piecePositions) { + piecePositions.put(position(D, ONE), new Queen(WHITE)); + piecePositions.put(position(D, EIGHT), new Queen(BLACK)); + } + + private static void initBishop(final Map piecePositions) { + piecePositions.put(position(C, ONE), new Bishop(WHITE)); + piecePositions.put(position(F, ONE), new Bishop(WHITE)); + piecePositions.put(position(C, EIGHT), new Bishop(BLACK)); + piecePositions.put(position(F, EIGHT), new Bishop(BLACK)); + } + + private static void initKnight(final Map piecePositions) { + piecePositions.put(position(B, ONE), new Knight(WHITE)); + piecePositions.put(position(G, ONE), new Knight(WHITE)); + piecePositions.put(position(B, EIGHT), new Knight(BLACK)); + piecePositions.put(position(G, EIGHT), new Knight(BLACK)); + } + + private static void initRook(final Map piecePositions) { + piecePositions.put(position(A, ONE), new Rook(WHITE)); + piecePositions.put(position(H, ONE), new Rook(WHITE)); + piecePositions.put(position(A, EIGHT), new Rook(BLACK)); + piecePositions.put(position(H, EIGHT), new Rook(BLACK)); + + } + + private static void initWhitePawns(final Map piecePositions) { + Arrays.stream(File.values()) + .forEach(file -> piecePositions.put(position(file, TWO), new Pawn(WHITE))); + } + + private static void initBlackPawns(final Map piecePositions) { + Arrays.stream(File.values()) + .forEach(file -> piecePositions.put(position(file, SEVEN), new Pawn(BLACK))); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/main/java/domain/board/File.java b/src/main/java/domain/board/File.java new file mode 100644 index 00000000000..f4504ddfb76 --- /dev/null +++ b/src/main/java/domain/board/File.java @@ -0,0 +1,38 @@ +package domain.board; + +import java.util.Arrays; + +public enum File { + A(0), + B(1), + C(2), + D(3), + E(4), + F(5), + G(6), + H(7); + + private final int index; + + File(final int index) { + this.index = index; + } + + public static File of(final String value) { + return Arrays.stream(values()) + .filter(file -> file.name().equals(value.toUpperCase())) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 File 값입니다.")); + } + + public static File of(final int index) { + return Arrays.stream(values()) + .filter(file -> file.getIndex() == index) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 인덱스입니다.")); + } + + public int getIndex() { + return this.index; + } +} diff --git a/src/main/java/domain/board/Position.java b/src/main/java/domain/board/Position.java new file mode 100644 index 00000000000..51b2270f99f --- /dev/null +++ b/src/main/java/domain/board/Position.java @@ -0,0 +1,40 @@ +package domain.board; + +import domain.piece.MovementDirection; + +public record Position(File file, Rank rank) { + private static final int POSITIONS_VALUES_SIZE = 2; + private static final int FILE_INDEX = 0; + private static final int RANK_INDEX = 1; + + public static Position of(final String value) { + String[] positionValues = value.split(""); + validatePositionValue(positionValues); + + File file = File.of(positionValues[FILE_INDEX]); + Rank rank = Rank.of(positionValues[RANK_INDEX]); + + return new Position(file, rank); + } + + private static void validatePositionValue(final String[] values) { + if (values.length != POSITIONS_VALUES_SIZE) { + throw new IllegalArgumentException("잘못된 위치값입니다."); + } + } + + public Position next(final MovementDirection movementDirection) { + File nextFile = File.of(this.columnIndex() + movementDirection.getColumnDistance()); + Rank nextRank = Rank.of(this.rowIndex() + movementDirection.getRowDistance()); + + return new Position(nextFile, nextRank); + } + + public int rowIndex() { + return rank.getIndex(); + } + + public int columnIndex() { + return file.getIndex(); + } +} diff --git a/src/main/java/domain/board/Rank.java b/src/main/java/domain/board/Rank.java new file mode 100644 index 00000000000..32697ddf3b7 --- /dev/null +++ b/src/main/java/domain/board/Rank.java @@ -0,0 +1,52 @@ +package domain.board; + +import java.util.Arrays; + +public enum Rank { + ONE(1), + TWO(2), + THREE(3), + FOUR(4), + FIVE(5), + SIX(6), + SEVEN(7), + EIGHT(8); + + private static final int BASE_INDEX = 8; + + private final int value; + + Rank(final int value) { + this.value = value; + } + + public static Rank of(final String value) { + return Arrays.stream(Rank.values()) + .filter(rank -> rank.value() == parseNumber(value)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 Rank값입니다.")); + } + + private static int parseNumber(final String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Rank는 정수만 입력할 수 있습니다."); + } + } + + public static Rank of(final int index) { + return Arrays.stream(Rank.values()) + .filter(rank -> rank.getIndex() == index) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 인덱스입니다.")); + } + + public int value() { + return value; + } + + public int getIndex() { + return BASE_INDEX - value; + } +} diff --git a/src/main/java/domain/game/ChessGame.java b/src/main/java/domain/game/ChessGame.java new file mode 100644 index 00000000000..837e3c9595e --- /dev/null +++ b/src/main/java/domain/game/ChessGame.java @@ -0,0 +1,47 @@ +package domain.game; + +import domain.board.Board; +import domain.board.BoardInitializer; +import domain.board.Position; +import domain.piece.Piece; +import domain.piece.PieceColor; + +import java.util.Map; + +import static domain.piece.PieceColor.WHITE; + +public class ChessGame { + private final Board board; + + private PieceColor currentColor = WHITE; + private boolean isGameRunning = false; + + private ChessGame(final Board board) { + this.board = board; + } + + public static ChessGame initGame() { + return new ChessGame(BoardInitializer.initBoard()); + } + + public void gameStart() { + isGameRunning = true; + } + + public void gameEnd() { + isGameRunning = false; + } + + public boolean isGameRunning() { + return isGameRunning; + } + + public Map piecePositions() { + return board.piecePositions(); + } + + public void movePiece(final Position source, final Position destination) { + board.movePiece(currentColor, source, destination); + currentColor = currentColor.toggle(); + } +} diff --git a/src/main/java/domain/game/End.java b/src/main/java/domain/game/End.java new file mode 100644 index 00000000000..0e381c82c82 --- /dev/null +++ b/src/main/java/domain/game/End.java @@ -0,0 +1,9 @@ +package domain.game; + +public class End implements GameCommand { + + @Override + public void execute(final ChessGame chessGame) { + chessGame.gameEnd(); + } +} diff --git a/src/main/java/domain/game/GameCommand.java b/src/main/java/domain/game/GameCommand.java new file mode 100644 index 00000000000..80c0ac6f9a2 --- /dev/null +++ b/src/main/java/domain/game/GameCommand.java @@ -0,0 +1,5 @@ +package domain.game; + +public interface GameCommand { + void execute(ChessGame chessGame); +} diff --git a/src/main/java/domain/game/GameCommandType.java b/src/main/java/domain/game/GameCommandType.java new file mode 100644 index 00000000000..f11cab99098 --- /dev/null +++ b/src/main/java/domain/game/GameCommandType.java @@ -0,0 +1,48 @@ +package domain.game; + +import domain.board.Position; + +import java.util.Arrays; +import java.util.function.Function; + +public enum GameCommandType { + START("start", command -> new Start()), + MOVE("move", GameCommandType::toMove), + END("end", command -> new End()); + + private static final int COMMAND_INDEX = 0; + private static final int BEFORE_POSITION = 1; + private static final int AFTER_POSITION = 2; + private static final int COMMAND_WITH_POSITIONS_SIZE = 3; + + private final String value; + private final Function gameCommand; + + GameCommandType(final String value, final Function gameCommand) { + this.value = value; + this.gameCommand = gameCommand; + } + + public static GameCommand of(final String[] values) { + return Arrays.stream(GameCommandType.values()) + .filter(it -> it.value.equalsIgnoreCase(values[COMMAND_INDEX])) + .map(it -> it.gameCommand.apply(values)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 명령어입니다!")); + } + + private static Move toMove(final String[] values) { + validatePositionValue(values); + + Position source = Position.of(values[BEFORE_POSITION]); + Position destination = Position.of(values[AFTER_POSITION]); + + return new Move(source, destination); + } + + private static void validatePositionValue(final String[] values) { + if (values.length != COMMAND_WITH_POSITIONS_SIZE) { + throw new IllegalArgumentException("잘못된 출발지/도착지입니다."); + } + } +} diff --git a/src/main/java/domain/game/Move.java b/src/main/java/domain/game/Move.java new file mode 100644 index 00000000000..754477df975 --- /dev/null +++ b/src/main/java/domain/game/Move.java @@ -0,0 +1,22 @@ +package domain.game; + +import domain.board.Position; + +public class Move implements GameCommand { + private final Position source; + private final Position destination; + + public Move(final Position source, final Position destination) { + this.source = source; + this.destination = destination; + } + + @Override + public void execute(final ChessGame chessGame) { + if (!chessGame.isGameRunning()) { + throw new IllegalArgumentException("아직 게임이 시작되지 않았습니다."); + } + + chessGame.movePiece(source, destination); + } +} diff --git a/src/main/java/domain/game/Start.java b/src/main/java/domain/game/Start.java new file mode 100644 index 00000000000..c895ce4142c --- /dev/null +++ b/src/main/java/domain/game/Start.java @@ -0,0 +1,13 @@ +package domain.game; + +public class Start implements GameCommand { + + @Override + public void execute(final ChessGame chessGame) { + if (chessGame.isGameRunning()) { + throw new IllegalArgumentException("이미 게임이 진행중입니다."); + } + + chessGame.gameStart(); + } +} diff --git a/src/main/java/domain/piece/Bishop.java b/src/main/java/domain/piece/Bishop.java new file mode 100644 index 00000000000..90893491c4e --- /dev/null +++ b/src/main/java/domain/piece/Bishop.java @@ -0,0 +1,54 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +import java.util.List; +import java.util.stream.Stream; + +import static domain.piece.CommonMovementDirection.calculateDirection; +import static domain.piece.PieceType.BISHOP; + +public class Bishop extends Piece { + private static final PieceType PIECE_TYPE = BISHOP; + + public Bishop(final PieceColor color) { + super(color); + } + + @Override + public void move(final Position source, final Position destination, final Board board) { + CommonMovementDirection movementDirection = calculateDirection(source, destination); + movementDirection.checkBishopMovableMovement(); + + List movePaths = Stream.iterate(source, current -> current.next(movementDirection)) + .takeWhile(current -> current.equals(source) || isContinuable(current, destination, board)) + .toList(); + + Position alivePosition = movePaths.get(movePaths.size() - 1).next(movementDirection); + checkAlivePosition(alivePosition, board); + } + + private boolean isContinuable(final Position current, final Position destination, Board board) { + if (current.equals(destination)) { + return false; + } + + if (board.existPiece(current)) { + throw new IllegalArgumentException("목적지 경로에 기물이 존재하여 이동할 수 없습니다."); + } + + return true; + } + + private void checkAlivePosition(final Position alivePosition, final Board board) { + if (board.existPiece(alivePosition) && board.existTeamColor(alivePosition, color)) { + throw new IllegalArgumentException("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + } + + @Override + public PieceType pieceType() { + return PIECE_TYPE; + } +} diff --git a/src/main/java/domain/piece/CommonMovementDirection.java b/src/main/java/domain/piece/CommonMovementDirection.java new file mode 100644 index 00000000000..0ddb40fda33 --- /dev/null +++ b/src/main/java/domain/piece/CommonMovementDirection.java @@ -0,0 +1,74 @@ +package domain.piece; + +import domain.board.Position; + +import java.util.Arrays; +import java.util.List; +import java.util.function.BiPredicate; + +public enum CommonMovementDirection implements MovementDirection { + UP(-1, 0, (rowDistance, columnDistance) -> rowDistance < 0 && columnDistance == 0), + DOWN(1, 0, (rowDistance, columnDistance) -> rowDistance > 0 && columnDistance == 0), + RIGHT(0, 1, (rowDistance, columnDistance) -> rowDistance == 0 && columnDistance > 0), + LEFT(0, -1, (rowDistance, columnDistance) -> rowDistance == 0 && columnDistance < 0), + UP_RIGHT(-1, 1, (rowDistance, columnDistance) -> rowDistance < 0 && columnDistance > 0), + UP_LEFT(-1, -1, (rowDistance, columnDistance) -> rowDistance < 0 && columnDistance < 0), + DOWN_RIGHT(1, 1, (rowDistance, columnDistance) -> rowDistance > 0 && columnDistance > 0), + DOWN_LEFT(1, -1, (rowDistance, columnDistance) -> rowDistance > 0 && columnDistance < 0); + + private final int rowDistance; + private final int columnDistance; + private final BiPredicate condition; + + CommonMovementDirection(final int rowDistance, final int columnDistance, final BiPredicate condition) { + this.rowDistance = rowDistance; + this.columnDistance = columnDistance; + this.condition = condition; + } + + public static CommonMovementDirection calculateDirection(final Position source, final Position destination) { + final int rowDifference = destination.rowIndex() - source.rowIndex(); + final int columnDifference = destination.columnIndex() - source.columnIndex(); + + validateDistance(rowDifference, columnDifference); + + return Arrays.stream(CommonMovementDirection.values()) + .filter(unitVector -> unitVector.condition.test(rowDifference, columnDifference)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("상/하/좌/우 혹은 대각선으로 이동할 수 없는 칸입니다.")); + } + + private static void validateDistance(final int rowDistance, final int columnDistance) { + if (rowDistance == 0 && columnDistance == 0 + || (!(Math.abs(rowDistance) == Math.abs(columnDistance)) + && !(rowDistance == 0 || columnDistance == 0))) { + throw new IllegalArgumentException(("상/하/좌/우 혹은 대각선으로 이동할 수 없는 칸입니다.")); + } + } + + public void checkBishopMovableMovement() { + List bishopMovableMovement = List.of(UP_RIGHT, UP_LEFT, DOWN_RIGHT, DOWN_LEFT); + + if (!bishopMovableMovement.contains(this)) { + throw new IllegalArgumentException("비숍이 이동할 수 있는 방향이 아닙니다."); + } + } + + public void checkRookMovableMovement() { + List rookMovableMovement = List.of(UP, DOWN, RIGHT, LEFT); + + if (!rookMovableMovement.contains(this)) { + throw new IllegalArgumentException("룩이 이동할 수 있는 방향이 아닙니다."); + } + } + + @Override + public int getRowDistance() { + return rowDistance; + } + + @Override + public int getColumnDistance() { + return columnDistance; + } +} diff --git a/src/main/java/domain/piece/King.java b/src/main/java/domain/piece/King.java new file mode 100644 index 00000000000..8bf984368ba --- /dev/null +++ b/src/main/java/domain/piece/King.java @@ -0,0 +1,45 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +import static domain.piece.PieceType.KING; +import static domain.piece.CommonMovementDirection.calculateDirection; + +public class King extends Piece { + private static final PieceType PIECE_TYPE = KING; + + public King(final PieceColor color) { + super(color); + } + + @Override + public void move(final Position source, final Position destination, final Board board) { + CommonMovementDirection movementDirection = calculateDirection(source, destination); + checkMoveDistance(source, destination, movementDirection); + + Position alivePosition = source.next(movementDirection); + + checkAlivePosition(alivePosition, board); + } + + private void checkMoveDistance(final Position source, final Position destination, final CommonMovementDirection movementDirection) { + int rowDistance = destination.rowIndex() - source.rowIndex(); + int columnDistance = destination.columnIndex() - source.columnIndex(); + + if (movementDirection.getRowDistance() != rowDistance || movementDirection.getColumnDistance() != columnDistance) { + throw new IllegalArgumentException("이동할 수 없는 거리입니다."); + } + } + + private void checkAlivePosition(final Position alivePosition, final Board board) { + if (board.existPiece(alivePosition) && board.existTeamColor(alivePosition, color)) { + throw new IllegalArgumentException("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + } + + @Override + public PieceType pieceType() { + return PIECE_TYPE; + } +} diff --git a/src/main/java/domain/piece/Knight.java b/src/main/java/domain/piece/Knight.java new file mode 100644 index 00000000000..18c92df1faa --- /dev/null +++ b/src/main/java/domain/piece/Knight.java @@ -0,0 +1,34 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +import static domain.piece.PieceType.KNIGHT; +import static domain.piece.KnightMovementDirection.calculateDirection; + +public class Knight extends Piece { + private static final PieceType PIECE_TYPE = KNIGHT; + + public Knight(final PieceColor color) { + super(color); + } + + @Override + public void move(final Position source, final Position destination, final Board board) { + KnightMovementDirection movementDirection = calculateDirection(source, destination); + + Position alivePosition = source.next(movementDirection); + checkAlivePosition(alivePosition, board); + } + + private void checkAlivePosition(final Position alivePosition, final Board board) { + if (board.existPiece(alivePosition) && board.existTeamColor(alivePosition, color)) { + throw new IllegalArgumentException("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + } + + @Override + public PieceType pieceType() { + return PIECE_TYPE; + } +} diff --git a/src/main/java/domain/piece/KnightMovementDirection.java b/src/main/java/domain/piece/KnightMovementDirection.java new file mode 100644 index 00000000000..eda0dd7d451 --- /dev/null +++ b/src/main/java/domain/piece/KnightMovementDirection.java @@ -0,0 +1,62 @@ +package domain.piece; + +import domain.board.Position; + +import java.util.Arrays; + +public enum KnightMovementDirection implements MovementDirection { + UP_RIGHT(-2, 1), + UP_LEFT(-2, -1), + DOWN_RIGHT(2, 1), + DOWN_LEFT(2, -1), + RIGHT_UP(-1, 2), + RIGHT_DOWN(1, 2), + LEFT_UP(-1, -2), + LEFT_DOWN(1, -2); + + private final int rowDistance; + private final int columnDistance; + + KnightMovementDirection(final int rowDistance, final int columnDistance) { + this.rowDistance = rowDistance; + this.columnDistance = columnDistance; + } + + public static KnightMovementDirection calculateDirection(final Position source, final Position destination) { + final int rowDifference = destination.rowIndex() - source.rowIndex(); + final int columnDifference = destination.columnIndex() - source.columnIndex(); + + return Arrays.stream(values()) + .filter(knightMovementDirection -> knightMovementDirection.matchDistance(rowDifference, columnDifference)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("나이트가 이동할 수 없는 방향입니다.")); + } + + private boolean matchDistance(final int rowDistance, final int columnDistance) { + return this.rowDistance == rowDistance && this.columnDistance == columnDistance; + } + + @Override + public int getRowDistance() { + return rowDistance; + } + + @Override + public int getColumnDistance() { + return columnDistance; + } + + + // TODO : 레거시 + public static boolean isMovableDirection(final Position source, final Position destination) { + return Arrays.stream(values()) + .anyMatch(knightDirection -> knightDirection.find(source, destination)); + } + + private boolean find(final Position source, final Position destination) { + final int rowDistance = destination.rowIndex() - source.rowIndex(); + final int columnDistance = destination.columnIndex() - source.columnIndex(); + + return this.rowDistance == rowDistance && this.columnDistance == columnDistance; + } +} diff --git a/src/main/java/domain/piece/MovementDirection.java b/src/main/java/domain/piece/MovementDirection.java new file mode 100644 index 00000000000..e0413387989 --- /dev/null +++ b/src/main/java/domain/piece/MovementDirection.java @@ -0,0 +1,8 @@ +package domain.piece; + +public interface MovementDirection { + + int getRowDistance(); + + int getColumnDistance(); +} diff --git a/src/main/java/domain/piece/Pawn.java b/src/main/java/domain/piece/Pawn.java new file mode 100644 index 00000000000..31f4a61b9a0 --- /dev/null +++ b/src/main/java/domain/piece/Pawn.java @@ -0,0 +1,69 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +import static domain.board.Rank.SEVEN; +import static domain.board.Rank.TWO; +import static domain.piece.PieceType.PAWN; +import static domain.piece.PawnMovementDirection.calculateDirection; +import static domain.piece.PieceColor.BLACK; +import static domain.piece.PieceColor.WHITE; + +public class Pawn extends Piece { + private static final PieceType PIECE_TYPE = PAWN; + + public Pawn(final PieceColor color) { + super(color); + } + + @Override + public void move(final Position source, final Position destination, final Board board) { + PawnMovementDirection movementDirection = calculateDirection(color, source, destination); + if (movementDirection.isCrossStep()) { + checkAlivePositionOfCrossStep(source.next(movementDirection), board); + return; + } + + if (movementDirection.isTwoStep()) { + checkIsStartPosition(source); + } + checkMovePaths(source, movementDirection, board); + } + + private void checkAlivePositionOfCrossStep(final Position alivePosition, final Board board) { + if (!board.existPiece(alivePosition) || board.existTeamColor(alivePosition, color)) { + throw new IllegalArgumentException("적 기물이 존재하지 않으면 대각선으로 이동할 수 없습니다"); + } + } + + private void checkIsStartPosition(final Position source) { + if ((color == WHITE && source.rank() != TWO) || (color == BLACK && source.rank() != SEVEN)) { + throw new IllegalArgumentException("시작 위치가 아니면 두 칸 이동할 수 없습니다."); + } + } + + private void checkMovePaths( + final Position source, + final PawnMovementDirection movementDirection, + final Board board + ) { + Position current = source; + int moveDistance = Math.abs(movementDirection.getRowDistance()); + for (int i = 0; i < moveDistance; i++) { + current = current.next(movementDirection.convertOneStep()); + checkPathHasPiece(current, board); + } + } + + private void checkPathHasPiece(final Position path, final Board board) { + if (board.existPiece(path)) { + throw new IllegalArgumentException("전진시 기물이 존재하는 경로 혹은 목적지로 이동할 수 없습니다."); + } + } + + @Override + public PieceType pieceType() { + return PIECE_TYPE; + } +} diff --git a/src/main/java/domain/piece/PawnMovementDirection.java b/src/main/java/domain/piece/PawnMovementDirection.java new file mode 100644 index 00000000000..d539720861c --- /dev/null +++ b/src/main/java/domain/piece/PawnMovementDirection.java @@ -0,0 +1,84 @@ +package domain.piece; + +import domain.board.Position; + +import java.util.Arrays; + +import static domain.piece.PieceColor.BLACK; +import static domain.piece.PieceColor.WHITE; + +public enum PawnMovementDirection implements MovementDirection { + UP_ONE_STEP(-1, 0, WHITE), + UP_TWO_STEP(-2, 0, WHITE), + UP_RIGHT(-1, 1, WHITE), + UP_LEFT(-1, -1, WHITE), + DOWN_ONE_STEP(1, 0, BLACK), + DOWN_TWO_STEP(2, 0, BLACK), + DOWN_RIGHT(1, 1, BLACK), + DOWN_LEFT(1, -1, BLACK); + + private final int rowDistance; + private final int columnDistance; + private final PieceColor pieceColor; + + PawnMovementDirection(final int rowDistance, final int columnDistance, final PieceColor pieceColor) { + this.rowDistance = rowDistance; + this.columnDistance = columnDistance; + this.pieceColor = pieceColor; + } + + public static PawnMovementDirection calculateDirection(final PieceColor pieceColor, final Position source, final Position destination) { + final int rowDifference = destination.rowIndex() - source.rowIndex(); + final int columnDifference = destination.columnIndex() - source.columnIndex(); + + return Arrays.stream(values()) + .filter(knightMovementDirection -> knightMovementDirection.matchDistance(pieceColor, rowDifference, columnDifference)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException(pieceColor.name() + "색상의 폰이 이동할 수 없는 방향입니다.")); + } + + private boolean matchDistance(final PieceColor pieceColor, final int rowDistance, final int columnDistance) { + return this.pieceColor == pieceColor + && this.rowDistance == rowDistance + && this.columnDistance == columnDistance; + } + + public boolean isCrossStep() { + return this == UP_RIGHT || this == UP_LEFT + || this == DOWN_RIGHT || this == DOWN_LEFT; + } + + public boolean isTwoStep() { + return this == UP_TWO_STEP || this == DOWN_TWO_STEP; + } + + public PawnMovementDirection convertOneStep() { + if (!this.isTwoStep() && !this.isOneStep()) { + throw new IllegalArgumentException("전진 방향만 변환할 수 있습니다."); + } + + if (this == UP_TWO_STEP) { + return UP_ONE_STEP; + } + + if (this == DOWN_TWO_STEP) { + return DOWN_ONE_STEP; + } + + return this; + } + + private boolean isOneStep() { + return this == UP_ONE_STEP || this == DOWN_ONE_STEP; + } + + @Override + public int getRowDistance() { + return rowDistance; + } + + @Override + public int getColumnDistance() { + return columnDistance; + } +} diff --git a/src/main/java/domain/piece/Piece.java b/src/main/java/domain/piece/Piece.java new file mode 100644 index 00000000000..00babe0fbc8 --- /dev/null +++ b/src/main/java/domain/piece/Piece.java @@ -0,0 +1,24 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +public abstract class Piece { + protected final PieceColor color; + + public Piece(final PieceColor color) { + this.color = color; + } + + public abstract void move(final Position source, final Position destination, final Board board); + + public abstract PieceType pieceType(); + + public boolean isTeam(final PieceColor teamColor) { + return this.color == teamColor; + } + + public PieceColor pieceColor() { + return color; + } +} diff --git a/src/main/java/domain/piece/PieceColor.java b/src/main/java/domain/piece/PieceColor.java new file mode 100644 index 00000000000..d4cb0e67321 --- /dev/null +++ b/src/main/java/domain/piece/PieceColor.java @@ -0,0 +1,13 @@ +package domain.piece; + +public enum PieceColor { + BLACK, WHITE; + + public PieceColor toggle() { + if (this == BLACK) { + return WHITE; + } + + return BLACK; + } +} diff --git a/src/main/java/domain/piece/PieceType.java b/src/main/java/domain/piece/PieceType.java new file mode 100644 index 00000000000..3002a47b4e6 --- /dev/null +++ b/src/main/java/domain/piece/PieceType.java @@ -0,0 +1,10 @@ +package domain.piece; + +public enum PieceType { + PAWN, + ROOK, + KNIGHT, + BISHOP, + QUEEN, + KING +} diff --git a/src/main/java/domain/piece/Queen.java b/src/main/java/domain/piece/Queen.java new file mode 100644 index 00000000000..84af535703b --- /dev/null +++ b/src/main/java/domain/piece/Queen.java @@ -0,0 +1,53 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +import java.util.List; +import java.util.stream.Stream; + +import static domain.piece.CommonMovementDirection.calculateDirection; +import static domain.piece.PieceType.QUEEN; + +public class Queen extends Piece { + private static final PieceType PIECE_TYPE = QUEEN; + + public Queen(final PieceColor color) { + super(color); + } + + @Override + public void move(final Position source, final Position destination, final Board board) { + CommonMovementDirection movementDirection = calculateDirection(source, destination); + + List movePaths = Stream.iterate(source, current -> current.next(movementDirection)) + .takeWhile(current -> current.equals(source) || isContinuable(current, destination, board)) + .toList(); + + Position alivePosition = movePaths.get(movePaths.size() - 1).next(movementDirection); + checkAlivePosition(alivePosition, board); + } + + private boolean isContinuable(final Position current, final Position destination, final Board board) { + if (current.equals(destination)) { + return false; + } + + if (board.existPiece(current)) { + throw new IllegalArgumentException("목적지 경로에 기물이 존재하여 이동할 수 없습니다."); + } + + return true; + } + + private void checkAlivePosition(final Position alivePosition, final Board board) { + if (board.existPiece(alivePosition) && board.existTeamColor(alivePosition, color)) { + throw new IllegalArgumentException("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + } + + @Override + public PieceType pieceType() { + return PIECE_TYPE; + } +} diff --git a/src/main/java/domain/piece/Rook.java b/src/main/java/domain/piece/Rook.java new file mode 100644 index 00000000000..6f5fbbf6e3a --- /dev/null +++ b/src/main/java/domain/piece/Rook.java @@ -0,0 +1,55 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.Position; + +import java.util.List; +import java.util.stream.Stream; + +import static domain.piece.CommonMovementDirection.calculateDirection; +import static domain.piece.PieceType.ROOK; + + +public class Rook extends Piece { + private static final PieceType PIECE_TYPE = ROOK; + + public Rook(final PieceColor color) { + super(color); + } + + @Override + public void move(final Position source, final Position destination, final Board board) { + CommonMovementDirection movementDirection = calculateDirection(source, destination); + movementDirection.checkRookMovableMovement(); + + List movePaths = Stream.iterate(source, current -> current.next(movementDirection)) + .takeWhile(current -> current.equals(source) || isContinuable(current, destination, board)) + .toList(); + + Position alivePosition = movePaths.get(movePaths.size() - 1).next(movementDirection); + checkAlivePosition(alivePosition, board); + } + + private boolean isContinuable(final Position current, final Position destination, final Board board) { + if (current.equals(destination)) { + return false; + } + + if (board.existPiece(current)) { + throw new IllegalArgumentException("목적지 경로에 기물이 존재하여 이동할 수 없습니다."); + } + + return true; + } + + private void checkAlivePosition(final Position alivePosition, final Board board) { + if (board.existPiece(alivePosition) && board.existTeamColor(alivePosition, color)) { + throw new IllegalArgumentException("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + } + + @Override + public PieceType pieceType() { + return PIECE_TYPE; + } +} diff --git a/src/main/java/dto/BoardDto.java b/src/main/java/dto/BoardDto.java new file mode 100644 index 00000000000..c41de62ca33 --- /dev/null +++ b/src/main/java/dto/BoardDto.java @@ -0,0 +1,19 @@ +package dto; + +import domain.board.Position; +import domain.piece.Piece; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public record BoardDto(Map value) { + + public static BoardDto from(Map piecePositions) { + Map value = new HashMap<>(); + piecePositions.forEach(((position, piece) -> + value.put(PositionDto.from(position), PieceDto.from(piece)))); + + return new BoardDto(Collections.unmodifiableMap(value)); + } +} diff --git a/src/main/java/dto/PieceDto.java b/src/main/java/dto/PieceDto.java new file mode 100644 index 00000000000..66a33e05564 --- /dev/null +++ b/src/main/java/dto/PieceDto.java @@ -0,0 +1,12 @@ +package dto; + +import domain.piece.PieceType; +import domain.piece.Piece; +import domain.piece.PieceColor; + +public record PieceDto(PieceType type, PieceColor color) { + + public static PieceDto from(final Piece piece) { + return new PieceDto(piece.pieceType(), piece.pieceColor()); + } +} diff --git a/src/main/java/dto/PositionDto.java b/src/main/java/dto/PositionDto.java new file mode 100644 index 00000000000..6f4eb347c09 --- /dev/null +++ b/src/main/java/dto/PositionDto.java @@ -0,0 +1,14 @@ +package dto; + +import domain.board.Position; + +public record PositionDto(int row, int column) { + + public static PositionDto from(final Position position) { + return new PositionDto(position.rowIndex(), position.columnIndex()); + } + + public static PositionDto emptyPosition() { + return new PositionDto(0, 0); + } +} diff --git a/src/main/java/othercase/End.java b/src/main/java/othercase/End.java new file mode 100644 index 00000000000..b4e6cb85eaa --- /dev/null +++ b/src/main/java/othercase/End.java @@ -0,0 +1,9 @@ +package othercase; + +public class End implements GameCommand { + + @Override + public void execute(final GameController gameController) { + gameController.endGame(); + } +} diff --git a/src/main/java/othercase/GameCommand.java b/src/main/java/othercase/GameCommand.java new file mode 100644 index 00000000000..3572688467c --- /dev/null +++ b/src/main/java/othercase/GameCommand.java @@ -0,0 +1,5 @@ +package othercase; + +public interface GameCommand { + void execute(GameController gameController); +} diff --git a/src/main/java/othercase/GameCommandType.java b/src/main/java/othercase/GameCommandType.java new file mode 100644 index 00000000000..3a0fef8b272 --- /dev/null +++ b/src/main/java/othercase/GameCommandType.java @@ -0,0 +1,48 @@ +package othercase; + +import domain.board.Position; + +import java.util.Arrays; +import java.util.function.Function; + +public enum GameCommandType { + START("start", command -> new Start()), + MOVE("move", GameCommandType::toMove), + END("end", command -> new End()); + + private static final int COMMAND_INDEX = 0; + private static final int BEFORE_POSITION = 1; + private static final int AFTER_POSITION = 2; + private static final int COMMAND_WITH_POSITIONS_SIZE = 3; + + private final String value; + private final Function gameCommand; + + GameCommandType(final String value, final Function gameCommand) { + this.value = value; + this.gameCommand = gameCommand; + } + + public static GameCommand of(final String[] values) { + return Arrays.stream(GameCommandType.values()) + .filter(it -> it.value.equalsIgnoreCase(values[COMMAND_INDEX])) + .map(it -> it.gameCommand.apply(values)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 명령어입니다!")); + } + + private static Move toMove(final String[] values) { + validatePositionValue(values); + + Position source = Position.of(values[BEFORE_POSITION]); + Position destination = Position.of(values[AFTER_POSITION]); + + return new Move(source, destination); + } + + private static void validatePositionValue(final String[] values) { + if (values.length != COMMAND_WITH_POSITIONS_SIZE) { + throw new IllegalArgumentException("잘못된 출발지/도착지입니다."); + } + } +} diff --git a/src/main/java/othercase/GameController.java b/src/main/java/othercase/GameController.java new file mode 100644 index 00000000000..3fe6d1070d9 --- /dev/null +++ b/src/main/java/othercase/GameController.java @@ -0,0 +1,80 @@ +package othercase; + +import domain.board.Position; +import domain.game.ChessGame; +import dto.BoardDto; +import view.InputView; +import view.OutputView; + +public class GameController { + private final InputView inputView; + private final OutputView outputView; + private final ChessGame chessGame; + + public GameController(final InputView inputView, final OutputView outputView, final ChessGame chessGame) { + this.inputView = inputView; + this.outputView = outputView; + this.chessGame = chessGame; + } + + public void run() { + try { + GameCommand gameCommand = inputCommand(); + gameCommand.execute(this); + } catch (Exception e) { + outputView.printErrorMessage(e.getMessage()); + run(); + } + } + + private GameCommand inputCommand() { + try { + String[] inputValues = inputView.inputCommand().split(" "); + return GameCommandType.of(inputValues); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + return inputCommand(); + } + } + + public void gameStart() { + if (isGameRunning()) { + throw new IllegalArgumentException("이미 게임이 진행중입니다."); + } + + chessGame.gameStart(); + outputView.printWelcomeMessage(); + + while (chessGame.isGameRunning()) { + BoardDto boardDto = BoardDto.from(chessGame.piecePositions()); + outputView.printBoard(boardDto); + playTurn(); + } + } + + private boolean isGameRunning() { + return chessGame.isGameRunning(); + } + + private void playTurn() { + try { + GameCommand gameCommand = inputCommand(); + gameCommand.execute(this); + } catch (Exception e) { + outputView.printErrorMessage(e.getMessage()); + playTurn(); + } + } + + public void movePiece(final Position source, final Position destination) { + if (!chessGame.isGameRunning()) { + throw new IllegalArgumentException("게임이 시작되지 않았습니다."); + } + + chessGame.movePiece(source, destination); + } + + public void endGame() { + chessGame.gameEnd(); + } +} diff --git a/src/main/java/othercase/Move.java b/src/main/java/othercase/Move.java new file mode 100644 index 00000000000..44c565bf99d --- /dev/null +++ b/src/main/java/othercase/Move.java @@ -0,0 +1,18 @@ +package othercase; + +import domain.board.Position; + +public class Move implements GameCommand { + private final Position source; + private final Position destination; + + public Move(final Position source, final Position destination) { + this.source = source; + this.destination = destination; + } + + @Override + public void execute(final GameController gameController) { + gameController.movePiece(source, destination); + } +} diff --git a/src/main/java/othercase/Start.java b/src/main/java/othercase/Start.java new file mode 100644 index 00000000000..47de1c23a48 --- /dev/null +++ b/src/main/java/othercase/Start.java @@ -0,0 +1,9 @@ +package othercase; + +public class Start implements GameCommand { + + @Override + public void execute(final GameController gameController) { + gameController.gameStart(); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000000..5ff300cb63c --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,20 @@ +package view; + +import java.util.Scanner; + +public class InputView { + private final Scanner sc = new Scanner(System.in); + + public String inputCommand() { + String input = sc.nextLine(); + validateEmpty(input); + + return input; + } + + private void validateEmpty(final String input) { + if (input == null || input.isBlank()) { + throw new IllegalArgumentException("공백을 입력할 수 없습니다."); + } + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000000..5d3c3ab8436 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,79 @@ +package view; + +import domain.piece.PieceColor; +import domain.piece.PieceType; +import dto.BoardDto; +import dto.PieceDto; +import dto.PositionDto; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static domain.piece.PieceType.PAWN; +import static domain.piece.PieceType.ROOK; +import static domain.piece.PieceType.KNIGHT; +import static domain.piece.PieceType.BISHOP; +import static domain.piece.PieceType.QUEEN; +import static domain.piece.PieceType.KING; + +public class OutputView { + private static final Map pieceFormat = Map.ofEntries( + Map.entry(ROOK, "R"), + Map.entry(KNIGHT, "N"), + Map.entry(BISHOP, "B"), + Map.entry(QUEEN, "Q"), + Map.entry(KING, "K"), + Map.entry(PAWN, "P") + ); + + public void printErrorMessage(final String message) { + System.out.println(message); + System.out.println(); + } + + public void printWelcomeMessage() { + System.out.println("> 체스 게임을 시작합니다."); + System.out.println("> 게임 시작: start"); + System.out.println("> 게임 종료: end"); + System.out.println("> 게임 이동: move source위치 target위치 - 예. move b2 b3"); + } + + public void printBoard(final BoardDto boardDto) { + List boardStatus = convertBoardStatus(boardDto.value()); + boardStatus.forEach(System.out::println); + System.out.println(); + } + + private List convertBoardStatus(final Map boardStatus) { + String[][] boardMessage = initEmptyBoardMessage(); + boardStatus.forEach((position, piece) -> addPieceStatusInBoardMessage(boardMessage, position, piece)); + + return Arrays.stream(boardMessage) + .map(rowString -> String.join("", rowString)) + .toList(); + } + + private static String[][] initEmptyBoardMessage() { + String[][] strings = new String[8][8]; + for (String[] row : strings) { + Arrays.fill(row, "."); + } + + return strings; + } + + private void addPieceStatusInBoardMessage(final String[][] boardMessage, final PositionDto position, final PieceDto piece) { + boardMessage[position.row()][position.column()] = convertPieceTypeToString(piece.type(), piece.color()); + } + + private String convertPieceTypeToString(final PieceType pieceType, final PieceColor pieceColor) { + String pieceMessage = pieceFormat.get(pieceType); + + if (pieceColor == PieceColor.WHITE) { + return pieceMessage.toLowerCase(); + } + + return pieceMessage; + } +} diff --git a/src/test/java/domain/board/BoardTest.java b/src/test/java/domain/board/BoardTest.java new file mode 100644 index 00000000000..3afc8f424ab --- /dev/null +++ b/src/test/java/domain/board/BoardTest.java @@ -0,0 +1,107 @@ +package domain.board; + +import domain.piece.Pawn; +import domain.piece.Piece; +import domain.piece.Rook; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static domain.board.File.*; +import static domain.board.Rank.*; +import static domain.piece.PieceColor.BLACK; +import static domain.piece.PieceColor.WHITE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class BoardTest { + + @DisplayName("기물을 이동시킨다.") + @Test + void movePieceTest() { + // Given + Position source = position(B, TWO); + Position destination = position(B, FOUR); + Map piecePositions = new HashMap<>(Map.of(source, new Pawn(WHITE))); + Board board = new Board(piecePositions); + + // When + board.movePiece(WHITE, source, destination); + boolean existsPieceOnSource = piecePositions.containsKey(source); + boolean existsPieceOnDestination = piecePositions.containsKey(destination); + + // Then + assertThat(existsPieceOnSource).isFalse(); + assertThat(existsPieceOnDestination).isTrue(); + } + + @DisplayName("목적지에 적 기물이 존재하면 적 기물을 제거하고 이동시킨다.") + @Test + void movePieceWithRemoveEnemyPieceTest() { + // Given + Position source = position(B, TWO); + Position destination = position(B, FOUR); + Rook myPiece = new Rook(WHITE); + Pawn enemyPiece = new Pawn(BLACK); + Map piecePositions = new HashMap<>(Map.of(source, myPiece, destination, enemyPiece)); + Board board = new Board(piecePositions); + + // When + board.movePiece(WHITE, source, destination); + boolean existsPieceOnSource = piecePositions.containsKey(source); + Piece destinationPiece = piecePositions.get(destination); + + // Then + assertThat(existsPieceOnSource).isFalse(); + assertThat(destinationPiece).isNotEqualTo(enemyPiece); + assertThat(destinationPiece).isEqualTo(myPiece); + } + + @DisplayName("입력된 출발지와 목적지의 위치가 같으면 예외를 발생시킨다.") + @Test + void throwExceptionWhenSourceEqualsDestinationTest() { + // Given + Board board = BoardInitializer.initBoard(); + Position source = new Position(B, TWO); + Position destination = new Position(B, TWO); + + // When & Then + assertThatThrownBy(() -> board.movePiece(WHITE, source, destination)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("출발지와 목적지가 같을 수 없습니다."); + } + + @DisplayName("기물이 존재하지 않는 출발지 위치가 입력되면 예외를 발생시킨다.") + @Test + void throwExceptionWhenNotExistPieceSourceTest() { + // Given + Board board = BoardInitializer.initBoard(); + Position source = new Position(D, FIVE); + Position destination = new Position(B, TWO); + + // When & Then + assertThatThrownBy(() -> board.movePiece(WHITE, source, destination)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("출발지에 기물이 존재하지 않습니다."); + } + + @DisplayName("상대방의 기물을 이동시키려고 하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenMoveEnemyPieceTest() { + // Given + Board board = BoardInitializer.initBoard(); + Position source = new Position(B, SEVEN); + Position destination = new Position(B, SIX); + + // When & Then + assertThatThrownBy(() -> board.movePiece(WHITE, source, destination)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("상대방의 기물을 이동시킬 수 없습니다."); + } + + private Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/test/java/domain/piece/BishopTest.java b/src/test/java/domain/piece/BishopTest.java new file mode 100644 index 00000000000..eaa0094bc04 --- /dev/null +++ b/src/test/java/domain/piece/BishopTest.java @@ -0,0 +1,110 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class BishopTest { + + @DisplayName("주어진 출발지 -> 도착지를 Bishop이 이동할 수 있는지 검증한다.") + @MethodSource("checkMovableTestCase") + @ParameterizedTest + void checkMovableTest(final Position source, final Position destination) { + // Given + Bishop bishop = new Bishop(PieceColor.WHITE); + Map piecePositions = Map.of(position(File.C, Rank.ONE), new Rook(PieceColor.BLACK)); + Board board = new Board(piecePositions); + + // When & Then + assertThatCode(() -> bishop.move(source, destination, board)) + .doesNotThrowAnyException(); + } + + private static Stream checkMovableTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.E, Rank.FIVE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.THREE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.ONE)), + Arguments.of(position(File.B, Rank.TWO), position(File.C, Rank.ONE)) + ); + } + + @DisplayName("Bishop이 이동할 수 없는 방향의 도착지가 입력되면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenInvalidDirectionTestCase") + @ParameterizedTest + void throwExceptionWhenInvalidDirectionTest(final Position source, final Position destination) { + // Given + Bishop bishop = new Bishop(PieceColor.WHITE); + Map piecePositions = Collections.emptyMap(); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() ->bishop.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("비숍이 이동할 수 있는 방향이 아닙니다."); + } + + private static Stream throwExceptionWhenInvalidDirectionTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.FIVE)), + Arguments.of(position(File.B, Rank.TWO), position(File.G, Rank.TWO)) + ); + } + + @DisplayName("이동 경로에 기물이 존재하면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenPathsHasPieceTestCase") + @ParameterizedTest + void throwExceptionWhenPathsHasPieceTest(final Position source, final Position destination) { + // Given + Bishop bishop = new Bishop(PieceColor.WHITE); + Map piecePositions = Map.of( + position(File.C, Rank.THREE), new Bishop(PieceColor.BLACK) + ); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() -> bishop.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("목적지 경로에 기물이 존재하여 이동할 수 없습니다."); + } + + private static Stream throwExceptionWhenPathsHasPieceTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.E, Rank.FIVE)), + Arguments.of(position(File.D, Rank.FOUR), position(File.B, Rank.TWO)) + ); + } + + @DisplayName("도착지에 아군 기물이 존재하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenDestinationHasTeamPieceTest() { + // Given + Position source = position(File.B, Rank.TWO); + Position destination = position(File.D, Rank.FOUR); + Bishop bishop = new Bishop(PieceColor.WHITE); + Map piecePositions = Map.of(destination, new Bishop(PieceColor.WHITE)); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() -> bishop.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/test/java/domain/piece/KingTest.java b/src/test/java/domain/piece/KingTest.java new file mode 100644 index 00000000000..654784165c2 --- /dev/null +++ b/src/test/java/domain/piece/KingTest.java @@ -0,0 +1,84 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class KingTest { + + @DisplayName("주어진 출발지 -> 도착지를 Rook이 이동할 수 있는지 검증한다.") + @MethodSource("checkMovableTestCase") + @ParameterizedTest + void checkMovableTest(final Position source, final Position destination) { + // Given + King king = new King(PieceColor.WHITE); + Map piecePositions = Map.of(position(File.D, Rank.TWO), new Rook(PieceColor.BLACK)); + Board board = new Board(piecePositions); + + // When & Then + assertThatCode(() -> king.move(source, destination, board)) + .doesNotThrowAnyException(); + } + + private static Stream checkMovableTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.THREE)), + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.ONE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.TWO)), + Arguments.of(position(File.B, Rank.TWO), position(File.C, Rank.TWO)), + Arguments.of(position(File.B, Rank.TWO), position(File.C, Rank.THREE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.THREE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.ONE)), + Arguments.of(position(File.B, Rank.TWO), position(File.C, Rank.ONE)) + ); + } + + @DisplayName("출발지에서 목적지까지 거리가 두 칸 이상이면 예외를 발생시킨다.") + @Test + void throwExceptionWhenDistanceOverOrEqualTwo() { + // Given + Position source = position(File.B, Rank.THREE); + Position destination = position(File.B, Rank.SIX); + King king = new King(PieceColor.WHITE); + Map piecePositions = Collections.emptyMap(); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() -> king.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 거리입니다."); + } + + @DisplayName("도착지에 아군 기물이 존재하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenDestinationHasTeamPieceTest() { + // Given + Position source = position(File.B, Rank.TWO); + Position destination = position(File.B, Rank.THREE); + King king = new King(PieceColor.WHITE); + Map piecePositions = Map.of(destination, new Rook(PieceColor.WHITE)); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() -> king.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/test/java/domain/piece/KnightTest.java b/src/test/java/domain/piece/KnightTest.java new file mode 100644 index 00000000000..0b5bd219f08 --- /dev/null +++ b/src/test/java/domain/piece/KnightTest.java @@ -0,0 +1,31 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class KnightTest { + + @DisplayName("도착지에 아군 기물이 존재하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenDestinationHasTeamPieceTest() { + // Given + Position source = new Position(File.C, Rank.FOUR); + Position destination = new Position(File.D, Rank.SIX); + Knight knight = new Knight(PieceColor.WHITE); + Map piecePositions = Map.of(destination, new Rook(PieceColor.WHITE)); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() -> knight.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } +} diff --git a/src/test/java/domain/piece/PawnTest.java b/src/test/java/domain/piece/PawnTest.java new file mode 100644 index 00000000000..cb10e4144bc --- /dev/null +++ b/src/test/java/domain/piece/PawnTest.java @@ -0,0 +1,132 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; + +import static domain.piece.PieceColor.BLACK; +import static domain.piece.PieceColor.WHITE; +import static domain.board.File.*; +import static domain.piece.PawnMovementDirection.*; +import static domain.board.Rank.*; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PawnTest { + + @DisplayName("주어진 출발지 -> 도착지를 Pawn이 이동할 수 있는지 검증한다.") + @MethodSource("checkMovableTestCase") + @ParameterizedTest + void checkMovableTest(final PieceColor pieceColor, final PieceColor enemyColor, final Position source, final Position destination) { + // Given + Pawn pawn = new Pawn(pieceColor); + Map piecePositions = Map.of( + position(E, SIX), new Rook(enemyColor), + position(C, SIX), new Rook(enemyColor), + position(E, FOUR), new Rook(enemyColor), + position(C, FOUR), new Rook(enemyColor) + ); + Board board = new Board(piecePositions); + + // When & Then + assertThatCode(() -> pawn.move(source, destination, board)) + .doesNotThrowAnyException(); + } + + private static Stream checkMovableTestCase() { + return Stream.of( + Arguments.of(WHITE, BLACK, position(F, TWO), position(F, FOUR), UP_TWO_STEP), + Arguments.of(WHITE, BLACK, position(C, FOUR), position(C, FIVE), UP_ONE_STEP), + Arguments.of(WHITE, BLACK, position(D, FIVE), position(E, SIX), UP_RIGHT), + Arguments.of(WHITE, BLACK, position(D, FIVE), position(C, SIX), UP_LEFT), + Arguments.of(BLACK, WHITE, position(G, SEVEN), position(G, FIVE), DOWN_TWO_STEP), + Arguments.of(BLACK, WHITE, position(C, FOUR), position(C, THREE), DOWN_ONE_STEP), + Arguments.of(BLACK, WHITE, position(D, FIVE), position(E, FOUR), DOWN_RIGHT), + Arguments.of(BLACK, WHITE, position(D, FIVE), position(C, FOUR), DOWN_LEFT) + ); + } + + @DisplayName("대각석으로 이동할 경우 목적지에 적 기물이 존재하지 않으면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenCrossStepDestinationNotHasEnemyTestCase") + @ParameterizedTest + void throwExceptionWhenCrossStepDestinationNotHasEnemyTest(final PieceColor pieceColor, final Position source, final Position destination) { + // Given + Pawn pawn = new Pawn(pieceColor); + Map piecePositions = Collections.emptyMap(); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() -> pawn.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("적 기물이 존재하지 않으면 대각선으로 이동할 수 없습니다"); + } + + private static Stream throwExceptionWhenCrossStepDestinationNotHasEnemyTestCase() { + return Stream.of( + Arguments.of(WHITE, position(D, FIVE), position(E, SIX), UP_RIGHT), + Arguments.of(WHITE, position(D, FIVE), position(C, SIX), UP_LEFT), + Arguments.of(BLACK, position(D, FIVE), position(E, FOUR), DOWN_RIGHT), + Arguments.of(BLACK, position(D, FIVE), position(C, FOUR), DOWN_LEFT) + ); + } + + @DisplayName("두 칸 전진할 경우 출발지가 폰의 시작 위치가 아니면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenOneStepSourceIsNotStartPositionTestCase") + @ParameterizedTest + void throwExceptionWhenOneStepSourceIsNotStartPositionTest(final PieceColor pieceColor, final Position source, final Position destination) { + // Given + Pawn pawn = new Pawn(pieceColor); + Map piecePositions = Collections.emptyMap(); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() -> pawn.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시작 위치가 아니면 두 칸 이동할 수 없습니다."); + } + + private static Stream throwExceptionWhenOneStepSourceIsNotStartPositionTestCase() { + return Stream.of( + Arguments.of(WHITE, position(C, THREE), position(C, FIVE)), + Arguments.of(BLACK, position(C, FIVE), position(C, THREE)) + ); + } + + @DisplayName("폰이 전진할 시 경로에 기물이 존재하면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenForwardPathHasPieceTestCase") + @ParameterizedTest + void throwExceptionWhenForwardPathHasPieceTest(final PieceColor pieceColor, final Position source, final Position destination) { + // Given + Pawn pawn = new Pawn(pieceColor); + Map piecePositions = Map.of( + position(D, THREE), new Rook(WHITE), + position(C, THREE), new Rook(BLACK) + ); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() -> pawn.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("전진시 기물이 존재하는 경로 혹은 목적지로 이동할 수 없습니다."); + } + + private static Stream throwExceptionWhenForwardPathHasPieceTestCase() { + return Stream.of( + Arguments.of(WHITE, position(D, TWO), position(D, FOUR)), + Arguments.of(BLACK, position(C, FOUR), position(C, THREE)) + ); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/test/java/domain/piece/QueenTest.java b/src/test/java/domain/piece/QueenTest.java new file mode 100644 index 00000000000..2d9c3f23116 --- /dev/null +++ b/src/test/java/domain/piece/QueenTest.java @@ -0,0 +1,92 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class QueenTest { + + @DisplayName("주어진 출발지 -> 도착지를 Rook이 이동할 수 있는지 검증한다.") + @MethodSource("checkMovableTestCase") + @ParameterizedTest + void checkMovableTest(final Position source, final Position destination) { + // Given + Queen queen = new Queen(PieceColor.WHITE); + Map piecePositions = Map.of(position(File.D, Rank.TWO), new Rook(PieceColor.BLACK)); + Board board = new Board(piecePositions); + + // When & Then + assertThatCode(() -> queen.move(source, destination, board)) + .doesNotThrowAnyException(); + } + + private static Stream checkMovableTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.SIX)), + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.ONE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.TWO)), + Arguments.of(position(File.B, Rank.TWO), position(File.D, Rank.TWO)), + Arguments.of(position(File.B, Rank.TWO), position(File.E, Rank.FIVE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.THREE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.ONE)), + Arguments.of(position(File.B, Rank.TWO), position(File.C, Rank.ONE)) + ); + } + + @DisplayName("이동 경로에 기물이 존재하면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenPathsHasPieceTestCase") + @ParameterizedTest + void throwExceptionWhenPathsHasPieceTest(final Position source, final Position destination) { + // Given + Queen queen = new Queen(PieceColor.WHITE); + Map piecePositions = Map.of( + position(File.B, Rank.FOUR), new Bishop(PieceColor.BLACK), + position(File.D, Rank.TWO), new Rook(PieceColor.WHITE) + ); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() -> queen.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("목적지 경로에 기물이 존재하여 이동할 수 없습니다."); + } + + private static Stream throwExceptionWhenPathsHasPieceTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.SIX)), + Arguments.of(position(File.B, Rank.TWO), position(File.G, Rank.TWO)) + ); + } + + @DisplayName("도착지에 아군 기물이 존재하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenDestinationHasTeamPieceTest() { + // Given + Position source = position(File.B, Rank.TWO); + Position destination = position(File.B, Rank.SIX); + Queen queen = new Queen(PieceColor.WHITE); + Map piecePositions = Map.of(destination, new Rook(PieceColor.WHITE)); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() -> queen.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/test/java/domain/piece/RookTest.java b/src/test/java/domain/piece/RookTest.java new file mode 100644 index 00000000000..cd934d4523b --- /dev/null +++ b/src/test/java/domain/piece/RookTest.java @@ -0,0 +1,111 @@ +package domain.piece; + +import domain.board.Board; +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RookTest { + + @DisplayName("주어진 출발지 -> 도착지를 Rook이 이동할 수 있는지 검증한다.") + @MethodSource("checkMovableTestCase") + @ParameterizedTest + void checkMovableTest(final Position source, final Position destination) { + // Given + Rook rook = new Rook(PieceColor.WHITE); + Map piecePositions = Map.of(position(File.D, Rank.TWO), new Rook(PieceColor.BLACK)); + Board board = new Board(piecePositions); + + // When & Then + assertThatCode(() -> rook.move(source, destination, board)) + .doesNotThrowAnyException(); + } + + private static Stream checkMovableTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.SIX)), + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.ONE)), + Arguments.of(position(File.B, Rank.TWO), position(File.A, Rank.TWO)), + Arguments.of(position(File.B, Rank.TWO), position(File.D, Rank.TWO)) + ); + } + + @DisplayName("Rook이 이동할 수 없는 방향의 도착지가 입력되면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenInvalidDirectionTestCase") + @ParameterizedTest + void throwExceptionWhenInvalidDirectionTest(final Position source, final Position destination) { + // Given + Rook rook = new Rook(PieceColor.WHITE); + Map piecePositions = Collections.emptyMap(); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() ->rook.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("룩이 이동할 수 있는 방향이 아닙니다."); + } + + private static Stream throwExceptionWhenInvalidDirectionTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.E, Rank.FIVE)), + Arguments.of(position(File.B, Rank.TWO), position(File.G, Rank.SEVEN)) + ); + } + + @DisplayName("이동 경로에 기물이 존재하면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenPathsHasPieceTestCase") + @ParameterizedTest + void throwExceptionWhenPathsHasPieceTest(final Position source, final Position destination) { + // Given + Rook rook = new Rook(PieceColor.WHITE); + Map piecePositions = Map.of( + position(File.B, Rank.FOUR), new Rook(PieceColor.BLACK), + position(File.D, Rank.TWO), new Rook(PieceColor.WHITE) + ); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() -> rook.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("목적지 경로에 기물이 존재하여 이동할 수 없습니다."); + } + + private static Stream throwExceptionWhenPathsHasPieceTestCase() { + return Stream.of( + Arguments.of(position(File.B, Rank.TWO), position(File.B, Rank.SIX)), + Arguments.of(position(File.B, Rank.TWO), position(File.G, Rank.TWO)) + ); + } + + @DisplayName("도착지에 아군 기물이 존재하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenDestinationHasTeamPieceTest() { + // Given + Position source = position(File.B, Rank.TWO); + Position destination = position(File.B, Rank.SIX); + Rook rook = new Rook(PieceColor.WHITE); + Map piecePositions = Map.of(destination, new Rook(PieceColor.WHITE)); + Board board = new Board(piecePositions); + + // When & Then + assertThatThrownBy(() -> rook.move(source, destination, board)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("아군 기물이 위치한 칸으로는 이동할 수 없습니다."); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/test/java/domain/position/CommonMovementDirectionTest.java b/src/test/java/domain/position/CommonMovementDirectionTest.java new file mode 100644 index 00000000000..5b3adb309cd --- /dev/null +++ b/src/test/java/domain/position/CommonMovementDirectionTest.java @@ -0,0 +1,67 @@ +package domain.position; + +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.piece.CommonMovementDirection; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static domain.board.File.*; +import static domain.piece.CommonMovementDirection.*; +import static domain.board.Rank.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CommonMovementDirectionTest { + + @DisplayName("입력받은 출발지/목적지의 거리를 바탕으로 이동방향을 반환한다.") + @MethodSource("findMovementDirectionTestCase") + @ParameterizedTest + void findMovementDirection(final Position source, final Position destination, final CommonMovementDirection expect) { + // When + CommonMovementDirection commonMovementDirection = CommonMovementDirection.calculateDirection(source, destination); + + // Then + assertThat(commonMovementDirection).isEqualTo(expect); + } + + private static Stream findMovementDirectionTestCase() { + return Stream.of( + Arguments.of(position(B, TWO), position(B, SIX), UP), + Arguments.of(position(B, FIVE), position(B, TWO), DOWN), + Arguments.of(position(B, TWO), position(G, TWO), RIGHT), + Arguments.of(position(E, FIVE), position(B, FIVE), LEFT), + Arguments.of(position(C, TWO), position(E, FOUR), UP_RIGHT), + Arguments.of(position(F, TWO), position(C, FIVE), UP_LEFT), + Arguments.of(position(D, FOUR), position(F, TWO), DOWN_RIGHT), + Arguments.of(position(E, FOUR), position(C, TWO), DOWN_LEFT) + ); + } + + @DisplayName("방향 계산이 불가능한 출발지/목적지 위치 정보가 입력되면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenInvalidSourceAndDestinationTestCase") + @ParameterizedTest + void throwExceptionWhenInvalidSourceAndDestination(final Position source, final Position destination) { + // When & THen + assertThatThrownBy(() -> CommonMovementDirection.calculateDirection(source, destination)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("상/하/좌/우 혹은 대각선으로 이동할 수 없는 칸입니다."); + } + + private static Stream throwExceptionWhenInvalidSourceAndDestinationTestCase() { + return Stream.of( + Arguments.of(position(B, TWO), position(B, TWO)), + Arguments.of(position(B, ONE), position(C, FOUR)), + Arguments.of(position(A, EIGHT), position(G, ONE)) + ); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/test/java/domain/position/FileTest.java b/src/test/java/domain/position/FileTest.java new file mode 100644 index 00000000000..56b8f21370f --- /dev/null +++ b/src/test/java/domain/position/FileTest.java @@ -0,0 +1,36 @@ +package domain.position; + +import domain.board.File; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class FileTest { + + @DisplayName("인덱스를 입력하면 해당하는 File을 반환한다.") + @Test + void getFileTest() { + // Given + final int index = 2; + + // When + File file = File.of(index); + + // Then + assertThat(file).isEqualTo(File.C); + } + + @DisplayName("유효하지 않은 인덱스를 입력하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenInputInvalidIndex() { + // Given + final int index = 13; + + // When & Then + assertThatThrownBy(() -> File.of(index)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("유효하지 않은 인덱스입니다."); + } +} diff --git a/src/test/java/domain/position/KnightMovementDirectionTest.java b/src/test/java/domain/position/KnightMovementDirectionTest.java new file mode 100644 index 00000000000..c6586bf5697 --- /dev/null +++ b/src/test/java/domain/position/KnightMovementDirectionTest.java @@ -0,0 +1,75 @@ +package domain.position; + +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.piece.KnightMovementDirection; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static domain.board.File.*; +import static domain.piece.KnightMovementDirection.*; +import static domain.board.Rank.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class KnightMovementDirectionTest { + + @DisplayName("입력된 출발지 & 목적지를 계산해서 나이트의 이동 방향을 반환한다.") + @MethodSource("calculateDirectionTestCase") + @ParameterizedTest + void calculateDirectionTest(final Position source, final Position destination, final KnightMovementDirection expect) { + // When + KnightMovementDirection knightMovementDirection = calculateDirection(source, destination); + + // Then + assertThat(knightMovementDirection).isEqualTo(expect); + } + + private static Stream calculateDirectionTestCase() { + return Stream.of( + Arguments.of(position(C, FOUR), position(D, SIX), UP_RIGHT), + Arguments.of(position(C, FOUR), position(B, SIX), UP_LEFT), + Arguments.of(position(D, FIVE), position(E, THREE), DOWN_RIGHT), + Arguments.of(position(D, FIVE), position(C, THREE), DOWN_LEFT), + Arguments.of(position(C, FOUR), position(E, FIVE), RIGHT_UP), + Arguments.of(position(C, FOUR), position(E, THREE), RIGHT_DOWN), + Arguments.of(position(E, FOUR), position(C, FIVE), LEFT_UP), + Arguments.of(position(E, FOUR), position(C, THREE), LEFT_DOWN) + ); + } + + @DisplayName("나이트가 이동할 수 없는 출발지/도착지 위치 정보가 입력되면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenInvalidLocationTestCase") + @ParameterizedTest + void throwExceptionWhenInvalidLocationTest(final Position source, final Position destination) { + // When & Then + assertThatThrownBy(() -> KnightMovementDirection.calculateDirection(source, destination)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("나이트가 이동할 수 없는 방향입니다."); + } + + private static Stream throwExceptionWhenInvalidLocationTestCase() { + return Stream.of( + Arguments.of(position(B, TWO), position(B, SIX)), + Arguments.of(position(B, FIVE), position(B, TWO)), + Arguments.of(position(B, TWO), position(G, TWO)), + Arguments.of(position(E, FIVE), position(B, FIVE)), + Arguments.of(position(C, TWO), position(E, FOUR)), + Arguments.of(position(F, TWO), position(C, FIVE)), + Arguments.of(position(D, FOUR), position(F, TWO)), + Arguments.of(position(E, FOUR), position(C, TWO)), + Arguments.of(position(B, TWO), position(B, TWO)), + Arguments.of(position(B, ONE), position(C, FOUR)), + Arguments.of(position(A, EIGHT), position(G, ONE)) + ); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/test/java/domain/position/PawnMovementDirectionTest.java b/src/test/java/domain/position/PawnMovementDirectionTest.java new file mode 100644 index 00000000000..b9d8893ddcb --- /dev/null +++ b/src/test/java/domain/position/PawnMovementDirectionTest.java @@ -0,0 +1,81 @@ +package domain.position; + +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.piece.PawnMovementDirection; +import domain.piece.PieceColor; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static domain.piece.PieceColor.*; +import static domain.board.File.*; +import static domain.piece.PawnMovementDirection.*; +import static domain.board.Rank.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PawnMovementDirectionTest { + + @DisplayName("입력된 출발지 & 목적지를 계산해서 폰의 이동 방향을 반환한다.") + @MethodSource("calculateDirectionTestCase") + @ParameterizedTest + void calculateDirectionTest(final PieceColor pieceColor, final Position source, final Position destination, final PawnMovementDirection expect) { + // When + PawnMovementDirection movementDirection = calculateDirection(pieceColor, source, destination); + + // Then + assertThat(movementDirection).isEqualTo(expect); + } + + private static Stream calculateDirectionTestCase() { + return Stream.of( + Arguments.of(WHITE, position(C, TWO), position(C, FOUR), UP_TWO_STEP), + Arguments.of(WHITE, position(C, FOUR), position(C, FIVE), UP_ONE_STEP), + Arguments.of(WHITE, position(D, FIVE), position(E, SIX), UP_RIGHT), + Arguments.of(WHITE, position(D, FIVE), position(C, SIX), UP_LEFT), + Arguments.of(BLACK, position(C, SEVEN), position(C, FIVE), DOWN_TWO_STEP), + Arguments.of(BLACK, position(C, FOUR), position(C, THREE), DOWN_ONE_STEP), + Arguments.of(BLACK, position(D, FIVE), position(E, FOUR), DOWN_RIGHT), + Arguments.of(BLACK, position(D, FIVE), position(C, FOUR), DOWN_LEFT) + ); + } + + @DisplayName("색상별 방향을 계산할 수 없는 춮발지 & 목적지가 입력되면 예외를 발생시킨다.") + @MethodSource("throwExceptionWhenInputInvalidPositionTestCase") + @ParameterizedTest + void throwExceptionWhenInputInvalidPositionTest(final PieceColor pieceColor, final Position source, final Position destination) { + // When & Then + assertThatThrownBy(() -> PawnMovementDirection.calculateDirection(pieceColor, source, destination)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(pieceColor.name() + "색상의 폰이 이동할 수 없는 방향입니다."); + } + + private static Stream throwExceptionWhenInputInvalidPositionTestCase() { + return Stream.of( + Arguments.of(BLACK, position(C, TWO), position(C, FOUR)), + Arguments.of(BLACK, position(C, FOUR), position(C, FIVE)), + Arguments.of(BLACK, position(D, FIVE), position(E, SIX)), + Arguments.of(BLACK, position(D, FIVE), position(C, SIX)), + Arguments.of(WHITE, position(C, SEVEN), position(C, FIVE)), + Arguments.of(WHITE, position(C, FOUR), position(C, THREE)), + Arguments.of(WHITE, position(D, FIVE), position(E, FOUR)), + Arguments.of(WHITE, position(D, FIVE), position(C, FOUR)), + Arguments.of(WHITE, position(B, TWO), position(B, SIX)), + Arguments.of(WHITE, position(B, TWO), position(B, ONE)), + Arguments.of(WHITE, position(B, TWO), position(A, TWO)), + Arguments.of(WHITE, position(B, TWO), position(C, ONE)), + Arguments.of(BLACK, position(B, TWO), position(D, TWO)), + Arguments.of(BLACK, position(B, TWO), position(E, FIVE)), + Arguments.of(BLACK, position(B, TWO), position(A, THREE)) + ); + } + + private static Position position(final File file, final Rank rank) { + return new Position(file, rank); + } +} diff --git a/src/test/java/domain/position/PositionTest.java b/src/test/java/domain/position/PositionTest.java new file mode 100644 index 00000000000..121870d3032 --- /dev/null +++ b/src/test/java/domain/position/PositionTest.java @@ -0,0 +1,41 @@ +package domain.position; + +import domain.board.File; +import domain.board.Position; +import domain.board.Rank; +import domain.piece.CommonMovementDirection; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PositionTest { + @DisplayName("행과 열 정보를 가진 Position 인스턴스를 생성한다.") + @Test + void createPositionTest() { + // Given + File file = File.A; + Rank rank = Rank.ONE; + + // When + Position position = new Position(file, rank); + + // Then + assertThat(position.rank()).isEqualTo(rank); + assertThat(position.file()).isEqualTo(file); + } + + @DisplayName("이동 방향을 전달하면 새로운 위치의 Position을 반환한다.") + @Test + void addPositionTest() { + // Given + Position position = new Position(File.D, Rank.TWO); + + // When + Position newPosition = position.next(CommonMovementDirection.UP_RIGHT); + + // Then + assertThat(newPosition.file()).isEqualTo(File.E); + assertThat(newPosition.rank()).isEqualTo(Rank.THREE); + } +} diff --git a/src/test/java/domain/position/RankTest.java b/src/test/java/domain/position/RankTest.java new file mode 100644 index 00000000000..6411e9401c9 --- /dev/null +++ b/src/test/java/domain/position/RankTest.java @@ -0,0 +1,36 @@ +package domain.position; + +import domain.board.Rank; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RankTest { + + @DisplayName("인덱스를 입력하면 해당하는 Rank를 반환한다.") + @Test + void getFileTest() { + // Given + final int index = 2; + + // When + Rank file = Rank.of(index); + + // Then + assertThat(file).isEqualTo(Rank.SIX); + } + + @DisplayName("유효하지 않은 인덱스를 입력하면 예외를 발생시킨다.") + @Test + void throwExceptionWhenInputInvalidIndex() { + // Given + final int index = 13; + + // When & Then + assertThatThrownBy(() -> Rank.of(index)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("유효하지 않은 인덱스입니다."); + } +}