diff --git a/README.md b/README.md index 8102f91c870..98b471a3d77 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,46 @@ ## 우아한테크코스 코드리뷰 - [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) + + +# 체스 +체스 기물의 움직임을 구현한다. + +## view +- [x] 체스 판을 출력할 수 있다. + +## domain +### 체스 판 +- [x] 행은 1~8, 열은 a~h로 표현된다.(8x8) +- [x] 체스 판을 초기화할 수 있다. +- [x] 특정 위치의 말을 원하는 위치로 움직일 수 있다. + +### 체스 기물 + +- [x] 출발지와 도착지를 기반으로, 각 기물이 해당 경로로 갈 수 있는지 스스로 확인할 수 있다. +- [x] 경로에 기물이 존재하는 경우 움직일 수 없다.(knight 제외) +- [x] 도착지에 상대 기물이 존재하는 경우 잡을 수 있다. +- [x] 본인의 색깔이 무엇인지 알고 있다. +- [x] 본인이 잡혔는지 알고 있다. + +#### king +- [x] 상, 하, 좌, 우, 대각선으로 1칸 이동할 수 있다. +- [x] 잡히면 게임이 종료된다. + +#### bishop +- [x] 대각선으로 여러 칸 이동할 수 있다. + +#### knight +- [x] 상, 하, 좌, 우로 1칸 이동 후 대각선으로 1칸 이동할 수 있다. +- [x] 다른 기물을 뛰어넘을 수 있다. + +#### Pawn +- [x] 앞으로 1칸 이동할 수 있다. +- [x] 초기 위치에서는 앞으로 2칸까지 이동할 수 있다. +- [x] 대각선 한 칸 앞에 위치한 기물을 잡을 수 있다. + +#### Queen +- [x] 수직, 수평, 대각선으로 여러 칸 이동할 수 있다. + +#### Rook +- [x] 수직, 수평으로 여러 칸 이동할 수 있다. diff --git a/src/main/java/chess/Application.java b/src/main/java/chess/Application.java new file mode 100644 index 00000000000..0ffb0857a58 --- /dev/null +++ b/src/main/java/chess/Application.java @@ -0,0 +1,14 @@ +package chess; + +import chess.controller.ChessController; +import chess.view.InputView; +import chess.view.OutputView; + +public class Application { + + public static void main(String[] args) { + + ChessController chessController = new ChessController(new InputView(), new OutputView()); + chessController.run(); + } +} diff --git a/src/main/java/chess/controller/ChessController.java b/src/main/java/chess/controller/ChessController.java new file mode 100644 index 00000000000..cd929ec5283 --- /dev/null +++ b/src/main/java/chess/controller/ChessController.java @@ -0,0 +1,55 @@ +package chess.controller; + +import chess.domain.board.ChessBoard; +import chess.domain.Color; +import chess.domain.Position; +import chess.domain.board.ChessBoardBasicInitializer; +import chess.view.InputView; +import chess.view.OutputView; +import java.util.List; + +public class ChessController { + + private final InputView inputView; + private final OutputView outputView; + + public ChessController(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void run() { + ChessBoard chessBoard = new ChessBoard(new ChessBoardBasicInitializer()); + Color nowTurn = Color.WHITE; + + while (true) { + outputView.printChessBoard(chessBoard); + outputView.printTurnMessage(nowTurn); + + processChessTurn(chessBoard, nowTurn); + if (chessBoard.checkOppositeKingCaptured(nowTurn)) { + break; + } + nowTurn = nowTurn.opposite(); + } + + outputView.printWinningMessage(nowTurn); + } + + private void processChessTurn(ChessBoard chessBoard, Color nowTurn) { + InputProcessor.processUntilSuccess(() -> { + List originAndDestination = inputView.getMoveInput(); + Position origin = originAndDestination.get(0); + checkValidPieceColor(chessBoard, nowTurn, origin); + Position destination = originAndDestination.get(1); + + chessBoard.moveAndCapturePiece(origin, destination); + }, OutputView::printErrorMessage); + } + + private void checkValidPieceColor(ChessBoard chessBoard, Color nowTurn, Position origin) { + if (chessBoard.getPieceOfPosition(origin).getColor() != nowTurn) { + throw new IllegalArgumentException("현재는 %s의 차례입니다.".formatted(nowTurn.name())); + } + } +} diff --git a/src/main/java/chess/controller/InputProcessor.java b/src/main/java/chess/controller/InputProcessor.java new file mode 100644 index 00000000000..1640aa54cb0 --- /dev/null +++ b/src/main/java/chess/controller/InputProcessor.java @@ -0,0 +1,38 @@ +package chess.controller; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class InputProcessor { + + public static void processUntilSuccess(Runnable runnable, Consumer printer) { + while(true) { + try { + runnable.run(); + return; + } catch(Exception e) { + printer.accept(e.getMessage()); + } + } + } + + public static void processUntilSuccess(Consumer consumer, Consumer printer, T t) { + while(true) { + try { + consumer.accept(t); + } catch(Exception e) { + printer.accept(e.getMessage()); + } + } + } + + public static T processUntilSuccess(Supplier supplier, Consumer printer) { + while(true) { + try { + return supplier.get(); + } catch(Exception e) { + printer.accept(e.getMessage()); + } + } + } +} diff --git a/src/main/java/chess/Color.java b/src/main/java/chess/domain/Color.java similarity index 75% rename from src/main/java/chess/Color.java rename to src/main/java/chess/domain/Color.java index 55cd020b681..553be44b7d9 100644 --- a/src/main/java/chess/Color.java +++ b/src/main/java/chess/domain/Color.java @@ -1,4 +1,6 @@ -package chess; +package chess.domain; + +import java.util.List; public enum Color { @@ -25,4 +27,8 @@ public Color opposite() { default -> EMPTY; }; } + + public static List getGameColors() { + return List.of(WHITE, BLACK); + } } diff --git a/src/main/java/chess/Column.java b/src/main/java/chess/domain/Column.java similarity index 92% rename from src/main/java/chess/Column.java rename to src/main/java/chess/domain/Column.java index b64b4dc77a3..63efc8e8d1e 100644 --- a/src/main/java/chess/Column.java +++ b/src/main/java/chess/domain/Column.java @@ -1,4 +1,4 @@ -package chess; +package chess.domain; public enum Column { @@ -15,6 +15,7 @@ public boolean isFarLeft() { return ordinal() == 0; } + public boolean isFarRight() { return ordinal() + 1 == values().length; } @@ -32,7 +33,7 @@ public Column moveLeft(final int step) { return values()[ordinal() - step]; } - throw new IllegalStateException("움직일 수 없는 위치입니다."); + throw new IllegalStateException("움직일 수 없는 위치입니다. column left"); } public boolean canMoveRight(final int step) { @@ -48,6 +49,6 @@ public Column moveRight(final int step) { return values()[ordinal() + step]; } - throw new IllegalStateException("움직일 수 없는 위치입니다."); + throw new IllegalStateException("움직일 수 없는 위치입니다. column right"); } } diff --git a/src/main/java/chess/Movement.java b/src/main/java/chess/domain/Movement.java similarity index 89% rename from src/main/java/chess/Movement.java rename to src/main/java/chess/domain/Movement.java index e57c6e91bb9..818676ef475 100644 --- a/src/main/java/chess/Movement.java +++ b/src/main/java/chess/domain/Movement.java @@ -1,4 +1,4 @@ -package chess; +package chess.domain; public enum Movement { UP(0, 1), @@ -45,4 +45,8 @@ public boolean isVertical() { public boolean isDiagonal() { return x != 0 && y != 0 && Math.abs(x) == Math.abs(y); } + + public boolean isTwoTimeVerticalMove() { + return this == UP_UP || this == DOWN_DOWN; + } } diff --git a/src/main/java/chess/Position.java b/src/main/java/chess/domain/Position.java similarity index 95% rename from src/main/java/chess/Position.java rename to src/main/java/chess/domain/Position.java index 3ebeb0ea185..ef91e35086a 100644 --- a/src/main/java/chess/Position.java +++ b/src/main/java/chess/domain/Position.java @@ -1,4 +1,4 @@ -package chess; +package chess.domain; public record Position( Column column, @@ -167,4 +167,12 @@ public Position moveHorizontal(final int step) { } return this; } + + @Override + public String toString() { + return "Position{" + + "column=" + column + + ", row=" + row + + '}'; + } } diff --git a/src/main/java/chess/Row.java b/src/main/java/chess/domain/Row.java similarity index 92% rename from src/main/java/chess/Row.java rename to src/main/java/chess/domain/Row.java index 126ed048daa..a056c6f047e 100644 --- a/src/main/java/chess/Row.java +++ b/src/main/java/chess/domain/Row.java @@ -1,4 +1,4 @@ -package chess; +package chess.domain; public enum Row { @@ -32,7 +32,7 @@ public Row moveUp(final int step) { return values()[ordinal() - step]; } - throw new IllegalStateException("움직일 수 없는 위치입니다."); + throw new IllegalStateException("움직일 수 없는 위치입니다. row up"); } public boolean canMoveDown(final int step) { @@ -48,6 +48,6 @@ public Row moveDown(final int step) { return values()[ordinal() + step]; } - throw new IllegalStateException("움직일 수 없는 위치입니다."); + throw new IllegalStateException("움직일 수 없는 위치입니다. row down"); } } diff --git a/src/main/java/chess/domain/board/ChessBoard.java b/src/main/java/chess/domain/board/ChessBoard.java new file mode 100644 index 00000000000..30acc542a97 --- /dev/null +++ b/src/main/java/chess/domain/board/ChessBoard.java @@ -0,0 +1,70 @@ +package chess.domain.board; + +import chess.domain.Color; +import chess.domain.Movement; +import chess.domain.Position; +import chess.domain.piece.limited_moving_chess_piece.BlackPawn; +import chess.domain.piece.ChessPiece; +import chess.domain.piece.limited_moving_chess_piece.King; +import chess.domain.piece.limited_moving_chess_piece.None; +import chess.domain.piece.limited_moving_chess_piece.Pawn; +import chess.domain.piece.limited_moving_chess_piece.WhitePawn; +import java.util.List; +import java.util.Map; + +public class ChessBoard { + + private final Map board; + private final ChessPiece blackKing; + private final ChessPiece whiteKing; + + public ChessBoard(ChessBoardInitializer initializer) { + blackKing = new King(Color.BLACK); + whiteKing = new King(Color.WHITE); + board = initializer.initialize(blackKing, whiteKing); + } + + public ChessPiece getPieceOfPosition(Position position) { + return board.get(position); + } + + public void moveAndCapturePiece(Position origin, Position destination) { + ChessPiece movePiece = getPieceOfPosition(origin); + List route = movePiece.findRoute(origin, destination); + boolean isExistHurdleOnRoute = checkHurdleExistOnRouteWithoutDestination(origin, route); + ChessPiece targetPiece = getPieceOfPosition(destination); + movePiece.validateCanMove(route, isExistHurdleOnRoute, targetPiece); + + if (movePiece.getClass().equals(BlackPawn.class) || movePiece.getClass().equals(WhitePawn.class)) { + ((Pawn) movePiece).isMoved(); + } + + targetPiece.capture(); + board.put(origin, new None()); + board.put(destination, movePiece); + } + + private boolean checkHurdleExistOnRouteWithoutDestination(Position origin, List route) { + List routeWithoutDestination = route.subList(0, route.size() - 1); + Position origin2 = origin; + for (Movement movement : routeWithoutDestination) { + if (origin2.canMove(movement)) { + origin2 = origin2.move(movement); + if (!getPieceOfPosition(origin2).isEmpty()) { + return true; + } + } + } + return false; + } + + public boolean checkOppositeKingCaptured(Color color) { + if (color == Color.BLACK) { + return whiteKing.isCaptured(); + } + if (color == Color.WHITE) { + return blackKing.isCaptured(); + } + return false; + } +} diff --git a/src/main/java/chess/domain/board/ChessBoardBasicInitializer.java b/src/main/java/chess/domain/board/ChessBoardBasicInitializer.java new file mode 100644 index 00000000000..fd30e501174 --- /dev/null +++ b/src/main/java/chess/domain/board/ChessBoardBasicInitializer.java @@ -0,0 +1,68 @@ +package chess.domain.board; + +import chess.domain.Color; +import chess.domain.Column; +import chess.domain.Position; +import chess.domain.Row; +import chess.domain.piece.ChessPiece; +import chess.domain.piece.limited_moving_chess_piece.BlackPawn; +import chess.domain.piece.limited_moving_chess_piece.Knight; +import chess.domain.piece.limited_moving_chess_piece.None; +import chess.domain.piece.limited_moving_chess_piece.WhitePawn; +import chess.domain.piece.linear_moving_chess_piece.Bishop; +import chess.domain.piece.linear_moving_chess_piece.Queen; +import chess.domain.piece.linear_moving_chess_piece.Rook; +import java.util.HashMap; +import java.util.Map; + +public class ChessBoardBasicInitializer implements ChessBoardInitializer { + + @Override + public Map initialize(ChessPiece blackKing, ChessPiece whiteKing) { + Map board = new HashMap<>(); + + for (Row row : Row.values()) { + for (Column column : Column.values()) { + board.put(new Position(row, column), new None()); + } + } + + board.put(new Position(Row.EIGHT, Column.A), new Rook(Color.BLACK)); + board.put(new Position(Row.EIGHT, Column.B), new Knight(Color.BLACK)); + board.put(new Position(Row.EIGHT, Column.C), new Bishop(Color.BLACK)); + board.put(new Position(Row.EIGHT, Column.D), new Queen(Color.BLACK)); + board.put(new Position(Row.EIGHT, Column.E), blackKing); + board.put(new Position(Row.EIGHT, Column.F), new Bishop(Color.BLACK)); + board.put(new Position(Row.EIGHT, Column.G), new Knight(Color.BLACK)); + board.put(new Position(Row.EIGHT, Column.H), new Rook(Color.BLACK)); + + board.put(new Position(Row.SEVEN, Column.A), new BlackPawn()); + board.put(new Position(Row.SEVEN, Column.B), new BlackPawn()); + board.put(new Position(Row.SEVEN, Column.C), new BlackPawn()); + board.put(new Position(Row.SEVEN, Column.D), new BlackPawn()); + board.put(new Position(Row.SEVEN, Column.E), new BlackPawn()); + board.put(new Position(Row.SEVEN, Column.F), new BlackPawn()); + board.put(new Position(Row.SEVEN, Column.G), new BlackPawn()); + board.put(new Position(Row.SEVEN, Column.H), new BlackPawn()); + + board.put(new Position(Row.ONE, Column.A), new Rook(Color.WHITE)); + board.put(new Position(Row.ONE, Column.B), new Knight(Color.WHITE)); + board.put(new Position(Row.ONE, Column.C), new Bishop(Color.WHITE)); + board.put(new Position(Row.ONE, Column.D), new Queen(Color.WHITE)); + board.put(new Position(Row.ONE, Column.E), whiteKing); + board.put(new Position(Row.ONE, Column.F), new Bishop(Color.WHITE)); + board.put(new Position(Row.ONE, Column.G), new Knight(Color.WHITE)); + board.put(new Position(Row.ONE, Column.H), new Rook(Color.WHITE)); + + board.put(new Position(Row.TWO, Column.A), new WhitePawn()); + board.put(new Position(Row.TWO, Column.B), new WhitePawn()); + board.put(new Position(Row.TWO, Column.C), new WhitePawn()); + board.put(new Position(Row.TWO, Column.D), new WhitePawn()); + board.put(new Position(Row.TWO, Column.E), new WhitePawn()); + board.put(new Position(Row.TWO, Column.F), new WhitePawn()); + board.put(new Position(Row.TWO, Column.G), new WhitePawn()); + board.put(new Position(Row.TWO, Column.H), new WhitePawn()); + + return board; + } +} diff --git a/src/main/java/chess/domain/board/ChessBoardInitializer.java b/src/main/java/chess/domain/board/ChessBoardInitializer.java new file mode 100644 index 00000000000..1765611b5e3 --- /dev/null +++ b/src/main/java/chess/domain/board/ChessBoardInitializer.java @@ -0,0 +1,10 @@ +package chess.domain.board; + +import chess.domain.Position; +import chess.domain.piece.ChessPiece; +import java.util.Map; + +public interface ChessBoardInitializer { + + Map initialize(ChessPiece blackKing, ChessPiece whiteKing); +} diff --git a/src/main/java/chess/domain/piece/ChessPiece.java b/src/main/java/chess/domain/piece/ChessPiece.java new file mode 100644 index 00000000000..cce0b24a784 --- /dev/null +++ b/src/main/java/chess/domain/piece/ChessPiece.java @@ -0,0 +1,19 @@ +package chess.domain.piece; + +import chess.domain.Color; +import chess.domain.Movement; +import chess.domain.Position; +import java.util.List; + +public interface ChessPiece { + + String name(); + void validateCanMove(List route, boolean isExistHurdleOnRoute, ChessPiece targetPiece); + List findRoute(Position origin, Position destination); + default boolean isEmpty() { + return false; + } + Color getColor(); + void capture(); + boolean isCaptured(); +} diff --git a/src/main/java/chess/domain/piece/limited_moving_chess_piece/BlackPawn.java b/src/main/java/chess/domain/piece/limited_moving_chess_piece/BlackPawn.java new file mode 100644 index 00000000000..56e92d48955 --- /dev/null +++ b/src/main/java/chess/domain/piece/limited_moving_chess_piece/BlackPawn.java @@ -0,0 +1,14 @@ +package chess.domain.piece.limited_moving_chess_piece; + +import chess.domain.Color; +import chess.domain.Movement; +import java.util.List; + +public class BlackPawn extends Pawn { + + private static final List ROUTES = List.of(Movement.DOWN_DOWN, Movement.DOWN, Movement.LEFT_DOWN, Movement.RIGHT_DOWN); + + public BlackPawn() { + super(ROUTES, Color.BLACK); + } +} diff --git a/src/main/java/chess/domain/piece/limited_moving_chess_piece/King.java b/src/main/java/chess/domain/piece/limited_moving_chess_piece/King.java new file mode 100644 index 00000000000..9063798d901 --- /dev/null +++ b/src/main/java/chess/domain/piece/limited_moving_chess_piece/King.java @@ -0,0 +1,22 @@ +package chess.domain.piece.limited_moving_chess_piece; + +import chess.domain.Color; +import chess.domain.Movement; +import java.util.List; + +public class King extends LimitedMovingChessPiece { + + private static final List ROUTES = List.of( + Movement.LEFT, Movement.UP, Movement.RIGHT, Movement.DOWN, + Movement.LEFT_DOWN, Movement.LEFT_UP, Movement.RIGHT_DOWN, Movement.RIGHT_UP + ); + + public King(Color side) { + super(ROUTES, side); + } + + @Override + public String name() { + return "K"; + } +} diff --git a/src/main/java/chess/domain/piece/limited_moving_chess_piece/Knight.java b/src/main/java/chess/domain/piece/limited_moving_chess_piece/Knight.java new file mode 100644 index 00000000000..a4d322c6b93 --- /dev/null +++ b/src/main/java/chess/domain/piece/limited_moving_chess_piece/Knight.java @@ -0,0 +1,32 @@ +package chess.domain.piece.limited_moving_chess_piece; + +import chess.domain.Color; +import chess.domain.Movement; +import chess.domain.piece.ChessPiece; +import java.util.List; + +public class Knight extends LimitedMovingChessPiece { + + private static final List ROUTES = List.of( + Movement.DOWN_DOWN_LEFT, Movement.DOWN_DOWN_RIGHT, + Movement.RIGHT_RIGHT_DOWN, Movement.RIGHT_RIGHT_UP, + Movement.UP_UP_LEFT, Movement.UP_UP_RIGHT, + Movement.LEFT_LEFT_DOWN, Movement.LEFT_LEFT_UP + ); + + public Knight(Color side) { + super(ROUTES, side); + } + + @Override + public void validateCanMove(List route, boolean isExistHurdleOnRoute, ChessPiece targetPiece) { + if (this.getColor() == targetPiece.getColor()) { + throw new IllegalStateException("도착지에 같은 팀의 말이 존재하기 때문에 이동할 수 없습니다."); + } + } + + @Override + public String name() { + return "N"; + } +} diff --git a/src/main/java/chess/domain/piece/limited_moving_chess_piece/LimitedMovingChessPiece.java b/src/main/java/chess/domain/piece/limited_moving_chess_piece/LimitedMovingChessPiece.java new file mode 100644 index 00000000000..a4bae6d82e3 --- /dev/null +++ b/src/main/java/chess/domain/piece/limited_moving_chess_piece/LimitedMovingChessPiece.java @@ -0,0 +1,55 @@ +package chess.domain.piece.limited_moving_chess_piece; + +import chess.domain.Color; +import chess.domain.Movement; +import chess.domain.Position; +import chess.domain.piece.ChessPiece; +import java.util.List; + +public abstract class LimitedMovingChessPiece implements ChessPiece { + + protected final List movements; + protected final Color side; + protected boolean isCaptured; + + public LimitedMovingChessPiece(List movements, Color side) { + this.movements = movements; + this.side = side; + this.isCaptured = false; + } + + @Override + public void validateCanMove(List route, boolean isExistHurdleOnRoute, ChessPiece targetPiece) { + if (isExistHurdleOnRoute) { + throw new IllegalStateException("경로에 장애물이 존재하기 때문에 이동할 수 없습니다."); + } + if (this.getColor() == targetPiece.getColor()) { + throw new IllegalStateException("도착지에 같은 팀의 말이 존재하기 때문에 이동할 수 없습니다."); + } + } + + @Override + public List findRoute(Position origin, Position destination) { + for (Movement movement : movements) { + if (origin.canMove(movement) && origin.move(movement).equals(destination)) { + return List.of(movement); + } + } + throw new IllegalStateException("해당 기물은 해당 경로로 이동할 수 없습니다."); + } + + @Override + public Color getColor() { + return side; + } + + @Override + public void capture() { + this.isCaptured = true; + } + + @Override + public boolean isCaptured() { + return isCaptured; + } +} diff --git a/src/main/java/chess/domain/piece/limited_moving_chess_piece/None.java b/src/main/java/chess/domain/piece/limited_moving_chess_piece/None.java new file mode 100644 index 00000000000..8e938e6d9c6 --- /dev/null +++ b/src/main/java/chess/domain/piece/limited_moving_chess_piece/None.java @@ -0,0 +1,51 @@ +package chess.domain.piece.limited_moving_chess_piece; + +import chess.domain.Color; +import chess.domain.Movement; +import chess.domain.Position; +import chess.domain.piece.ChessPiece; +import java.util.List; + +public class None implements ChessPiece { + + protected final Color side; + + public None() { + this.side = Color.EMPTY; + } + + @Override + public List findRoute(Position origin, Position destination) { + throw new IllegalStateException("움직일 말이 존재하지 않습니다."); + } + + @Override + public String name() { + return "-"; + } + + @Override + public void validateCanMove(List route, boolean isExistHurdleOnRoute, ChessPiece targetPiece) { + throw new IllegalStateException("움직일 말이 존재하지 않습니다."); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public Color getColor() { + return side; + } + + @Override + public void capture() { + + } + + @Override + public boolean isCaptured() { + return false; + } +} diff --git a/src/main/java/chess/domain/piece/limited_moving_chess_piece/Pawn.java b/src/main/java/chess/domain/piece/limited_moving_chess_piece/Pawn.java new file mode 100644 index 00000000000..7498236b518 --- /dev/null +++ b/src/main/java/chess/domain/piece/limited_moving_chess_piece/Pawn.java @@ -0,0 +1,51 @@ +package chess.domain.piece.limited_moving_chess_piece; + +import chess.domain.Color; +import chess.domain.Movement; +import chess.domain.Position; +import chess.domain.piece.ChessPiece; +import java.util.List; + +public abstract class Pawn extends LimitedMovingChessPiece { + + protected boolean isFirstMove; + + public Pawn(List movements, Color side) { + super(movements, side); + this.isFirstMove = true; + } + + @Override + public void validateCanMove(List route, boolean isExistHurdleOnRoute, ChessPiece targetPiece) { + if (isExistHurdleOnRoute) { + throw new IllegalStateException("경로에 장애물이 존재하기 때문에 이동할 수 없습니다."); + } + if (route.getFirst().isDiagonal() && (side.opposite() != targetPiece.getColor())) { + throw new IllegalStateException("대각선 위치에 상대 말이 없기 때문에 움직일 수 없습니다."); + } + if (!route.getFirst().isDiagonal() && !targetPiece.isEmpty()) { + throw new IllegalStateException("도착지에 말이 존재하기 때문에 움직일 수 없습니다."); + } + } + + @Override + public String name() { + return "P"; + } + + public List findRoute(Position origin, Position destination) { + for (Movement movement : movements) { + if (origin.canMove(movement) && origin.move(movement).equals(destination)) { + if (movement.isTwoTimeVerticalMove() && !isFirstMove) { + throw new IllegalStateException("폰은 처음 움직일 때만 두 칸 움직일 수 있습니다."); + } + return List.of(movement); + } + } + throw new IllegalStateException("해당 말은 해당 위치로 움직일 수 없습니다."); + } + + public void isMoved() { + this.isFirstMove = false; + } +} diff --git a/src/main/java/chess/domain/piece/limited_moving_chess_piece/WhitePawn.java b/src/main/java/chess/domain/piece/limited_moving_chess_piece/WhitePawn.java new file mode 100644 index 00000000000..8e17f2cdf73 --- /dev/null +++ b/src/main/java/chess/domain/piece/limited_moving_chess_piece/WhitePawn.java @@ -0,0 +1,14 @@ +package chess.domain.piece.limited_moving_chess_piece; + +import chess.domain.Color; +import chess.domain.Movement; +import java.util.List; + +public class WhitePawn extends Pawn { + + private static final List ROUTES = List.of(Movement.UP_UP, Movement.UP, Movement.LEFT_UP, Movement.RIGHT_UP); + + public WhitePawn() { + super(ROUTES, Color.WHITE); + } +} diff --git a/src/main/java/chess/domain/piece/linear_moving_chess_piece/Bishop.java b/src/main/java/chess/domain/piece/linear_moving_chess_piece/Bishop.java new file mode 100644 index 00000000000..633803ec63a --- /dev/null +++ b/src/main/java/chess/domain/piece/linear_moving_chess_piece/Bishop.java @@ -0,0 +1,19 @@ +package chess.domain.piece.linear_moving_chess_piece; + +import chess.domain.Color; +import chess.domain.Movement; +import java.util.List; + +public class Bishop extends LinearMovingChessPiece { + + private static final List DIRECTIONS = List.of(Movement.LEFT_DOWN, Movement.LEFT_UP, Movement.RIGHT_DOWN, Movement.RIGHT_UP); + + public Bishop(Color side) { + super(DIRECTIONS, side); + } + + @Override + public String name() { + return "B"; + } +} diff --git a/src/main/java/chess/domain/piece/linear_moving_chess_piece/LinearMovingChessPiece.java b/src/main/java/chess/domain/piece/linear_moving_chess_piece/LinearMovingChessPiece.java new file mode 100644 index 00000000000..6096a35c356 --- /dev/null +++ b/src/main/java/chess/domain/piece/linear_moving_chess_piece/LinearMovingChessPiece.java @@ -0,0 +1,62 @@ +package chess.domain.piece.linear_moving_chess_piece; + +import chess.domain.Color; +import chess.domain.Movement; +import chess.domain.Position; +import chess.domain.piece.ChessPiece; +import java.util.Collections; +import java.util.List; + +public abstract class LinearMovingChessPiece implements ChessPiece { + + protected final List directions; + protected final Color side; + protected boolean isCaptured; + + public LinearMovingChessPiece(List directions, Color side) { + this.directions = directions; + this.side = side; + this.isCaptured = false; + } + + @Override + public void validateCanMove(List route, boolean isExistHurdleOnRoute, ChessPiece targetPiece) { + if (isExistHurdleOnRoute) { + throw new IllegalStateException("경로에 장애물이 존재하기 때문에 이동할 수 없습니다."); + } + if (this.getColor() == targetPiece.getColor()) { + throw new IllegalStateException("도착지에 같은 팀의 말이 존재하기 때문에 이동할 수 없습니다."); + } + } + + @Override + public List findRoute(Position origin, Position destination) { + for (Movement direction : directions) { + Position origin2 = origin; + int i = 0; + while (origin2.canMove(direction)) { + origin2 = origin2.move(direction); + i++; + if (origin2.equals(destination)) { + return Collections.nCopies(i, direction); + } + } + } + throw new IllegalStateException("해당 기물은 해당 경로로 이동할 수 없습니다."); + } + + @Override + public Color getColor() { + return side; + } + + @Override + public void capture() { + this.isCaptured = true; + } + + @Override + public boolean isCaptured() { + return isCaptured; + } +} diff --git a/src/main/java/chess/domain/piece/linear_moving_chess_piece/Queen.java b/src/main/java/chess/domain/piece/linear_moving_chess_piece/Queen.java new file mode 100644 index 00000000000..8e2c7dcdec4 --- /dev/null +++ b/src/main/java/chess/domain/piece/linear_moving_chess_piece/Queen.java @@ -0,0 +1,22 @@ +package chess.domain.piece.linear_moving_chess_piece; + +import chess.domain.Color; +import chess.domain.Movement; +import java.util.List; + +public class Queen extends LinearMovingChessPiece { + + private static final List DIRECTIONS = List.of( + Movement.UP, Movement.DOWN, Movement.LEFT, Movement.RIGHT, + Movement.RIGHT_DOWN, Movement.LEFT_DOWN, Movement.RIGHT_UP, Movement.LEFT_UP + ); + + public Queen(Color side) { + super(DIRECTIONS, side); + } + + @Override + public String name() { + return "Q"; + } +} diff --git a/src/main/java/chess/domain/piece/linear_moving_chess_piece/Rook.java b/src/main/java/chess/domain/piece/linear_moving_chess_piece/Rook.java new file mode 100644 index 00000000000..bd119366641 --- /dev/null +++ b/src/main/java/chess/domain/piece/linear_moving_chess_piece/Rook.java @@ -0,0 +1,21 @@ +package chess.domain.piece.linear_moving_chess_piece; + +import chess.domain.Color; +import chess.domain.Movement; +import java.util.List; + +public class Rook extends LinearMovingChessPiece { + + private static final List DIRECTIONS = List.of( + Movement.UP, Movement.DOWN, Movement.LEFT, Movement.RIGHT + ); + + public Rook(Color side) { + super(DIRECTIONS, side); + } + + @Override + public String name() { + return "R"; + } +} diff --git a/src/main/java/chess/piece/Bishop.java b/src/main/java/chess/piece/Bishop.java deleted file mode 100644 index b14ab70f981..00000000000 --- a/src/main/java/chess/piece/Bishop.java +++ /dev/null @@ -1,5 +0,0 @@ -package chess.piece; - -public class Bishop { - -} diff --git a/src/main/java/chess/piece/King.java b/src/main/java/chess/piece/King.java deleted file mode 100644 index d64210cad13..00000000000 --- a/src/main/java/chess/piece/King.java +++ /dev/null @@ -1,5 +0,0 @@ -package chess.piece; - -public class King { - -} diff --git a/src/main/java/chess/piece/Knight.java b/src/main/java/chess/piece/Knight.java deleted file mode 100644 index 2ee7c47a3bc..00000000000 --- a/src/main/java/chess/piece/Knight.java +++ /dev/null @@ -1,5 +0,0 @@ -package chess.piece; - -public class Knight { - -} diff --git a/src/main/java/chess/piece/Pawn.java b/src/main/java/chess/piece/Pawn.java deleted file mode 100644 index c8b6cafa51e..00000000000 --- a/src/main/java/chess/piece/Pawn.java +++ /dev/null @@ -1,5 +0,0 @@ -package chess.piece; - -public class Pawn { - -} diff --git a/src/main/java/chess/piece/Queen.java b/src/main/java/chess/piece/Queen.java deleted file mode 100644 index 9b547261c4b..00000000000 --- a/src/main/java/chess/piece/Queen.java +++ /dev/null @@ -1,5 +0,0 @@ -package chess.piece; - -public class Queen { - -} diff --git a/src/main/java/chess/piece/Rook.java b/src/main/java/chess/piece/Rook.java deleted file mode 100644 index 7ed4d08bf03..00000000000 --- a/src/main/java/chess/piece/Rook.java +++ /dev/null @@ -1,5 +0,0 @@ -package chess.piece; - -public class Rook { - -} diff --git a/src/main/java/chess/view/InputView.java b/src/main/java/chess/view/InputView.java new file mode 100644 index 00000000000..0c2aa7826fb --- /dev/null +++ b/src/main/java/chess/view/InputView.java @@ -0,0 +1,86 @@ +package chess.view; + +import chess.domain.Column; +import chess.domain.Position; +import chess.domain.Row; +import java.util.List; +import java.util.Scanner; + +public class InputView { + + private static final Scanner scanner = new Scanner(System.in); + private static final String LINE_SEPARATOR = System.lineSeparator(); + + public List getMoveInput() { + System.out.println(LINE_SEPARATOR + "움직일 말의 위치와 도착지를 입력하세요.(ex. a1 b2)"); + + try { + String positions = scanner.nextLine(); + Position origin = parseToPosition(String.valueOf(positions.charAt(0)), String.valueOf(positions.charAt(1))); + Position destination = parseToPosition(String.valueOf(positions.charAt(3)), String.valueOf(positions.charAt(4))); + return List.of(origin, destination); + } catch (Exception e) { + throw new IllegalArgumentException("올바르지 않은 위치 입력입니다."); + } + } + + private Position parseToPosition(String column, String row) { + return new Position(parseToRow(row), parseToColumn(column)); + } + + private Column parseToColumn(String columnRaw) { + if (columnRaw.equals("a")) { + return Column.A; + } + if (columnRaw.equals("b")) { + return Column.B; + } + if (columnRaw.equals("c")) { + return Column.C; + } + if (columnRaw.equals("d")) { + return Column.D; + } + if (columnRaw.equals("e")) { + return Column.E; + } + if (columnRaw.equals("f")) { + return Column.F; + } + if (columnRaw.equals("g")) { + return Column.G; + } + if (columnRaw.equals("h")) { + return Column.H; + } + throw new IllegalArgumentException("존재하지 않는 열입니다."); + } + + private Row parseToRow(String rowRaw) { + if (rowRaw.equals("1")) { + return Row.ONE; + } + if (rowRaw.equals("2")) { + return Row.TWO; + } + if (rowRaw.equals("3")) { + return Row.THREE; + } + if (rowRaw.equals("4")) { + return Row.FOUR; + } + if (rowRaw.equals("5")) { + return Row.FIVE; + } + if (rowRaw.equals("6")) { + return Row.SIX; + } + if (rowRaw.equals("7")) { + return Row.SEVEN; + } + if (rowRaw.equals("8")) { + return Row.EIGHT; + } + throw new IllegalArgumentException("존재하지 않는 행입니다."); + } +} diff --git a/src/main/java/chess/view/OutputView.java b/src/main/java/chess/view/OutputView.java new file mode 100644 index 00000000000..704ed872063 --- /dev/null +++ b/src/main/java/chess/view/OutputView.java @@ -0,0 +1,108 @@ +package chess.view; + +import chess.domain.board.ChessBoard; +import chess.domain.Color; +import chess.domain.Column; +import chess.domain.Position; +import chess.domain.Row; +import chess.domain.piece.ChessPiece; + +public class OutputView { + + private static final String LINE_SEPARATOR = System.lineSeparator(); + + public void printChessBoard(ChessBoard chessBoard) { + System.out.print(" "); + for (Column column : Column.values()) { + System.out.print(parseColumn(column) + " "); + } + + System.out.println(); + for (Row row : Row.values()) { + System.out.print(parseRow(row) + " "); + for (Column column : Column.values()) { + ChessPiece piece = chessBoard.getPieceOfPosition(new Position(row, column)); + System.out.print(colorMessage(piece.name(), piece.getColor()) + " "); + } + System.out.println(); + } + } + + public void printTurnMessage(Color color) { + System.out.println(LINE_SEPARATOR + colorMessage(color.name(), color) + "의 차례입니다."); + } + + public void printWinningMessage(Color winner) { + System.out.println(LINE_SEPARATOR + colorMessage(winner.name(), winner) + "의 승리입니다!"); + } + + public static void printErrorMessage(String message) { + System.out.println(message); + } + + private String parseColumn(Column column) { + if (column == Column.A) { + return "a"; + } + if (column == Column.B) { + return "b"; + } + if (column == Column.C) { + return "c"; + } + if (column == Column.D) { + return "d"; + } + if (column == Column.E) { + return "e"; + } + if (column == Column.F) { + return "f"; + } + if (column == Column.G) { + return "g"; + } + if (column == Column.H) { + return "h"; + } + return null; + } + + private String parseRow(Row row) { + if (row == Row.ONE) { + return "1"; + } + if (row == Row.TWO) { + return "2"; + } + if (row == Row.THREE) { + return "3"; + } + if (row == Row.FOUR) { + return "4"; + } + if (row == Row.FIVE) { + return "5"; + } + if (row == Row.SIX) { + return "6"; + } + if (row == Row.SEVEN) { + return "7"; + } + if (row == Row.EIGHT) { + return "8"; + } + return null; + } + + private String colorMessage(String message, Color color) { + if (color == Color.BLACK) { + return "\u001B[90m" + message + "\u001B[0m"; + } + if (color == Color.WHITE) { + return "\u001B[97m" + message + "\u001B[0m"; + } + return "\u001B[34m" + message + "\u001B[0m"; + } +} diff --git a/src/test/java/chess/ColumnTest.java b/src/test/java/chess/ColumnTest.java index e43523240f7..73225bfe923 100644 --- a/src/test/java/chess/ColumnTest.java +++ b/src/test/java/chess/ColumnTest.java @@ -1,5 +1,6 @@ package chess; +import chess.domain.Column; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/chess/Fixtures.java b/src/test/java/chess/Fixtures.java index f940ab37137..c1b135539fd 100644 --- a/src/test/java/chess/Fixtures.java +++ b/src/test/java/chess/Fixtures.java @@ -1,5 +1,9 @@ package chess; +import chess.domain.Column; +import chess.domain.Position; +import chess.domain.Row; + @SuppressWarnings("unused") public final class Fixtures { diff --git a/src/test/java/chess/PositionTest.java b/src/test/java/chess/PositionTest.java index 3ad7cc64084..031fdd196a9 100644 --- a/src/test/java/chess/PositionTest.java +++ b/src/test/java/chess/PositionTest.java @@ -1,5 +1,7 @@ package chess; +import chess.domain.Movement; +import chess.domain.Position; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; diff --git a/src/test/java/chess/RowTest.java b/src/test/java/chess/RowTest.java index fcb65485410..feeb0883429 100644 --- a/src/test/java/chess/RowTest.java +++ b/src/test/java/chess/RowTest.java @@ -1,5 +1,6 @@ package chess; +import chess.domain.Row; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/chess/domain/piece/KnightTest.java b/src/test/java/chess/domain/piece/KnightTest.java new file mode 100644 index 00000000000..cf1b8127376 --- /dev/null +++ b/src/test/java/chess/domain/piece/KnightTest.java @@ -0,0 +1,44 @@ +package chess.domain.piece; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import chess.domain.Color; +import chess.domain.Column; +import chess.domain.Movement; +import chess.domain.Position; +import chess.domain.Row; +import chess.domain.piece.limited_moving_chess_piece.Knight; +import org.junit.jupiter.api.Test; + +class KnightTest { + + @Test + void 말이_움직일_수_있는_경로인지_확인할_수_있다() { + ChessPiece knight = new Knight(Color.BLACK); + Position origin = new Position(Row.FOUR, Column.D); + Position destination = new Position(Row.FIVE, Column.F); + + assertDoesNotThrow(() -> knight.findRoute(origin, destination)); + } + + @Test + void 말이_움직일_수_없는_경로인지_확인할_수_있다() { + ChessPiece knight = new Knight(Color.BLACK); + Position origin = new Position(Row.FOUR, Column.D); + Position destination = new Position(Row.SIX, Column.F); + + assertThatThrownBy(() -> knight.findRoute(origin, destination)); + } + + @Test + void 경로를_찾을_수_있다() { + ChessPiece knight = new Knight(Color.WHITE); + Position origin = new Position(Row.FOUR, Column.D); + Position destination = new Position(Row.FIVE, Column.F); + + assertThat(knight.findRoute(origin, destination)) + .containsExactly(Movement.RIGHT_RIGHT_UP); + } +} diff --git a/src/test/java/chess/domain/piece/RookTest.java b/src/test/java/chess/domain/piece/RookTest.java new file mode 100644 index 00000000000..1472558cfa0 --- /dev/null +++ b/src/test/java/chess/domain/piece/RookTest.java @@ -0,0 +1,45 @@ +package chess.domain.piece; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import chess.domain.Color; +import chess.domain.Column; +import chess.domain.Movement; +import chess.domain.Position; +import chess.domain.Row; +import chess.domain.piece.linear_moving_chess_piece.Rook; +import org.junit.jupiter.api.Test; + +class RookTest { + + @Test + void 룩이_움직일_수_있는_경로인지_확인할_수_있다() { + ChessPiece rook = new Rook(Color.BLACK); + Position origin = new Position(Row.FOUR, Column.B); + Position destination = new Position(Row.FOUR, Column.G); + + assertDoesNotThrow(() -> rook.findRoute(origin, destination)); + } + + @Test + void 룩이_움직일_수_없는_경로인지_확인할_수_있다() { + ChessPiece rook = new Rook(Color.WHITE); + Position origin = new Position(Row.FOUR, Column.B); + Position destination = new Position(Row.SIX, Column.F); + + assertThatThrownBy(() -> rook.findRoute(origin, destination)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void 경로를_찾을_수_있다() { + ChessPiece rook = new Rook(Color.BLACK); + Position origin = new Position(Row.FOUR, Column.B); + Position destination = new Position(Row.FOUR, Column.G); + + assertThat(rook.findRoute(origin, destination)) + .containsExactly(Movement.RIGHT, Movement.RIGHT, Movement.RIGHT, Movement.RIGHT, Movement.RIGHT); + } +}