diff --git a/README.md b/README.md index 9775dda0ae..c88a33caae 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,40 @@ -# java-janggi +## 기능 요구사항 체크리스트 -장기 미션 저장소 +### 게임 초기화 + +- [x] 두 플레이어가 각각 진형을 선택한다 +- [x] 진형 선택지: 왼상마(귀마), 오른상마(귀마), 원앙마, 양귀마 +- [x] 선택한 진형에 따라 마(馬)와 상(象)의 위치가 결정된다 + +### 기물 이동 + +- [x] 차(車): 직선(가로/세로) 이동, 이동 경로에 기물이 없어야 한다 +- [x] 마(馬): 직선 1칸 + 대각선 1칸 이동, 첫 번째 칸에 기물이 없어야 한다 +- [x] 상(象): 직선 1칸 + 대각선 2칸 이동, 이동 경로 2칸에 기물이 없어야 한다 +- [x] 궁(將/帥): 상하좌우 1칸 이동 +- [x] 사(士): 상하좌우 1칸 이동 +- [x] 포(包): 직선 이동, 반드시 기물 1개를 뛰어넘어야 하며 포를 넘거나 포를 잡을 수 없다 +- [x] 병/졸(兵/卒): 앞 또는 좌우 1칸 이동 + +### 이동 유효성 검증 + +- [x] 이동 불가능한 위치로 이동 시 예외 처리 +- [x] 이동 경로 중간에 기물이 있을 경우 이동 불가 (포 제외) +- [x] 아군 기물이 있는 위치로 이동 불가 +- [x] 적군 기물이 있는 위치로 이동 시 해당 기물 잡기 +- [x] 포의 경우 이동 경로에 정확히 1개의 기물이 있어야 이동 가능 +- [x] 포는 포를 뛰어넘거나 포를 잡을 수 없다 + +### 게임 진행 + +- [x] 초(CHO)가 먼저 시작하고 이후 번갈아 진행한다 +- [x] 매 턴 현재 보드 상태를 출력한다 +- [x] 잘못된 입력 시 재입력을 받는다 + +### 출력 + +- [x] 10x9 보드를 콘솔에 출력한다 +- [x] 초(CHO)는 파란색, 한(HAN)은 빨간색으로 구분하여 출력한다 +- [x] 빈 칸은 `ㅡ`로 표시한다 + +--- diff --git a/src/main/java/JanggiApplication.java b/src/main/java/JanggiApplication.java new file mode 100644 index 0000000000..985dfb3321 --- /dev/null +++ b/src/main/java/JanggiApplication.java @@ -0,0 +1,12 @@ +import controller.JanggiController; +import domain.piece.ConsolePieceAppearance; +import view.InputView; +import view.OutputView; + +public class JanggiApplication { + public static void main(String[] args) { + JanggiController janggiController = new JanggiController(new InputView(), + new OutputView(new ConsolePieceAppearance())); + janggiController.run(); + } +} diff --git a/src/main/java/controller/FormationConverter.java b/src/main/java/controller/FormationConverter.java new file mode 100644 index 0000000000..0881e16428 --- /dev/null +++ b/src/main/java/controller/FormationConverter.java @@ -0,0 +1,18 @@ +package controller; + +import domain.board.FormationType; + +public final class FormationConverter { + + private FormationConverter() {} + + public static FormationType convert(int formationType) { + if (formationType == 1) return FormationType.LEFT_GIWMA; + if (formationType == 2) return FormationType.RIGHT_GIWMA; + if (formationType == 3) return FormationType.WONANGMA; + if (formationType == 4) return FormationType.YANGGWIMA; + + throw new IllegalArgumentException("올바르지 않은 상차림 번호입니다."); + }; + +} diff --git a/src/main/java/controller/JanggiController.java b/src/main/java/controller/JanggiController.java new file mode 100644 index 0000000000..a8f8765b92 --- /dev/null +++ b/src/main/java/controller/JanggiController.java @@ -0,0 +1,46 @@ +package controller; + +import domain.board.FormationType; +import domain.game.JanggiGame; +import domain.game.Team; +import domain.position.Position; +import java.util.List; +import view.InputView; +import view.OutputView; + +public class JanggiController { + private final InputView inputView; + private final OutputView outputView; + + public JanggiController(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void run() { + JanggiGame game = createGame(); + outputView.printBoard(game.getBoard()); + while (game.isRunning()) { + playTurn(game); + } + } + + private JanggiGame createGame() { + FormationType choFormation = FormationConverter.convert(inputView.initialFormation(Team.CHO)); + FormationType hanFormation = FormationConverter.convert(inputView.initialFormation(Team.HAN)); + return JanggiGame.of(choFormation, hanFormation); + } + + private void playTurn(JanggiGame game) { + while (true) { + try { + List positions = inputView.askMovePiecePosition(game.currentTurn()); + game.move(positions.get(0), positions.get(1)); + outputView.printBoard(game.getBoard()); + return; + } catch (IllegalArgumentException e) { + outputView.printError(e.getMessage()); + } + } + } +} diff --git a/src/main/java/domain/board/AbstractBoardFactory.java b/src/main/java/domain/board/AbstractBoardFactory.java new file mode 100644 index 0000000000..8a82b4b564 --- /dev/null +++ b/src/main/java/domain/board/AbstractBoardFactory.java @@ -0,0 +1,50 @@ +package domain.board; + +import domain.game.Team; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.position.Position; +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractBoardFactory implements BoardFactory { + + private static final Map factories = Map.of( + FormationType.LEFT_GIWMA, new LeftGwimaFactory(), + FormationType.RIGHT_GIWMA, new RightGwimaFactory(), + FormationType.WONANGMA, new WonangmaFactory(), + FormationType.YANGGWIMA, new YanggwimaFactory() + ); + + public static AbstractBoardFactory from(FormationType type) { + AbstractBoardFactory factory = factories.get(type); + if (factory == null) { + throw new IllegalStateException("해당 타입의 팩토리가 등록되지 않았습니다."); + } + return factory; + } + + @Override + public Map createFormation(Team team) { + Map pieces = new HashMap<>(); + setFixedPieces(pieces, team); + setVariablePieces(pieces, team); + return pieces; + } + + private void setFixedPieces(Map pieces, Team team) { + placePieces(pieces, team, team.getBackRow(), PieceType.CHA); + placePieces(pieces, team, team.getBackRow(), PieceType.SA); + placePieces(pieces, team, team.getGeneralRow(), PieceType.GENERAL); + placePieces(pieces, team, team.getCannonRow(), PieceType.PHO); + placePieces(pieces, team, team.getSoldierRow(), PieceType.BYEONG); + } + + private void placePieces(Map pieces, Team team, int row, PieceType type) { + for (int column : type.getInitialColumns()) { + pieces.put(new Position(row, column), type.createPiece(team)); + } + } + + protected abstract void setVariablePieces(Map pieces, Team team); +} diff --git a/src/main/java/domain/board/Board.java b/src/main/java/domain/board/Board.java new file mode 100644 index 0000000000..b8aae3e8a9 --- /dev/null +++ b/src/main/java/domain/board/Board.java @@ -0,0 +1,43 @@ +package domain.board; + +import domain.piece.EmptyPiece; +import domain.piece.Piece; +import domain.position.Position; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class Board { + private final Map pieces; + + public Board(Map pieces) { + this.pieces = new HashMap<>(pieces); + } + + public void move(Position source, Position destination) { + Piece piece = pieceAt(source); + validateCanMove(piece, source, destination); + List route = piece.calculateRoute(source, destination); + List piecesOnRoute = route.stream() + .map(this::pieceAt) + .collect(Collectors.toList()); + piece.validateRoute(piecesOnRoute, pieceAt(destination)); + applyMove(source, destination, piece); + } + + private void validateCanMove(Piece piece, Position source, Position destination) { + if (!piece.canMove(source, destination)) { + throw new IllegalArgumentException("이동할 수 없는 위치입니다."); + } + } + + private void applyMove(Position source, Position destination, Piece piece) { + pieces.put(destination, piece); + pieces.put(source, EmptyPiece.getInstance()); + } + + public Piece pieceAt(Position position) { + return pieces.getOrDefault(position, EmptyPiece.getInstance()); + } +} diff --git a/src/main/java/domain/board/BoardFactory.java b/src/main/java/domain/board/BoardFactory.java new file mode 100644 index 0000000000..284627b0d9 --- /dev/null +++ b/src/main/java/domain/board/BoardFactory.java @@ -0,0 +1,10 @@ +package domain.board; + +import domain.position.Position; +import domain.piece.Piece; +import domain.game.Team; +import java.util.Map; + +public interface BoardFactory { + Map createFormation(Team team); +} diff --git a/src/main/java/domain/board/FormationType.java b/src/main/java/domain/board/FormationType.java new file mode 100644 index 0000000000..2b1133c8e5 --- /dev/null +++ b/src/main/java/domain/board/FormationType.java @@ -0,0 +1,8 @@ +package domain.board; + +public enum FormationType { + LEFT_GIWMA, + RIGHT_GIWMA, + WONANGMA, + YANGGWIMA +} diff --git a/src/main/java/domain/board/LeftGwimaFactory.java b/src/main/java/domain/board/LeftGwimaFactory.java new file mode 100644 index 0000000000..ea04168f22 --- /dev/null +++ b/src/main/java/domain/board/LeftGwimaFactory.java @@ -0,0 +1,21 @@ +package domain.board; + +import domain.position.Position; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.game.Team; +import java.util.Map; + +public class LeftGwimaFactory extends AbstractBoardFactory { + @Override + protected void setVariablePieces(Map pieces, Team team) { + pieces.put(new Position(team.getBackRow(), PieceType.SANG.getInitialColumns().get(0)), + PieceType.SANG.createPiece(team)); + pieces.put(new Position(team.getBackRow(), PieceType.MA.getInitialColumns().get(1)), + PieceType.MA.createPiece(team)); + pieces.put(new Position(team.getBackRow(), PieceType.SANG.getInitialColumns().get(2)), + PieceType.SANG.createPiece(team)); + pieces.put(new Position(team.getBackRow(), PieceType.MA.getInitialColumns().get(3)), + PieceType.MA.createPiece(team)); + } +} diff --git a/src/main/java/domain/board/RightGwimaFactory.java b/src/main/java/domain/board/RightGwimaFactory.java new file mode 100644 index 0000000000..a1def17cc2 --- /dev/null +++ b/src/main/java/domain/board/RightGwimaFactory.java @@ -0,0 +1,23 @@ +package domain.board; + +import domain.position.Position; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.game.Team; +import java.util.Map; + +public class RightGwimaFactory extends AbstractBoardFactory { + @Override + protected void setVariablePieces(Map pieces, Team team) { + pieces.put(new Position(team.getBackRow(), PieceType.MA.getInitialColumns().get(0)), + PieceType.MA.createPiece(team)); + pieces.put(new Position(team.getBackRow(), PieceType.SANG.getInitialColumns().get(1)), + PieceType.SANG.createPiece(team)); + pieces.put(new Position(team.getBackRow(), PieceType.MA.getInitialColumns().get(2)), + PieceType.MA.createPiece(team)); + pieces.put(new Position(team.getBackRow(), PieceType.SANG.getInitialColumns().get(3)), + PieceType.SANG.createPiece(team)); + } +} + + diff --git a/src/main/java/domain/board/WonangmaFactory.java b/src/main/java/domain/board/WonangmaFactory.java new file mode 100644 index 0000000000..7e55b21961 --- /dev/null +++ b/src/main/java/domain/board/WonangmaFactory.java @@ -0,0 +1,21 @@ +package domain.board; + +import domain.position.Position; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.game.Team; +import java.util.Map; + +public class WonangmaFactory extends AbstractBoardFactory { + @Override + protected void setVariablePieces(Map pieces, Team team) { + pieces.put(new Position(team.getBackRow(), PieceType.SANG.getInitialColumns().get(0)), + PieceType.SANG.createPiece(team)); + pieces.put(new Position(team.getBackRow(), PieceType.MA.getInitialColumns().get(1)), + PieceType.MA.createPiece(team)); + pieces.put(new Position(team.getBackRow(), PieceType.MA.getInitialColumns().get(2)), + PieceType.MA.createPiece(team)); + pieces.put(new Position(team.getBackRow(), PieceType.SANG.getInitialColumns().get(3)), + PieceType.SANG.createPiece(team)); + } +} diff --git a/src/main/java/domain/board/YanggwimaFactory.java b/src/main/java/domain/board/YanggwimaFactory.java new file mode 100644 index 0000000000..592f1148b0 --- /dev/null +++ b/src/main/java/domain/board/YanggwimaFactory.java @@ -0,0 +1,21 @@ +package domain.board; + +import domain.position.Position; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.game.Team; +import java.util.Map; + +public class YanggwimaFactory extends AbstractBoardFactory { + @Override + protected void setVariablePieces(Map pieces, Team team) { + pieces.put(new Position(team.getBackRow(), PieceType.MA.getInitialColumns().get(0)), + PieceType.MA.createPiece(team)); + pieces.put(new Position(team.getBackRow(), PieceType.SANG.getInitialColumns().get(1)), + PieceType.SANG.createPiece(team)); + pieces.put(new Position(team.getBackRow(), PieceType.SANG.getInitialColumns().get(2)), + PieceType.SANG.createPiece(team)); + pieces.put(new Position(team.getBackRow(), PieceType.MA.getInitialColumns().get(3)), + PieceType.MA.createPiece(team)); + } +} diff --git a/src/main/java/domain/game/JanggiGame.java b/src/main/java/domain/game/JanggiGame.java new file mode 100644 index 0000000000..bb589051f0 --- /dev/null +++ b/src/main/java/domain/game/JanggiGame.java @@ -0,0 +1,45 @@ +package domain.game; + +import domain.board.AbstractBoardFactory; +import domain.board.Board; +import domain.board.FormationType; +import domain.piece.Piece; +import domain.position.Position; +import java.util.HashMap; +import java.util.Map; + +public class JanggiGame { + private Turn turn; + private final Board board; + + public JanggiGame(Turn turn, Board board) { + this.turn = turn; + this.board = board; + } + + public static JanggiGame of(FormationType choFormation, FormationType hanFormation) { + Map pieces = new HashMap<>(); + pieces.putAll(AbstractBoardFactory.from(choFormation) + .createFormation(Team.CHO)); + pieces.putAll(AbstractBoardFactory.from(hanFormation) + .createFormation(Team.HAN)); + return new JanggiGame(Turn.first(),new Board(pieces)); + } + + public void move(Position source, Position destination) { + board.move(source, destination); + turn = turn.next(); + } + + public boolean isRunning() { + return true; // TODO: 종료 조건 구현 + } + + public Team currentTurn() { + return turn.current(); + } + + public Board getBoard() { + return board; + } +} diff --git a/src/main/java/domain/game/Team.java b/src/main/java/domain/game/Team.java new file mode 100644 index 0000000000..f2c44ee055 --- /dev/null +++ b/src/main/java/domain/game/Team.java @@ -0,0 +1,48 @@ +package domain.game; + +public enum Team { + CHO("초", 1, 2, 3, 4), + HAN("한", 10, 9, 8, 7); + + private final String teamName; + private final int backRow; + private final int generalRow; + private final int cannonRow; + private final int soldierRow; + + Team(String teamName, int backRow, int generalRow, int cannonRow, int soldierRow) { + this.teamName = teamName; + this.backRow = backRow; + this.generalRow = generalRow; + this.cannonRow = cannonRow; + this.soldierRow = soldierRow; + } + + public int getBackRow() { + return backRow; + } + + public int getGeneralRow() { + return generalRow; + } + + public int getCannonRow() { + return cannonRow; + } + + public int getSoldierRow() { + return soldierRow; + } + + public int forwardRowDirection() { + if (this == CHO) { + return 1; + } + return -1; + } + + @Override + public String toString() { + return teamName; + } +} diff --git a/src/main/java/domain/game/Turn.java b/src/main/java/domain/game/Turn.java new file mode 100644 index 0000000000..ada7f3fdbb --- /dev/null +++ b/src/main/java/domain/game/Turn.java @@ -0,0 +1,28 @@ +package domain.game; + +public class Turn { + private final Team current; + + private Turn(Team current) { + this.current = current; + } + + public static Turn first() { + return new Turn(Team.CHO); + } + + public Turn next() { + if (current == Team.CHO) { + return new Turn(Team.HAN); + } + return new Turn(Team.CHO); + } + + public boolean isTurn(Team team) { + return this.current == team; + } + + public Team current() { + return current; + } +} diff --git a/src/main/java/domain/piece/ActivePiece.java b/src/main/java/domain/piece/ActivePiece.java new file mode 100644 index 0000000000..6e0cb0685d --- /dev/null +++ b/src/main/java/domain/piece/ActivePiece.java @@ -0,0 +1,55 @@ +package domain.piece; + +import domain.game.Team; +import domain.position.Position; +import java.util.List; + +public abstract class ActivePiece implements Piece { + private final Team team; + private final PieceType type; + + protected ActivePiece(Team team, PieceType type) { + this.team = team; + this.type = type; + } + + @Override + public boolean isAlly(Piece other) { + return other instanceof ActivePiece activePiece && this.team == activePiece.team; + } + + @Override + public void validateRoute(List piecesOnRoute, Piece destinationPiece) { + for (Piece piece : piecesOnRoute) { + if (piece.isNotEmpty()) { + throw new IllegalArgumentException("이동 경로에 기물이 있습니다."); + } + } + validateDestination(destinationPiece); + } + + private void validateDestination(Piece destinationPiece) { + if (destinationPiece.isAlly(this)) { + throw new IllegalArgumentException("아군 기물이 있는 위치로 이동할 수 없습니다."); + } + } + + protected int forwardDirection() { + return team.forwardRowDirection(); + } + + @Override + public String display(PieceAppearance colorizer) { + return colorizer.colorize(team, type); + } + + @Override + public String toString() { + return type.name(); + } + + @Override + public boolean isNotEmpty() { + return true; + } +} diff --git a/src/main/java/domain/piece/Cannon.java b/src/main/java/domain/piece/Cannon.java new file mode 100644 index 0000000000..9369eb014c --- /dev/null +++ b/src/main/java/domain/piece/Cannon.java @@ -0,0 +1,51 @@ +package domain.piece; + +import domain.game.Team; +import domain.position.Position; +import java.util.List; + +public class Cannon extends ActivePiece { + + public Cannon(Team team) { + super(team, PieceType.PHO); + } + + @Override + public boolean canMove(Position source, Position target) { + return source.isSameCol(target) || source.isSameRow(target); + } + + @Override + public List calculateRoute(Position source, Position target) { + if (source.isSameCol(target)) { + return source.makeRowStraightRoute(target); + } + + return source.makeColStraightRoute(target); + } + + @Override + public void validateRoute(List piecesOnRoute, Piece destinationPiece) { + int count = 0; + for (Piece piece : piecesOnRoute) { + if (piece instanceof Cannon) { + throw new IllegalArgumentException("포는 포를 넘지 못합니다."); + } + if (piece.isNotEmpty()) { + count++; + } + } + + if (count != 1) { + throw new IllegalArgumentException("포가 넘을 수 있는 기물의 개수는 하나입니다."); + } + + if (destinationPiece instanceof Cannon) { + throw new IllegalArgumentException("포는 포를 잡을 수 없습니다."); + } + if (destinationPiece.isAlly(this)) { + throw new IllegalArgumentException("아군 기물이 있는 위치로 이동할 수 없습니다."); + } + } + +} diff --git a/src/main/java/domain/piece/Chariot.java b/src/main/java/domain/piece/Chariot.java new file mode 100644 index 0000000000..8674404bc0 --- /dev/null +++ b/src/main/java/domain/piece/Chariot.java @@ -0,0 +1,28 @@ +package domain.piece; + + +import domain.position.Position; +import domain.game.Team; +import java.util.List; + +public class Chariot extends ActivePiece { + + public Chariot(Team team) { + super(team, PieceType.CHA); + } + + @Override + public boolean canMove(Position source, Position target) { + return source.isSameCol(target) || source.isSameRow(target); + } + + @Override + public List calculateRoute(Position source, Position target) { + if (source.isSameCol(target)) { + return source.makeRowStraightRoute(target); + } + + return source.makeColStraightRoute(target); + } + +} diff --git a/src/main/java/domain/piece/ConsolePieceAppearance.java b/src/main/java/domain/piece/ConsolePieceAppearance.java new file mode 100644 index 0000000000..73a01c8d0a --- /dev/null +++ b/src/main/java/domain/piece/ConsolePieceAppearance.java @@ -0,0 +1,32 @@ +package domain.piece; + +import domain.game.Team; +import java.util.Map; + +public class ConsolePieceAppearance implements PieceAppearance { + private static final String RESET = "\u001B[0m"; + private static final Map COLORS = Map.of( + Team.CHO, "\u001B[34m", + Team.HAN, "\u001B[31m" + ); + + private static final Map DISPLAY_NAMES = Map.of( + PieceType.CHA, "차", + PieceType.MA, "마", + PieceType.SANG, "상", + PieceType.SA, "사", + PieceType.GENERAL, "궁", + PieceType.PHO, "포", + PieceType.BYEONG, "병" + ); + + @Override + public String colorize(Team team, PieceType type) { + return COLORS.get(team) + DISPLAY_NAMES.get(type) + RESET; + } + + @Override + public String colorizeEmpty() { + return "ㅡ"; + } +} diff --git a/src/main/java/domain/piece/Elephant.java b/src/main/java/domain/piece/Elephant.java new file mode 100644 index 0000000000..21c17bc2e2 --- /dev/null +++ b/src/main/java/domain/piece/Elephant.java @@ -0,0 +1,47 @@ +package domain.piece; + +import domain.position.Position; +import domain.game.Team; +import java.util.List; + +public class Elephant extends ActivePiece { + + private static final List rowOffsets = List.of(2, 3, -2, -3, -3, -2, 2, 3); + private static final List columnOffsets = List.of(3, 2, 3, 2, -2, -3, -3, -2); + + public Elephant(Team team) { + super(team, PieceType.SANG); + } + + @Override + public boolean canMove(Position source, Position target) { + int rowDiff = target.rowDiff(source); + int colDiff = target.columnDiff(source); + + for (int i = 0; i < rowOffsets.size(); i++) { + if (rowOffsets.get(i) == rowDiff && columnOffsets.get(i) == colDiff) { + return true; + } + } + return false; + } + + @Override + public List calculateRoute(Position source, Position target) { + if (source.columnDiff(target) == -3) { + Position mid = source.addPosition(0, 1); + return List.of(mid, mid.middlePosition(target)); + } + if (source.columnDiff(target) == 3) { + Position mid = source.addPosition(0, -1); + return List.of(mid, mid.middlePosition(target)); + } + if (source.rowDiff(target) == -3) { + Position mid = source.addPosition(1, 0); + return List.of(mid, mid.middlePosition(target)); + } + Position mid = source.addPosition(-1, 0); + return List.of(mid, mid.middlePosition(target)); + } + +} diff --git a/src/main/java/domain/piece/EmptyPiece.java b/src/main/java/domain/piece/EmptyPiece.java new file mode 100644 index 0000000000..13bd430774 --- /dev/null +++ b/src/main/java/domain/piece/EmptyPiece.java @@ -0,0 +1,51 @@ +package domain.piece; + +import domain.position.Position; +import java.util.List; + +public class EmptyPiece implements Piece { + + private EmptyPiece() {} + + private static class LazyHolder { + private static final EmptyPiece INSTANCE = new EmptyPiece(); + } + + public static EmptyPiece getInstance() { + return LazyHolder.INSTANCE; + } + + @Override + public boolean canMove(Position source, Position target) { + return false; + } + + @Override + public List calculateRoute(Position source, Position target) { + return List.of(); + } + + @Override + public void validateRoute(List piecesOnRoute, Piece destinationPiece) { + } + + @Override + public boolean isNotEmpty() { + return false; + } + + @Override + public boolean isAlly(Piece other) { + return false; + } + + @Override + public String display(PieceAppearance colorizer) { + return colorizer.colorizeEmpty(); + } + + @Override + public String toString() { + return PieceType.EMPTY.name(); + } +} diff --git a/src/main/java/domain/piece/General.java b/src/main/java/domain/piece/General.java new file mode 100644 index 0000000000..48d44f15d9 --- /dev/null +++ b/src/main/java/domain/piece/General.java @@ -0,0 +1,32 @@ +package domain.piece; + +import domain.position.Position; +import domain.game.Team; +import java.util.List; + +public class General extends ActivePiece { + private static final List rowOffsets = List.of(-1, 1, 0, 0); + private static final List columnOffsets = List.of(0, 0, -1, 1); + + public General(Team team) { + super(team, PieceType.GENERAL); + } + + @Override + public boolean canMove(Position source, Position target) { + int rowDiff = target.rowDiff(source); + int colDiff = target.columnDiff(source); + + for (int i = 0; i < rowOffsets.size(); i++) { + if (rowOffsets.get(i) == rowDiff && columnOffsets.get(i) == colDiff) { + return true; + } + } + return false; + } + + @Override + public List calculateRoute(Position source, Position target) { + return List.of(); + } +} diff --git a/src/main/java/domain/piece/Guard.java b/src/main/java/domain/piece/Guard.java new file mode 100644 index 0000000000..b5b89ea615 --- /dev/null +++ b/src/main/java/domain/piece/Guard.java @@ -0,0 +1,32 @@ +package domain.piece; + +import domain.position.Position; +import domain.game.Team; +import java.util.List; + +public class Guard extends ActivePiece { + private static final List rowOffsets = List.of(-1, 1, 0, 0); + private static final List columnOffsets = List.of(0, 0, -1, 1); + + public Guard(Team team) { + super(team, PieceType.SA); + } + + @Override + public boolean canMove(Position source, Position target) { + int rowDiff = target.rowDiff(source); + int colDiff = target.columnDiff(source); + + for (int i = 0; i < rowOffsets.size(); i++) { + if (rowOffsets.get(i) == rowDiff && columnOffsets.get(i) == colDiff) { + return true; + } + } + return false; + } + + @Override + public List calculateRoute(Position source, Position target) { + return List.of(); + } +} diff --git a/src/main/java/domain/piece/Horse.java b/src/main/java/domain/piece/Horse.java new file mode 100644 index 0000000000..70e337260c --- /dev/null +++ b/src/main/java/domain/piece/Horse.java @@ -0,0 +1,46 @@ +package domain.piece; + +import domain.position.Position; +import domain.game.Team; +import java.util.List; + +public class Horse extends ActivePiece { + + private static final List rowOffsets = List.of(1, 2, 2, 1, -1, -2, -2, -1); + private static final List columnOffsets = List.of(2, 1, -1, -2, -2, -1, 1, 2); + + public Horse(Team team) { + super(team, PieceType.MA); + } + + @Override + public boolean canMove(Position source, Position target) { + int rowDiff = target.rowDiff(source); + int colDiff = target.columnDiff(source); + + for (int i = 0; i < rowOffsets.size(); i++) { + if (rowOffsets.get(i) == rowDiff && columnOffsets.get(i) == colDiff) { + return true; + } + } + return false; + } + + @Override + public List calculateRoute(Position source, Position target) { + if (source.columnDiff(target) == -2) { + return List.of(source.addPosition(0, 1)); + } + + if (source.columnDiff(target) == 2) { + return List.of(source.addPosition(0, -1)); + } + + if (source.rowDiff(target) == -2) { + return List.of(source.addPosition(1, 0)); + } + + return List.of(source.addPosition(-1, 0)); + } + +} diff --git a/src/main/java/domain/piece/Piece.java b/src/main/java/domain/piece/Piece.java new file mode 100644 index 0000000000..0ee81a9543 --- /dev/null +++ b/src/main/java/domain/piece/Piece.java @@ -0,0 +1,18 @@ +package domain.piece; + +import domain.position.Position; +import java.util.List; + +public interface Piece { + boolean canMove(Position source, Position target); + + List calculateRoute(Position source, Position target); + + void validateRoute(List piecesOnRoute, Piece destinationPiece); + + boolean isNotEmpty(); + + boolean isAlly(Piece other); + + String display(PieceAppearance colorizer); +} diff --git a/src/main/java/domain/piece/PieceAppearance.java b/src/main/java/domain/piece/PieceAppearance.java new file mode 100644 index 0000000000..a4cad188e8 --- /dev/null +++ b/src/main/java/domain/piece/PieceAppearance.java @@ -0,0 +1,9 @@ +package domain.piece; + +import domain.game.Team; + +public interface PieceAppearance { + String colorize(Team team, PieceType type); + + String colorizeEmpty(); +} diff --git a/src/main/java/domain/piece/PieceType.java b/src/main/java/domain/piece/PieceType.java new file mode 100644 index 0000000000..0835706c0a --- /dev/null +++ b/src/main/java/domain/piece/PieceType.java @@ -0,0 +1,34 @@ +package domain.piece; + +import domain.game.Team; +import java.util.List; +import java.util.function.Function; + +public enum PieceType { + CHA(13.0, List.of(1, 9), Chariot::new), + MA(5.0, List.of(2, 3, 7, 8), Horse::new), + SANG(3.0, List.of(2, 3, 7, 8), Elephant::new), + SA(3.0, List.of(4, 6), Guard::new), + GENERAL(0.0, List.of(5), General::new), + PHO(7.0, List.of(2, 8), Cannon::new), + BYEONG(2.0, List.of(1, 3, 5, 7, 9), Soldier::new), + EMPTY(0.0, List.of(), team -> EmptyPiece.getInstance()),; + + private final double score; + private final List initialColumns; + private final Function pieceFactory; + + PieceType(double score, List initialColumns, Function pieceFactory) { + this.score = score; + this.initialColumns = initialColumns; + this.pieceFactory = pieceFactory; + } + + public List getInitialColumns() { + return initialColumns; + } + + public Piece createPiece(Team team) { + return pieceFactory.apply(team); + } +} diff --git a/src/main/java/domain/piece/Soldier.java b/src/main/java/domain/piece/Soldier.java new file mode 100644 index 0000000000..f69187e88e --- /dev/null +++ b/src/main/java/domain/piece/Soldier.java @@ -0,0 +1,39 @@ +package domain.piece; + +import domain.position.Position; +import domain.game.Team; +import java.util.List; + +public class Soldier extends ActivePiece { + private static final List rowOffsets = List.of(-1, 1); + private static final List columnOffsets = List.of(0, 0); + + public Soldier(Team team) { + super(team, PieceType.BYEONG); + } + + @Override + public boolean canMove(Position source, Position target) { + if (isValidSideMove(source, target)) { + return true; + } + return target.rowDiff(source) == forwardDirection() && target.columnDiff(source) == 0; + } + + private boolean isValidSideMove(Position source, Position target) { + int rowDiff = target.rowDiff(source); + int colDiff = target.columnDiff(source); + + for (int i = 0; i < rowOffsets.size(); i++) { + if (rowOffsets.get(i) == colDiff && columnOffsets.get(i) == rowDiff) { + return true; + } + } + return false; + } + + @Override + public List calculateRoute(Position source, Position target) { + return List.of(); + } +} diff --git a/src/main/java/domain/position/Column.java b/src/main/java/domain/position/Column.java new file mode 100644 index 0000000000..1073e21625 --- /dev/null +++ b/src/main/java/domain/position/Column.java @@ -0,0 +1,54 @@ +package domain.position; + +public class Column { + + private static final int MIN_COLUMNS_NUMBER = 1; + private static final int MAX_COLUMNS_NUMBER = 9; + + private final int value; + + public Column(int value) { + validate(value); + this.value = value; + } + + private void validate(int value) { + if (value < MIN_COLUMNS_NUMBER || value > MAX_COLUMNS_NUMBER) { + throw new IllegalArgumentException("열의 위치는 1-9 사이에 있어야 합니다."); + } + } + + public Column divide(Column column) { + return new Column((this.value + column.value) / 2); + } + + public int min(Column other) { + return Math.min(other.value, value); + } + + public int max(Column other) { + return Math.max(other.value, value); + } + + public int diff(Column other) { + return this.value - other.value; + } + + public Column add(int measure) { + return new Column(this.value + measure); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Column column = (Column) o; + return value == column.value; + } + + @Override + public int hashCode() { + return Integer.hashCode(value); + } +} diff --git a/src/main/java/domain/position/Position.java b/src/main/java/domain/position/Position.java new file mode 100644 index 0000000000..0f3c9a6d7c --- /dev/null +++ b/src/main/java/domain/position/Position.java @@ -0,0 +1,86 @@ +package domain.position; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class Position { + private final Row row; + private final Column column; + + public Position(int row, int column) { + this.row = new Row(row); + this.column = new Column(column); + } + + public static Position from(String row, String column) { + try { + return new Position(Integer.parseInt(row), Integer.parseInt(column)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("위치는 숫자로 입력해주세요."); + } + } + + public Position(Row row, Column column) { + this.row = row; + this.column = column; + } + + public boolean isSameRow(Position other) { + return other.row.equals(this.row); + } + + public boolean isSameCol(Position other) { + return other.column.equals(this.column); + } + + public int rowDiff(Position other) { + return this.row.diff(other.row); + } + + public int columnDiff(Position other) { + return this.column.diff(other.column); + } + + public List makeColStraightRoute(Position other) { + List routes = new ArrayList<>(); + int start = other.column.min(this.column); + int end = other.column.max(this.column); + for (int i = start + 1; i < end; i++) { + routes.add(new Position(other.row, new Column(i))); + } + return routes; + } + + public List makeRowStraightRoute(Position other) { + List routes = new ArrayList<>(); + int start = other.row.min(this.row); + int end = other.row.max(this.row); + for (int i = start + 1; i < end; i++) { + routes.add(new Position(new Row(i), other.column)); + } + return routes; + } + + public Position addPosition(int x, int y) { + return new Position(this.row.add(x), this.column.add(y)); + } + + public Position middlePosition(Position other) { + return new Position(this.row.divide(other.row), this.column.divide(other.column)); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Position position = (Position) o; + return row.equals(position.row) && column.equals(position.column); + } + + @Override + public int hashCode() { + return Objects.hash(row, column); + } +} diff --git a/src/main/java/domain/position/Row.java b/src/main/java/domain/position/Row.java new file mode 100644 index 0000000000..4e5c5ac9d1 --- /dev/null +++ b/src/main/java/domain/position/Row.java @@ -0,0 +1,54 @@ +package domain.position; + +public class Row { + + private static final int MIN_ROW_NUMBER = 1; + private static final int MAX_ROW_NUMBER = 9; + + private final int value; + + public Row(int value) { + validate(value); + this.value = value; + } + + private void validate(int value) { + if (value <= 0 || value > 10) { + throw new IllegalArgumentException("행의 위치는 1-10 사이에 있어야 합니다."); + } + } + + public int min(Row other) { + return Math.min(other.value, value); + } + + public int max(Row other) { + return Math.max(other.value, value); + } + + public int diff(Row other) { + return this.value - other.value; + } + + public Row add(int measure) { + return new Row(this.value + measure); + } + + public Row divide(Row row) { + return new Row((this.value + row.value) / 2); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Row row = (Row) o; + return value == row.value; + } + + @Override + public int hashCode() { + return Integer.hashCode(value); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 0000000000..ead5829444 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,57 @@ +package view; + +import domain.game.Team; +import domain.position.Position; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; + +public class InputView { + private static final int MOVE_INPUT_COUNT = 4; + private final Scanner scanner = new Scanner(System.in); + + public Integer initialFormation(Team team) { + while (true) { + System.out.println(team + " 진영 배치 전략을 입력 하세요.\n1. 왼상\n2. 오른상\n3. 원앙마\n4. 양귀마 "); + try { + int parseNumber = Integer.parseInt(scanner.nextLine()); + return validRange(parseNumber); + } catch (NumberFormatException e) { + System.out.println("숫자를 입력해주세요."); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + } + } + } + + private int validRange(int number) { + if (number < 1 || number > 4) { + throw new IllegalArgumentException("1 ~ 4 사이의 숫자로 입력해주세요."); + } + return number; + } + + public List askMovePiecePosition(Team team) { + while (true) { + System.out.println(team + "의 차례입니다. 움직일 기물의 위치와 이동할 위치를 행과 열 순서대로 입력하세요. ( 예: 2,5,4,3 )"); + try { + List inputs = Arrays.stream(scanner.nextLine().split(",")) + .map(String::trim) + .collect(Collectors.toList()); + validatePositionFormat(inputs); + Position source = Position.from(inputs.get(0), inputs.get(1)); + Position destination = Position.from(inputs.get(2), inputs.get(3)); + return List.of(source, destination); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + } + } + } + + private void validatePositionFormat(List inputs) { + if (inputs.size() != MOVE_INPUT_COUNT) { + throw new IllegalArgumentException("예시와 똑같은 형식으로 입력해주세요. (예: 2,5,4,3)"); + } + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 0000000000..1a1892d8ff --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,46 @@ +package view; + +import domain.board.Board; +import domain.piece.Piece; +import domain.piece.PieceAppearance; +import domain.position.Position; + +public class OutputView { + private static final int MAX_ROW = 10; + private static final int MAX_COLUMN = 9; + private final PieceAppearance appearance; + + public OutputView(PieceAppearance appearance) { + this.appearance = appearance; + } + + public void printBoard(Board board) { + StringBuilder sb = new StringBuilder(); + for (int row = MAX_ROW; row >= 1; row--) { + sb.append(row).append("\t"); + appendRow(sb, board, row); + sb.append(System.lineSeparator()); + } + appendColumnHeader(sb); + System.out.println(sb); + } + + public void printError(String message) { + System.out.println(message); + } + + private void appendColumnHeader(StringBuilder sb) { + sb.append(" \t"); + for (int column = 1; column <= MAX_COLUMN; column++) { + sb.append(column).append("\t"); + } + sb.append(System.lineSeparator()); + } + + private void appendRow(StringBuilder sb, Board board, int row) { + for (int column = 1; column <= MAX_COLUMN; column++) { + Piece piece = board.pieceAt(new Position(row, column)); + sb.append(piece.display(appearance)).append("\t"); + } + } +} diff --git a/src/test/java/domain/activePiece/CannonTest.java b/src/test/java/domain/activePiece/CannonTest.java new file mode 100644 index 0000000000..6d23af267c --- /dev/null +++ b/src/test/java/domain/activePiece/CannonTest.java @@ -0,0 +1,56 @@ +package domain.activePiece; + +import static org.assertj.core.api.Assertions.assertThat; + +import domain.game.Team; +import domain.piece.ActivePiece; +import domain.piece.Cannon; +import domain.piece.Piece; +import domain.position.Column; +import domain.position.Position; +import domain.position.Row; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class CannonTest { + + @Test + void 포는_직선_운동한다() { + Piece cannon = new Cannon(Team.HAN); + assertThat(cannon.canMove(new Position(1, 4), new Position(1, 8))).isTrue(); + + } + + @Test + void 포는_직선이_아닌_방향으로_가지_못한다() { + Piece cannon = new Cannon(Team.HAN); + assertThat(cannon.canMove(new Position(5, 5), new Position(3, 3))).isFalse(); + + } + + @Test + void 같은_열의_행_방향_경로를_계산한다() { + ActivePiece cannon = new Cannon(Team.HAN); + Position source = new Position(new Row(1), new Column(3)); + Position destination = new Position(new Row(4), new Column(3)); + List routes = new ArrayList<>( + List.of(new Position(new Row(2), new Column(3)), + new Position(new Row(3), new Column(3)))); + + assertThat(cannon.calculateRoute(source, destination)).isEqualTo(routes); + } + + @Test + void 같은_행의_열_방향_경로를_계산한다() { + ActivePiece cannon = new Cannon(Team.HAN); + Position source = new Position(new Row(1), new Column(3)); + Position destination = new Position(new Row(1), new Column(7)); + List routes = new ArrayList<>( + List.of(new Position(new Row(1), new Column(4)), + new Position(new Row(1), new Column(5)), + new Position(new Row(1), new Column(6)))); + + assertThat(cannon.calculateRoute(source, destination)).isEqualTo(routes); + } +} diff --git a/src/test/java/domain/activePiece/ChariotTest.java b/src/test/java/domain/activePiece/ChariotTest.java new file mode 100644 index 0000000000..7e55fd2c59 --- /dev/null +++ b/src/test/java/domain/activePiece/ChariotTest.java @@ -0,0 +1,56 @@ +package domain.activePiece; + +import static org.assertj.core.api.Assertions.assertThat; + +import domain.game.Team; +import domain.piece.ActivePiece; +import domain.piece.Chariot; +import domain.piece.Piece; +import domain.position.Column; +import domain.position.Position; +import domain.position.Row; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ChariotTest { + + @Test + void 차는_직선_운동한다() { + Piece cha = new Chariot(Team.HAN); + assertThat(cha.canMove(new Position(1, 4), new Position(1, 8))).isTrue(); + + } + + @Test + void 차는_직선이_아닌_방향으로_가지_못한다() { + Piece cha = new Chariot(Team.HAN); + assertThat(cha.canMove(new Position(5, 5), new Position(3, 3))).isFalse(); + + } + + @Test + void 같은_열의_행_방향_경로를_계산한다() { + ActivePiece chariot = new Chariot(Team.HAN); + Position source = new Position(new Row(1), new Column(3)); + Position destination = new Position(new Row(4), new Column(3)); + List routes = new ArrayList<>( + List.of(new Position(new Row(2), new Column(3)), + new Position(new Row(3), new Column(3)))); + + assertThat(chariot.calculateRoute(source, destination)).isEqualTo(routes); + } + + @Test + void 같은_행의_열_방향_경로를_계산한다() { + ActivePiece chariot = new Chariot(Team.HAN); + Position source = new Position(new Row(1), new Column(3)); + Position destination = new Position(new Row(1), new Column(7)); + List routes = new ArrayList<>( + List.of(new Position(new Row(1), new Column(4)), + new Position(new Row(1), new Column(5)), + new Position(new Row(1), new Column(6)))); + + assertThat(chariot.calculateRoute(source, destination)).isEqualTo(routes); + } +} diff --git a/src/test/java/domain/activePiece/ElephantTest.java b/src/test/java/domain/activePiece/ElephantTest.java new file mode 100644 index 0000000000..d45ccf4bc1 --- /dev/null +++ b/src/test/java/domain/activePiece/ElephantTest.java @@ -0,0 +1,50 @@ +package domain.activePiece; + +import static org.assertj.core.api.Assertions.assertThat; + +import domain.game.Team; +import domain.piece.ActivePiece; +import domain.piece.Elephant; +import domain.piece.Piece; +import domain.position.Position; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ElephantTest { + + @Test + void 정상_범위_입력() { + Piece elephant = new Elephant(Team.HAN); + assertThat(elephant.canMove(new Position(5, 5), new Position(7, 8))).isTrue(); + } + + @Test + void 정상_범위가_아니면_거짓() { + Piece elephant = new Elephant(Team.HAN); + assertThat(elephant.canMove(new Position(5, 5), new Position(8, 8))).isFalse(); + } + + @Test + void 상_정상_경로_출력() { + ActivePiece elephant = new Elephant(Team.HAN); + Position source = new Position(3, 3); + Position mid = new Position(4, 3); + Position mid2 = new Position(5, 4); + Position destination = new Position(6, 5); + List routes = List.of(mid, mid2); + + assertThat(elephant.calculateRoute(source, destination)).isEqualTo(routes); + } + + @Test + void 상_열_방향_경로를_계산한다() { + ActivePiece elephant = new Elephant(Team.HAN); + Position source = new Position(3, 3); + Position mid = new Position(3, 4); + Position mid2 = new Position(4, 5); + Position destination = new Position(5, 6); + List routes = List.of(mid, mid2); + + assertThat(elephant.calculateRoute(source, destination)).isEqualTo(routes); + } +} diff --git a/src/test/java/domain/activePiece/GeneralTest.java b/src/test/java/domain/activePiece/GeneralTest.java new file mode 100644 index 0000000000..5849069d35 --- /dev/null +++ b/src/test/java/domain/activePiece/GeneralTest.java @@ -0,0 +1,41 @@ +package domain.activePiece; + +import static org.assertj.core.api.Assertions.assertThat; + +import domain.game.Team; +import domain.piece.ActivePiece; +import domain.piece.General; +import domain.piece.Piece; +import domain.position.Column; +import domain.position.Position; +import domain.position.Row; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class GeneralTest { + + @Test + void 정상_범위_입력() { + Piece general = new General(Team.HAN); + assertThat(general.canMove(new Position(5, 5), new Position(5, 6))).isTrue(); + } + + @Test + void 정상_범위가_아니면_거짓() { + Piece general = new General(Team.HAN); + assertThat(general.canMove(new Position(5, 5), new Position(6, 6))).isFalse(); + } + + @Test + void 궁_정상_경로_출력_한다() { + ActivePiece general = new General(Team.HAN); + + Position source = new Position(new Row(1), new Column(3)); + Position destination = new Position(new Row(2), new Column(3)); + List routes = new ArrayList<>(); + + assertThat(general.calculateRoute(source, destination)).isEqualTo(routes); + } + +} diff --git a/src/test/java/domain/activePiece/GuardTest.java b/src/test/java/domain/activePiece/GuardTest.java new file mode 100644 index 0000000000..e0647015f5 --- /dev/null +++ b/src/test/java/domain/activePiece/GuardTest.java @@ -0,0 +1,41 @@ +package domain.activePiece; + +import static org.assertj.core.api.Assertions.assertThat; + +import domain.game.Team; +import domain.piece.ActivePiece; +import domain.piece.Guard; +import domain.piece.Piece; +import domain.position.Column; +import domain.position.Position; +import domain.position.Row; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class GuardTest { + + @Test + void 정상_범위_입력() { + Piece guard = new Guard(Team.HAN); + assertThat(guard.canMove(new Position(5, 5), new Position(6, 5))).isTrue(); + } + + @Test + void 정상_범위가_아니면_거짓() { + Piece guard = new Guard(Team.HAN); + assertThat(guard.canMove(new Position(5, 5), new Position(8, 8))).isFalse(); + } + + @Test + void 사_정상_경로_출력_한다() { + ActivePiece guard = new Guard(Team.HAN); + + Position source = new Position(new Row(1), new Column(3)); + Position destination = new Position(new Row(2), new Column(3)); + List routes = new ArrayList<>(); + + assertThat(guard.calculateRoute(source, destination)).isEqualTo(routes); + } + +} diff --git a/src/test/java/domain/activePiece/HorseTest.java b/src/test/java/domain/activePiece/HorseTest.java new file mode 100644 index 0000000000..7be01f014a --- /dev/null +++ b/src/test/java/domain/activePiece/HorseTest.java @@ -0,0 +1,48 @@ +package domain.activePiece; + +import static org.assertj.core.api.Assertions.assertThat; + +import domain.game.Team; +import domain.piece.ActivePiece; +import domain.piece.Horse; +import domain.piece.Piece; +import domain.position.Position; +import java.util.List; +import org.junit.jupiter.api.Test; + +class HorseTest { + + @Test + void 정상_범위_입력() { + Piece horse = new Horse(Team.HAN); + assertThat(horse.canMove(new Position(5, 5), new Position(6, 7))).isTrue(); + } + + @Test + void 정상_범위가_아니면_거짓() { + Piece horse = new Horse(Team.HAN); + assertThat(horse.canMove(new Position(5, 5), new Position(8, 8))).isFalse(); + } + + @Test + void 마_정상_경로_출력() { + ActivePiece horse = new Horse(Team.HAN); + Position source = new Position(3, 3); + Position mid = new Position(4, 3); + Position destination = new Position(5, 4); + List routes = List.of(mid); + + assertThat(horse.calculateRoute(source, destination)).isEqualTo(routes); + } + + @Test + void 마_열_방향_경로를_계산한다() { + ActivePiece horse = new Horse(Team.HAN); + Position source = new Position(3, 3); + Position mid = new Position(3, 4); + Position destination = new Position(4, 5); + List routes = List.of(mid); + + assertThat(horse.calculateRoute(source, destination)).isEqualTo(routes); + } +} diff --git a/src/test/java/domain/activePiece/SoldierTest.java b/src/test/java/domain/activePiece/SoldierTest.java new file mode 100644 index 0000000000..4c3135eb88 --- /dev/null +++ b/src/test/java/domain/activePiece/SoldierTest.java @@ -0,0 +1,89 @@ +package domain.activePiece; + +import static org.assertj.core.api.Assertions.assertThat; + +import domain.game.Team; +import domain.piece.ActivePiece; +import domain.piece.Piece; +import domain.piece.Soldier; +import domain.position.Column; +import domain.position.Position; +import domain.position.Row; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class SoldierTest { + + @Test + void 한_진영에서_왼쪽_이동() { + Piece soldier = new Soldier(Team.HAN); + assertThat(soldier.canMove(new Position(5, 5), new Position(5, 4))).isTrue(); + } + + @Test + void 한_진영에서_오른쪽_이동() { + Piece soldier = new Soldier(Team.HAN); + assertThat(soldier.canMove(new Position(5, 5), new Position(5, 6))).isTrue(); + } + + @Test + void 한_진영에서_전진_이동() { + Piece soldier = new Soldier(Team.HAN); + assertThat(soldier.canMove(new Position(5, 5), new Position(4, 5))).isTrue(); + } + + @Test + void 한_진영에서_후진_불가() { + Piece soldier = new Soldier(Team.HAN); + assertThat(soldier.canMove(new Position(5, 5), new Position(6, 5))).isFalse(); + } + + @Test + void 한_진영에서_정상_범위가_아니면_거짓() { + Piece soldier = new Soldier(Team.HAN); + assertThat(soldier.canMove(new Position(5, 5), new Position(8, 8))).isFalse(); + } + + @Test + void 초_진영에서_왼쪽_이동() { + Piece soldier = new Soldier(Team.CHO); + assertThat(soldier.canMove(new Position(5, 5), new Position(5, 4))).isTrue(); + } + + @Test + void 초_진영에서_오른쪽_이동() { + Piece soldier = new Soldier(Team.CHO); + assertThat(soldier.canMove(new Position(5, 5), new Position(5, 6))).isTrue(); + } + + @Test + void 초_진영에서_전진_이동() { + Piece soldier = new Soldier(Team.CHO); + assertThat(soldier.canMove(new Position(5, 5), new Position(6, 5))).isTrue(); + } + + @Test + void 초_진영에서_후진_불가() { + Piece soldier = new Soldier(Team.CHO); + assertThat(soldier.canMove(new Position(5, 5), new Position(4, 5))).isFalse(); + } + + @Test + void 초_진영에서_정상_범위가_아니면_거짓() { + Piece soldier = new Soldier(Team.CHO); + assertThat(soldier.canMove(new Position(5, 5), new Position(8, 8))).isFalse(); + } + + @Test + void 병은_빈_경로_출력_한다() { + ActivePiece soldier = new Soldier(Team.HAN); + + Position source = new Position(new Row(5), new Column(5)); + Position destination = new Position(new Row(4), new Column(5)); + List routes = new ArrayList<>(); + + assertThat(soldier.calculateRoute(source, destination)).isEqualTo(routes); + } + +} diff --git a/src/test/java/domain/board/BoardTest.java b/src/test/java/domain/board/BoardTest.java new file mode 100644 index 0000000000..ec90b54ced --- /dev/null +++ b/src/test/java/domain/board/BoardTest.java @@ -0,0 +1,161 @@ +package domain.board; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import domain.game.Team; +import domain.piece.Cannon; +import domain.piece.Chariot; +import domain.piece.EmptyPiece; +import domain.piece.Piece; +import domain.piece.Soldier; +import domain.position.Position; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class BoardTest { + + @Test + void 기물이_이동하면_원래_위치는_빈칸이_된다() { + Map pieces = new HashMap<>(); + pieces.put(new Position(1, 1), new Chariot(Team.HAN)); + Board board = new Board(pieces); + + board.move(new Position(1, 1), new Position(1, 5)); + + assertThat(board.pieceAt(new Position(1, 1)).isNotEmpty()).isFalse(); + } + + @Test + void 기물이_이동하면_도착_위치에_기물이_있다() { + Map pieces = new HashMap<>(); + pieces.put(new Position(1, 1), new Chariot(Team.HAN)); + Board board = new Board(pieces); + + board.move(new Position(1, 1), new Position(1, 5)); + + assertThat(board.pieceAt(new Position(1, 5)).isNotEmpty()).isTrue(); + } + + @Test + void 이동_경로에_기물이_있으면_예외() { + Map pieces = new HashMap<>(); + pieces.put(new Position(1, 1), new Chariot(Team.HAN)); + pieces.put(new Position(1, 3), new Soldier(Team.CHO)); + Board board = new Board(pieces); + + assertThatThrownBy(() -> board.move(new Position(1, 1), new Position(1, 5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동 경로에 기물이 있습니다."); + } + + @Test + void 아군_기물이_있는_위치로_이동하면_예외() { + Map pieces = new HashMap<>(); + pieces.put(new Position(1, 1), new Chariot(Team.HAN)); + pieces.put(new Position(1, 5), new Chariot(Team.HAN)); + Board board = new Board(pieces); + + assertThatThrownBy(() -> board.move(new Position(1, 1), new Position(1, 5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("아군 기물이 있는 위치로 이동할 수 없습니다."); + } + + @Test + void 적_기물을_잡으면_도착지에_내_기물이_있다() { + Map pieces = new HashMap<>(); + pieces.put(new Position(1, 1), new Chariot(Team.HAN)); + pieces.put(new Position(1, 5), new Chariot(Team.CHO)); + Board board = new Board(pieces); + + board.move(new Position(1, 1), new Position(1, 5)); + + assertThat(board.pieceAt(new Position(1, 5))).isInstanceOf(Chariot.class); + assertThat(board.pieceAt(new Position(1, 1))).isInstanceOf(EmptyPiece.class); + } + + @Test + void 이동할_수_없는_방향이면_예외() { + Map pieces = new HashMap<>(); + pieces.put(new Position(5, 5), new Chariot(Team.HAN)); + Board board = new Board(pieces); + + assertThatThrownBy(() -> board.move(new Position(5, 5), new Position(3, 3))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다."); + } + + @Test + void 포는_기물_하나를_넘어_이동한다() { + Map pieces = new HashMap<>(); + pieces.put(new Position(1, 1), new Cannon(Team.HAN)); + pieces.put(new Position(1, 3), new Soldier(Team.CHO)); + Board board = new Board(pieces); + + board.move(new Position(1, 1), new Position(1, 5)); + + assertThat(board.pieceAt(new Position(1, 5)).isNotEmpty()).isTrue(); + assertThat(board.pieceAt(new Position(1, 1)).isNotEmpty()).isFalse(); + } + + @Test + void 포가_넘을_기물이_없으면_예외() { + Map pieces = new HashMap<>(); + pieces.put(new Position(1, 1), new Cannon(Team.HAN)); + Board board = new Board(pieces); + + assertThatThrownBy(() -> board.move(new Position(1, 1), new Position(1, 5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("포가 넘을 수 있는 기물의 개수는 하나입니다."); + } + + @Test + void 포가_두_개_이상의_기물을_넘으면_예외() { + Map pieces = new HashMap<>(); + pieces.put(new Position(1, 1), new Cannon(Team.HAN)); + pieces.put(new Position(1, 3), new Soldier(Team.CHO)); + pieces.put(new Position(1, 5), new Soldier(Team.CHO)); + Board board = new Board(pieces); + + assertThatThrownBy(() -> board.move(new Position(1, 1), new Position(1, 7))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("포가 넘을 수 있는 기물의 개수는 하나입니다."); + } + + @Test + void 포는_포를_넘지_못한다() { + Map pieces = new HashMap<>(); + pieces.put(new Position(1, 1), new Cannon(Team.HAN)); + pieces.put(new Position(1, 3), new Cannon(Team.CHO)); + Board board = new Board(pieces); + + assertThatThrownBy(() -> board.move(new Position(1, 1), new Position(1, 5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("포는 포를 넘지 못합니다."); + } + + @Test + void 포는_포를_잡을_수_없다() { + Map pieces = new HashMap<>(); + pieces.put(new Position(1, 1), new Cannon(Team.HAN)); + pieces.put(new Position(1, 3), new Soldier(Team.CHO)); + pieces.put(new Position(1, 5), new Cannon(Team.CHO)); + Board board = new Board(pieces); + + assertThatThrownBy(() -> board.move(new Position(1, 1), new Position(1, 5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("포는 포를 잡을 수 없습니다."); + } + + @Test + void 빈_칸에서_이동_시도하면_예외() { + Map pieces = new HashMap<>(); + pieces.put(new Position(1, 1), EmptyPiece.getInstance()); + Board board = new Board(pieces); + + assertThatThrownBy(() -> board.move(new Position(1, 1), new Position(1, 5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 위치입니다."); + } +} diff --git a/src/test/java/domain/game/TurnTest.java b/src/test/java/domain/game/TurnTest.java new file mode 100644 index 0000000000..f656c6e3d8 --- /dev/null +++ b/src/test/java/domain/game/TurnTest.java @@ -0,0 +1,38 @@ +package domain.game; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class TurnTest { + + @Test + void 게임은_초_진영부터_시작한다() { + Turn turn = Turn.first(); + assertThat(turn.isTurn(Team.CHO)).isTrue(); + } + + @Test + void 초_진영_다음_턴은_한_진영이다() { + Turn turn = Turn.first(); + assertThat(turn.next().isTurn(Team.HAN)).isTrue(); + } + + @Test + void 한_진영_다음_턴은_초_진영이다() { + Turn turn = Turn.first().next(); + assertThat(turn.next().isTurn(Team.CHO)).isTrue(); + } + + @Test + void 현재_팀의_턴이면_true를_반환한다() { + Turn turn = Turn.first(); + assertThat(turn.isTurn(Team.CHO)).isTrue(); + } + + @Test + void 현재_팀이_아니면_false를_반환한다() { + Turn turn = Turn.first(); + assertThat(turn.isTurn(Team.HAN)).isFalse(); + } +} diff --git a/src/test/java/domain/position/PositionTest.java b/src/test/java/domain/position/PositionTest.java new file mode 100644 index 0000000000..3a9848c272 --- /dev/null +++ b/src/test/java/domain/position/PositionTest.java @@ -0,0 +1,90 @@ +package domain.position; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class PositionTest { + + @Test + void 경계값_최소_범위의_위치를_생성한다() { + Position position = new Position(1, 1); + + assertThat(position).isNotNull(); + } + + @Test + void 경계값_최대_범위의_위치를_생성한다() { + Position position = new Position(10, 9); + + assertThat(position).isNotNull(); + } + + @Test + void 행이_0이면_예외가_발생한다() { + assertThatThrownBy(() -> new Position(0, 5)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("행의 위치는"); + } + + @Test + void 행이_11이면_예외가_발생한다() { + assertThatThrownBy(() -> new Position(11, 5)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("행의 위치는"); + } + + @Test + void 열이_0이면_예외가_발생한다() { + assertThatThrownBy(() -> new Position(5, 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("열의 위치는"); + } + + @Test + void 열이_11이면_예외가_발생한다() { + assertThatThrownBy(() -> new Position(5, 11)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("열의 위치는"); + } + + @Test + void 행과_열이_모두_음수이면_예외가_발생한다() { + assertThatThrownBy(() -> new Position(-1, -1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("행의 위치는"); + } + + @Test + void 같은_행이면_true를_반환한다() { + Position position = new Position(1, 5); + Position otherPosition = new Position(1, 3); + + assertThat(position.isSameRow(otherPosition)).isTrue(); + } + + @Test + void 다른_행이면_false를_반환한다() { + Position position = new Position(1, 5); + Position otherPosition = new Position(2, 5); + + assertThat(position.isSameRow(otherPosition)).isFalse(); + } + + @Test + void 같은_열이면_true를_반환한다() { + Position position = new Position(1, 5); + Position otherPosition = new Position(3, 5); + + assertThat(position.isSameCol(otherPosition)).isTrue(); + } + + @Test + void 다른_열이면_false를_반환한다() { + Position position = new Position(1, 5); + Position otherPosition = new Position(1, 6); + + assertThat(position.isSameCol(otherPosition)).isFalse(); + } +}