diff --git a/.gitignore b/.gitignore index 6c01878138..8956079db5 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,12 @@ out/ ### VS Code ### .vscode/ + +### macOS ### +.DS_Store +.AppleDouble +.LSOverride +Icon? +._* +.Spotlight-V100 +.Trashes diff --git a/README.md b/README.md index 9775dda0ae..4512987c62 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,75 @@ # java-janggi 장기 미션 저장소 + +### 기능 요구사항 체크리스트 + +#### step 1 +- [x] 10x9 장기판 좌표를 `Point(y, x)`로 표현하고, 범위를 벗어난 좌표 생성 시 예외를 발생시킨다. +- [x] 기물 타입(`PieceType`)과 진영(`Team`)을 분리해 도메인 모델링한다. +- [x] 빈 칸을 `NonePiece`로 포장해 `Intersection.empty()`로 관리한다. +- [x] 장기판 초기화 시 전체 좌표를 빈 칸으로 채운 뒤, 생성기(`IntersectionGenerator`)가 전달한 기물을 배치한다. +- [x] 초/한 기본 배치(졸, 포, 장군, 사, 차)를 자동 생성한다. +- [x] 상/마 차림(`Formation`) 4가지를 지원하고, 진영별로 대칭 배치한다. +- [x] `Formation.valueOf(int)`로 입력 숫자를 차림 enum으로 변환한다. +- [x] 보드에서 좌표로 교차점(`Intersection`)을 조회할 수 있다. +- [x] 이동 규칙을 `MoveRule` 전략으로 분리하고, 기물 타입별 규칙 클래스를 연결한다. +- [x] 이동 경로를 `Direction`/`Directions`로 추상화해 “도착 가능 여부 + 경유 좌표”를 계산한다. +- [x] 차(`Chariot`)는 상하좌우 직선 이동만 가능하며, 경로 중간 장애물이 있으면 이동할 수 없다. +- [x] 마(`Horse`)는 2단계 경로(멱) 중 중간 칸이 막히면 이동할 수 없다. +- [x] 상(`Elephant`)은 3단계 경로 중 중간 칸이 막히면 이동할 수 없다. +- [x] 포(`Cannon`)는 정확히 1개의 장애물을 넘어야 하며, 포를 넘거나 포를 공격할 수 없다. +- [x] 장군(`General`)과 사(`Guard`)는 1칸 상하좌우 이동을 지원한다. +- [x] 졸(`Soldier`)은 좌/우 + 전진 이동을 지원하며, 진영에 따라 전진 방향이 다르다. +- [x] 같은 팀 기물이 있는 칸으로는 이동할 수 없다. +- [x] `JanggiBoard.tryToMove()` 수행 시 도착지는 출발 기물로 갱신되고, 출발지는 빈 칸이 된다. + +### 주요 로직 요약 + +#### step 1 +- **보드 생성** + - `JanggiBoard`가 10x9 전체 좌표를 `Intersection.empty()`로 초기화한다. +- **초기 기물 배치** + - `JanggiGenerator`가 진영(`CHO`, `HAN`)별 기본 행/열 규칙으로 기물을 만든다. + - 상/마는 `Formation`에 정의된 열 인덱스 조합으로 배치한다. +- **좌표/교차점 모델링** + - `Point`는 생성 시 범위 검증을 수행하고, `next(Vector)`로 이동 좌표를 계산한다. + - `Intersection`은 기물 도착(`arrive`)과 이탈(`leave`) 책임을 가진다. +- **이동 규칙 선택** + - 보드는 출발 지점의 기물 타입을 기준으로 `MoveRule` 구현체를 선택한다. +- **경로 계산** + - 규칙별 `Directions.findPoints(from, to)`로 목적지까지의 경유 좌표 목록을 만든다. +- **규칙 검증** + - 공통적으로 같은 팀 도착지 여부를 검증한다. + - 차/마/상은 경로 장애물 조건을 검증한다. + - 포는 “장애물 1개 필수”, “포 넘기/포 공격 금지”를 추가 검증한다. +- **실제 이동 반영** + - 검증 통과 시 도착지 `arrive(from)`, 출발지 `leave()` 순서로 상태를 갱신한다. + +### 기물별 이동 규칙 정리 + +- **차(`Chariot`)** + - 상/하/좌/우 직선 다칸 이동 + - 도착지 아군 금지 + - 경로 중간 장애물 금지 +- **포(`Cannon`)** + - 상/하/좌/우 직선 이동 + - 중간 장애물 정확히 1개 필요 + - 장애물/도착지가 포인 경우 금지 + - 도착지 아군 금지 +- **마(`Horse`)** + - 1칸 직선 + 1칸 대각(총 2스텝) + - 중간 경유 칸 장애물 금지 + - 도착지 아군 금지 +- **상(`Elephant`)** + - 1칸 직선 + 2칸 대각(총 3스텝) + - 경유 칸 장애물 금지 + - 도착지 아군 금지 +- **장군/사(`General`/`Guard`)** + - 1칸 상하좌우 이동 + - 도착지 아군 금지 + - (궁성 내부/대각 특수 규칙 미반영) +- **졸(`Soldier`)** + - 좌/우 + 전진 이동 + - CHO는 위쪽(UP), HAN은 아래쪽(DOWN) 전진 + - 도착지 아군 금지 diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 0000000000..6fd58da8ae --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,16 @@ +import controller.JanggiController; +import view.InputView; +import view.OutputView; + +public class Application { + + public static void main(String[] args) { + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + + JanggiController janggiController = new JanggiController(inputView, outputView); + janggiController.run(); + + } + +} diff --git a/src/main/java/controller/JanggiController.java b/src/main/java/controller/JanggiController.java new file mode 100644 index 0000000000..8e890b77bf --- /dev/null +++ b/src/main/java/controller/JanggiController.java @@ -0,0 +1,41 @@ +package controller; + +import domain.board.Formation; +import domain.board.JanggiBoard; +import domain.board.JanggiGenerator; +import domain.game.Game; +import domain.team.Team; +import dto.MoveDTO; +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() { + Formation hanFormation = Formation.valueOf(inputView.inputHanWingSetup()); + Formation choFormation = Formation.valueOf(inputView.inputChoWingSetup()); + JanggiGenerator janggiGenerator = new JanggiGenerator(hanFormation, choFormation); + + Game game = new Game(new JanggiBoard(janggiGenerator)); + + while (true) { + try { + outputView.printCurrentBoardStatus(game.boardStatus()); + final Team turn = game.currentTurn(); + outputView.printCurrentTurn(turn); + MoveDTO move = new MoveDTO(inputView.inputMovePiecePoint(), inputView.inputDestinationPoint()); + game.processTurn(move); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + } + } + } +} diff --git a/src/main/java/domain/board/Formation.java b/src/main/java/domain/board/Formation.java new file mode 100644 index 0000000000..dc72101dd3 --- /dev/null +++ b/src/main/java/domain/board/Formation.java @@ -0,0 +1,39 @@ +package domain.board; + +import java.util.Arrays; +import java.util.List; + +public enum Formation { + + HORSE_ELEPHANT_HORSE_ELEPHANT(1, List.of(2, 7), List.of(1, 6)), + HORSE_ELEPHANT_ELEPHANT_HORSE(2, List.of(2, 6), List.of(1, 7)), + ELEPHANT_HORSE_ELEPHANT_HORSE(3, List.of(1, 6), List.of(2, 7)), + ELEPHANT_HORSE_HORSE_ELEPHANT(4, List.of(1, 7), List.of(2, 6)), + ; + + private final int formatNumber; + private final List elephantFormations; + private final List horseFormations; + + Formation(int formatNumber, List elephantFormations, List horseFormations) { + this.formatNumber = formatNumber; + this.elephantFormations = elephantFormations; + this.horseFormations = horseFormations; + } + + public static Formation valueOf(int formatNumber) { + return Arrays.stream(Formation.values()) + .filter(formation -> formation.formatNumber == formatNumber) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + } + + public List elephantFormations() { + return elephantFormations; + } + + public List horseFormations() { + return horseFormations; + } + +} diff --git a/src/main/java/domain/board/IntersectionGenerator.java b/src/main/java/domain/board/IntersectionGenerator.java new file mode 100644 index 0000000000..b6ea857e38 --- /dev/null +++ b/src/main/java/domain/board/IntersectionGenerator.java @@ -0,0 +1,10 @@ +package domain.board; + +import domain.intersection.Intersection; +import java.util.List; + +public interface IntersectionGenerator { + + List makeIntersection(); + +} diff --git a/src/main/java/domain/board/JanggiBoard.java b/src/main/java/domain/board/JanggiBoard.java new file mode 100644 index 0000000000..431c74c35d --- /dev/null +++ b/src/main/java/domain/board/JanggiBoard.java @@ -0,0 +1,104 @@ +package domain.board; + +import domain.intersection.Intersection; +import domain.piece.move.CannonMoveRule; +import domain.piece.move.ChariotMoveRule; +import domain.piece.move.ElephantMoveRule; +import domain.piece.move.GeneralMoveRule; +import domain.piece.move.GuardMoveRule; +import domain.piece.move.HorseMoveRule; +import domain.piece.move.MoveRule; +import domain.piece.move.SoliderMoveRule; +import domain.point.Point; +import dto.BoardStatusDTO; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public class JanggiBoard { + + private static final int MAX_ROW = 10; + private static final int MAX_FILE = 9; + + private final Map intersections; + private final List moveRules; + + public JanggiBoard(IntersectionGenerator intersectionGenerator) { + this.intersections = fillEmptyIntersections(); + this.moveRules = setMoveRules(); + for (Intersection intersection : intersectionGenerator.makeIntersection()) { + intersections.put(intersection.getPoint(), intersection); + } + } + + private static Stream getAllPoints() { + return range(MAX_ROW).boxed() + .flatMap(row -> range(MAX_FILE).mapToObj(f -> new Point(row, f))); + } + + private static IntStream range(int maxRange) { + return IntStream.range(0, maxRange); + } + + public void tryToMove(Point start, Point end) { + Intersection from = findIntersection(start); + Intersection to = findIntersection(end); + + validateMoveRule(from, to); + move(to, from); + } + + private void move(Intersection to, Intersection from) { + to.arrive(from); + from.leave(); + } + + private void validateMoveRule(Intersection from, Intersection to) { + MoveRule moveRule = findMoveRule(from); + List possiblePoints = moveRule.findPossiblePoints(from, to); + List path = findPath(possiblePoints); + moveRule.checkMoveRule(from, path); + } + + public MoveRule findMoveRule(Intersection from) { + return moveRules.stream() + .filter(moveRule -> moveRule.support(from)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("선택한 좌표에 이동 가능한 기물이 없습니다.")); + } + + private List findPath(List possiblePoints) { + return possiblePoints.stream() + .map(this::findIntersection) + .toList(); + } + + public BoardStatusDTO boardStatus() { + return new BoardStatusDTO(intersections); + } + + public Intersection findIntersection(Point point) { + return intersections.get(point); + } + + private List setMoveRules() { + return List.of( + new ChariotMoveRule(), + new GeneralMoveRule(), + new GuardMoveRule(), + new ElephantMoveRule(), + new SoliderMoveRule(), + new CannonMoveRule(), + new HorseMoveRule() + ); + } + + private Map fillEmptyIntersections() { + return getAllPoints() + .collect(Collectors.toMap(point -> point, Intersection::empty)); + } + + +} diff --git a/src/main/java/domain/board/JanggiGenerator.java b/src/main/java/domain/board/JanggiGenerator.java new file mode 100644 index 0000000000..54a36a1588 --- /dev/null +++ b/src/main/java/domain/board/JanggiGenerator.java @@ -0,0 +1,103 @@ +package domain.board; + +import domain.intersection.Intersection; +import domain.piece.Cannon; +import domain.piece.Chariot; +import domain.piece.Elephant; +import domain.piece.General; +import domain.piece.Guard; +import domain.piece.Horse; +import domain.piece.Piece; +import domain.piece.Soldier; +import domain.point.Point; +import domain.team.Team; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class JanggiGenerator implements IntersectionGenerator { + + public static final int MAX_ROW = 9; + public static final int DEFAULT_SOLDIER_ROW = 3; + public static final int DEFAULT_CANNON_ROW = 2; + public static final int DEFAULT_GENERAL_ROW = 1; + public static final int DEFAULT_BACK_ROW = 0; + + private static final List DEFAULT_SOLDIER_FILES = List.of(0, 2, 4, 6, 8); + private static final List DEFAULT_CANNON_FILES = List.of(1, 7); + private static final List DEFAULT_GENERAL_FILES = List.of(4); + private static final List DEFAULT_GUARD_FILES = List.of(3, 5); + private static final List DEFAULT_CHARIOT_FILES = List.of(0, 8); + + private final Formation hanFormation; + private final Formation choFormation; + + public JanggiGenerator(Formation hanFormation, Formation choFormation) { + this.hanFormation = hanFormation; + this.choFormation = choFormation; + } + + public List makeIntersection() { + return Stream.of(Team.values()) + .flatMap(team -> Stream.of( + createDefaultSoldierIntersectionV2(team), + createDefaultCannonIntersectionV2(team), + createDefaultGeneralIntersectionV2(team), + createDefaultGuardIntersectionV2(team), + createDefaultChariotIntersectionV2(team), + createElephantAndHorseByFormation(team, getFormationByTeam(team)) + )) + .flatMap(List::stream) + .collect(Collectors.toCollection(ArrayList::new)); + } + + private List createDefaultSoldierIntersectionV2(Team team) { + return createIntersections(getRow(team, DEFAULT_SOLDIER_ROW), DEFAULT_SOLDIER_FILES, new Soldier(team)); + } + + private List createDefaultCannonIntersectionV2(Team team) { + return createIntersections(getRow(team, DEFAULT_CANNON_ROW), DEFAULT_CANNON_FILES, new Cannon(team)); + } + + private List createDefaultGeneralIntersectionV2(Team team) { + return createIntersections(getRow(team, DEFAULT_GENERAL_ROW), DEFAULT_GENERAL_FILES, new General(team)); + } + + private List createDefaultGuardIntersectionV2(Team team) { + return createIntersections(getRow(team, DEFAULT_BACK_ROW), DEFAULT_GUARD_FILES, new Guard(team)); + } + + private List createDefaultChariotIntersectionV2(Team team) { + return createIntersections(getRow(team, DEFAULT_BACK_ROW), DEFAULT_CHARIOT_FILES, new Chariot(team)); + } + + public List createElephantAndHorseByFormation(Team team, Formation formation) { + int row = getRow(team, DEFAULT_BACK_ROW); + return Stream.concat( + createIntersections(row, formation.elephantFormations(), new Elephant(team)).stream(), + createIntersections(row, formation.horseFormations(), new Horse(team)).stream() + ).collect(Collectors.toCollection(ArrayList::new)); + } + + private List createIntersections(int row, List files, Piece piece) { + return files.stream() + .map(file -> new Intersection(new Point(row, file), piece)) + .toList(); + } + + private Formation getFormationByTeam(Team team) { + if (team == Team.HAN) { + return hanFormation; + } + return choFormation; + } + + private int getRow(Team team, int row) { + if (team == Team.CHO) { + return MAX_ROW - row; + } + return row; + } + +} diff --git a/src/main/java/domain/game/Game.java b/src/main/java/domain/game/Game.java new file mode 100644 index 0000000000..db434ee8a0 --- /dev/null +++ b/src/main/java/domain/game/Game.java @@ -0,0 +1,35 @@ +package domain.game; + +import domain.board.JanggiBoard; +import domain.point.Point; +import domain.team.Team; +import dto.BoardStatusDTO; +import dto.MoveDTO; + +public class Game { + + private final JanggiBoard janggiBoard; + private Team turn; + + public Game(JanggiBoard janggiBoard) { + this.janggiBoard = janggiBoard; + this.turn = Team.CHO; + } + + public void processTurn(MoveDTO move) { + Point from = move.getFrom(); + Point to = move.getTo(); + janggiBoard.tryToMove(from, to); + + turn = turn.nextTurn(); + } + + public Team currentTurn() { + return turn; + } + + public BoardStatusDTO boardStatus() { + return janggiBoard.boardStatus(); + } + +} diff --git a/src/main/java/domain/intersection/Intersection.java b/src/main/java/domain/intersection/Intersection.java new file mode 100644 index 0000000000..c6dc99c062 --- /dev/null +++ b/src/main/java/domain/intersection/Intersection.java @@ -0,0 +1,73 @@ +package domain.intersection; + +import domain.piece.NonePiece; +import domain.piece.Piece; +import domain.piece.PieceType; +import domain.point.Point; +import java.util.Objects; + +public class Intersection { + + private final Point point; + private Piece piece; + + public Intersection(Point point, Piece piece) { + this.point = point; + this.piece = piece; + } + + public static Intersection empty(Point point) { + return new Intersection(point, new NonePiece()); + } + + public void arrive(Intersection from) { + this.piece = from.piece; + } + + public void leave() { + piece = new NonePiece(); + } + + public Point getPoint() { + return point; + } + + public boolean isSamePiece(Intersection intersection) { + return this.piece.equals(intersection.piece); + } + + public boolean isSamePiece(PieceType pieceType) { + return this.piece.isSamePiece(pieceType); + } + + public boolean isSameTeam(Intersection to) { + return piece.isSameTeam(to.piece); + } + + public boolean hasPiece() { + return piece.hasPiece(); + } + + public boolean isChoIntersection() { + return piece.isCho(); + } + + public String getChineseCharacter() { + return piece.getChineseCharacter(); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Intersection that = (Intersection) o; + return Objects.equals(point, that.point) && Objects.equals(piece, that.piece); + } + + @Override + public int hashCode() { + return Objects.hash(point, piece); + } + +} diff --git a/src/main/java/domain/piece/Cannon.java b/src/main/java/domain/piece/Cannon.java new file mode 100644 index 0000000000..6a0b2507c5 --- /dev/null +++ b/src/main/java/domain/piece/Cannon.java @@ -0,0 +1,11 @@ +package domain.piece; + +import domain.team.Team; + +public class Cannon extends Piece { + + public Cannon(Team team) { + super(team, PieceType.CANNON); + } + +} diff --git a/src/main/java/domain/piece/Chariot.java b/src/main/java/domain/piece/Chariot.java new file mode 100644 index 0000000000..6272ef1098 --- /dev/null +++ b/src/main/java/domain/piece/Chariot.java @@ -0,0 +1,11 @@ +package domain.piece; + +import domain.team.Team; + +public class Chariot extends Piece { + + public Chariot(Team team) { + super(team, PieceType.CHARIOT); + } + +} diff --git a/src/main/java/domain/piece/Elephant.java b/src/main/java/domain/piece/Elephant.java new file mode 100644 index 0000000000..b028102488 --- /dev/null +++ b/src/main/java/domain/piece/Elephant.java @@ -0,0 +1,11 @@ +package domain.piece; + +import domain.team.Team; + +public class Elephant extends Piece { + + public Elephant(Team team) { + super(team, PieceType.ELEPHANT); + } + +} diff --git a/src/main/java/domain/piece/General.java b/src/main/java/domain/piece/General.java new file mode 100644 index 0000000000..d6b8f2a167 --- /dev/null +++ b/src/main/java/domain/piece/General.java @@ -0,0 +1,11 @@ +package domain.piece; + +import domain.team.Team; + +public class General extends Piece { + + public General(Team team) { + super(team, PieceType.GENERAL); + } + +} diff --git a/src/main/java/domain/piece/Guard.java b/src/main/java/domain/piece/Guard.java new file mode 100644 index 0000000000..8af7e6d09e --- /dev/null +++ b/src/main/java/domain/piece/Guard.java @@ -0,0 +1,11 @@ +package domain.piece; + +import domain.team.Team; + +public class Guard extends Piece { + + public Guard(Team team) { + super(team, PieceType.GUARD); + } + +} diff --git a/src/main/java/domain/piece/Horse.java b/src/main/java/domain/piece/Horse.java new file mode 100644 index 0000000000..3f71347530 --- /dev/null +++ b/src/main/java/domain/piece/Horse.java @@ -0,0 +1,11 @@ +package domain.piece; + +import domain.team.Team; + +public class Horse extends Piece { + + public Horse(Team team) { + super(team, PieceType.HORSE); + } + +} diff --git a/src/main/java/domain/piece/NonePiece.java b/src/main/java/domain/piece/NonePiece.java new file mode 100644 index 0000000000..f32b5d30bf --- /dev/null +++ b/src/main/java/domain/piece/NonePiece.java @@ -0,0 +1,9 @@ +package domain.piece; + +public class NonePiece extends Piece { + + public NonePiece() { + super(PieceType.NONE); + } + +} diff --git a/src/main/java/domain/piece/Piece.java b/src/main/java/domain/piece/Piece.java new file mode 100644 index 0000000000..d12bafb55d --- /dev/null +++ b/src/main/java/domain/piece/Piece.java @@ -0,0 +1,55 @@ +package domain.piece; + +import domain.team.Team; +import java.util.Objects; + +public abstract class Piece { + + protected final Team team; + protected final PieceType pieceType; + + protected Piece(Team team, PieceType pieceType) { + this.team = team; + this.pieceType = pieceType; + } + + protected Piece(PieceType pieceType) { + this.team = null; + this.pieceType = pieceType; + } + + public boolean isSameTeam(Piece other) { + return this.team == other.team; + } + + public boolean isCho() { + return this.team == Team.CHO; + } + + public boolean isSamePiece(PieceType pieceType) { + return this.pieceType == pieceType; + } + + public boolean hasPiece() { + return this.pieceType != PieceType.NONE; + } + + public String getChineseCharacter() { + return pieceType.getChineseCharacter(team); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Piece piece = (Piece) o; + return pieceType == piece.pieceType; + } + + @Override + public int hashCode() { + return Objects.hashCode(pieceType); + } + +} diff --git a/src/main/java/domain/piece/PieceType.java b/src/main/java/domain/piece/PieceType.java new file mode 100644 index 0000000000..f7608a6333 --- /dev/null +++ b/src/main/java/domain/piece/PieceType.java @@ -0,0 +1,32 @@ +package domain.piece; + +import domain.team.Team; + +public enum PieceType { + + GENERAL("楚", "漢"), + CHARIOT("車", "車"), + CANNON("包", "包"), + HORSE("馬", "馬"), + ELEPHANT("象", "象"), + GUARD("士", "士"), + SOLDIER("卒", "兵"), + NONE("+", "+"), + ; + + private final String choChineseCharacter; + private final String hanChineseCharacter; + + PieceType(String choChineseCharacter, String hanChineseCharacter) { + this.choChineseCharacter = choChineseCharacter; + this.hanChineseCharacter = hanChineseCharacter; + } + + public String getChineseCharacter(Team team) { + if (team == Team.CHO) { + return choChineseCharacter; + } + return hanChineseCharacter; + } + +} diff --git a/src/main/java/domain/piece/Soldier.java b/src/main/java/domain/piece/Soldier.java new file mode 100644 index 0000000000..47e0c8ea28 --- /dev/null +++ b/src/main/java/domain/piece/Soldier.java @@ -0,0 +1,11 @@ +package domain.piece; + +import domain.team.Team; + +public class Soldier extends Piece { + + public Soldier(Team team) { + super(team, PieceType.SOLDIER); + } + +} diff --git a/src/main/java/domain/piece/move/CannonMoveRule.java b/src/main/java/domain/piece/move/CannonMoveRule.java new file mode 100644 index 0000000000..f3b3b1c835 --- /dev/null +++ b/src/main/java/domain/piece/move/CannonMoveRule.java @@ -0,0 +1,110 @@ +package domain.piece.move; + +import static domain.piece.move.Vector.DOWN; +import static domain.piece.move.Vector.LEFT; +import static domain.piece.move.Vector.RIGHT; +import static domain.piece.move.Vector.UP; + +import domain.intersection.Intersection; +import domain.piece.PieceType; +import domain.point.Point; +import java.util.List; + +public class CannonMoveRule extends MoveRule { + + public CannonMoveRule() { + super(PieceType.CANNON, initializeDirections()); + } + + private static void validateObstacleIsNotCannon(Intersection from, List list) { + if (list.getFirst().isSamePiece(from)) { + throw new IllegalArgumentException("포는 포를 넘어갈 수 없습니다."); + } + } + + public static Directions initializeDirections() { + return new Directions(List.of( + new Direction(List.of(UP)), + new Direction(List.of(UP, UP)), + new Direction(List.of(UP, UP, UP)), + new Direction(List.of(UP, UP, UP, UP)), + new Direction(List.of(UP, UP, UP, UP, UP)), + new Direction(List.of(UP, UP, UP, UP, UP, UP)), + new Direction(List.of(UP, UP, UP, UP, UP, UP, UP)), + new Direction(List.of(UP, UP, UP, UP, UP, UP, UP, UP)), + new Direction(List.of(UP, UP, UP, UP, UP, UP, UP, UP, UP)), + + new Direction(List.of(DOWN)), + new Direction(List.of(DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN, DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN, DOWN, DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN)), + + new Direction(List.of(RIGHT)), + new Direction(List.of(RIGHT, RIGHT)), + new Direction(List.of(RIGHT, RIGHT, RIGHT)), + new Direction(List.of(RIGHT, RIGHT, RIGHT, RIGHT)), + new Direction(List.of(RIGHT, RIGHT, RIGHT, RIGHT, RIGHT)), + new Direction(List.of(RIGHT, RIGHT, RIGHT, RIGHT, RIGHT, RIGHT)), + new Direction(List.of(RIGHT, RIGHT, RIGHT, RIGHT, RIGHT, RIGHT, RIGHT)), + new Direction(List.of(RIGHT, RIGHT, RIGHT, RIGHT, RIGHT, RIGHT, RIGHT, RIGHT)), + + new Direction(List.of(LEFT)), + new Direction(List.of(LEFT, LEFT)), + new Direction(List.of(LEFT, LEFT, LEFT)), + new Direction(List.of(LEFT, LEFT, LEFT, LEFT)), + new Direction(List.of(LEFT, LEFT, LEFT, LEFT, LEFT)), + new Direction(List.of(LEFT, LEFT, LEFT, LEFT, LEFT, LEFT)), + new Direction(List.of(LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT)), + new Direction(List.of(LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT)) + )); + } + + public boolean support(Intersection from) { + return from.isSamePiece(pieceType); + } + + public List findPossiblePoints(Intersection from, Intersection to) { + return directions.findPoints(from.getPoint(), to.getPoint()); + } + + public boolean checkMoveRule(Intersection from, List path) { + Intersection to = path.getLast(); + validateIsSameTeam(from, to); + validateObstacleCondition(from, path); + validateDestinationIsNotCannon(from, to); + return true; + } + + private void validateIsSameTeam(Intersection from, Intersection to) { + if (from.isSameTeam(to)) { + throw new IllegalArgumentException("같은 팀의 위치로 이동할 수 없습니다."); + } + } + + private void validateObstacleCondition(Intersection from, List path) { + List obstacles = path.subList(0, path.size() - 1).stream() + .filter(Intersection::hasPiece) + .toList(); + + validateObstacleIsOnly(obstacles); + validateObstacleIsNotCannon(from, obstacles); + } + + private void validateObstacleIsOnly(List list) { + if (list.size() != 1) { + throw new IllegalArgumentException("포는 반드시 기물 하나를 넘어야 합니다."); + } + } + + private void validateDestinationIsNotCannon(Intersection from, Intersection to) { + if (from.isSamePiece(to)) { + throw new IllegalArgumentException("포는 포를 공격할 수 없습니다."); + } + } + +} diff --git a/src/main/java/domain/piece/move/ChariotMoveRule.java b/src/main/java/domain/piece/move/ChariotMoveRule.java new file mode 100644 index 0000000000..043a159c81 --- /dev/null +++ b/src/main/java/domain/piece/move/ChariotMoveRule.java @@ -0,0 +1,93 @@ +package domain.piece.move; + +import static domain.piece.move.Vector.DOWN; +import static domain.piece.move.Vector.LEFT; +import static domain.piece.move.Vector.RIGHT; +import static domain.piece.move.Vector.UP; + +import domain.intersection.Intersection; +import domain.piece.PieceType; +import domain.point.Point; +import java.util.List; + +public class ChariotMoveRule extends MoveRule { + + public ChariotMoveRule() { + super(PieceType.CHARIOT, initializeDirections()); + } + + public static Directions initializeDirections() { + return new Directions(List.of( + new Direction(List.of(UP)), + new Direction(List.of(UP, UP)), + new Direction(List.of(UP, UP, UP)), + new Direction(List.of(UP, UP, UP, UP)), + new Direction(List.of(UP, UP, UP, UP, UP)), + new Direction(List.of(UP, UP, UP, UP, UP, UP)), + new Direction(List.of(UP, UP, UP, UP, UP, UP, UP)), + new Direction(List.of(UP, UP, UP, UP, UP, UP, UP, UP)), + new Direction(List.of(UP, UP, UP, UP, UP, UP, UP, UP, UP)), + + new Direction(List.of(DOWN)), + new Direction(List.of(DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN, DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN, DOWN, DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN)), + new Direction(List.of(DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN)), + + new Direction(List.of(RIGHT)), + new Direction(List.of(RIGHT, RIGHT)), + new Direction(List.of(RIGHT, RIGHT, RIGHT)), + new Direction(List.of(RIGHT, RIGHT, RIGHT, RIGHT)), + new Direction(List.of(RIGHT, RIGHT, RIGHT, RIGHT, RIGHT)), + new Direction(List.of(RIGHT, RIGHT, RIGHT, RIGHT, RIGHT, RIGHT)), + new Direction(List.of(RIGHT, RIGHT, RIGHT, RIGHT, RIGHT, RIGHT, RIGHT)), + new Direction(List.of(RIGHT, RIGHT, RIGHT, RIGHT, RIGHT, RIGHT, RIGHT, RIGHT)), + + new Direction(List.of(LEFT)), + new Direction(List.of(LEFT, LEFT)), + new Direction(List.of(LEFT, LEFT, LEFT)), + new Direction(List.of(LEFT, LEFT, LEFT, LEFT)), + new Direction(List.of(LEFT, LEFT, LEFT, LEFT, LEFT)), + new Direction(List.of(LEFT, LEFT, LEFT, LEFT, LEFT, LEFT)), + new Direction(List.of(LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT)), + new Direction(List.of(LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT)) + )); + } + + public boolean support(Intersection from) { + return from.isSamePiece(pieceType); + } + + public List findPossiblePoints(Intersection from, Intersection to) { + return directions.findPoints(from.getPoint(), to.getPoint()); + } + + public boolean checkMoveRule(Intersection from, List path) { + Intersection to = path.getLast(); + validateIsSameTeam(from, to); + validateObstacleCondition(path); + return true; + } + + private void validateIsSameTeam(Intersection from, Intersection to) { + if (from.isSameTeam(to)) { + throw new IllegalArgumentException("같은 팀의 위치로 이동할 수 없습니다."); + } + } + + private void validateObstacleCondition(List path) { + List routeWithoutTarget = path.subList(0, path.size() - 1); + + boolean hasObstacle = routeWithoutTarget.stream() + .anyMatch(Intersection::hasPiece); + + if (hasObstacle) { + throw new IllegalArgumentException("이동 경로에 다른 기물이 있어 통과할 수 없습니다."); + } + } + +} diff --git a/src/main/java/domain/piece/move/Direction.java b/src/main/java/domain/piece/move/Direction.java new file mode 100644 index 0000000000..ef6142635e --- /dev/null +++ b/src/main/java/domain/piece/move/Direction.java @@ -0,0 +1,36 @@ +package domain.piece.move; + +import domain.point.Point; +import java.util.ArrayList; +import java.util.List; + +public class Direction { + + List vectors; + + public Direction(List vectors) { + this.vectors = vectors; + } + + public boolean canReach(Point start, Point target) { + int dy = vectors.stream().mapToInt(Vector::dy).sum(); + int dx = vectors.stream().mapToInt(Vector::dx).sum(); + + if (start.canMake(dy, dx)) { + Point destination = start.movePoint(dy, dx); + return destination.equals(target); + } + + return false; + } + + public List getPoints(Point current) { + List points = new ArrayList<>(); + for (Vector vector : this.vectors) { + current = current.next(vector); + points.add(current); + } + return points; + } + +} diff --git a/src/main/java/domain/piece/move/Directions.java b/src/main/java/domain/piece/move/Directions.java new file mode 100644 index 0000000000..817bc745cc --- /dev/null +++ b/src/main/java/domain/piece/move/Directions.java @@ -0,0 +1,23 @@ +package domain.piece.move; + +import domain.point.Point; +import java.util.List; + +public class Directions { + + private final List directions; + + public Directions(List directions) { + this.directions = directions; + } + + public List findPoints(Point from, Point to) { + for (Direction direction : directions) { + if (direction.canReach(from, to)) { + return direction.getPoints(from); + } + } + throw new IllegalArgumentException(); + } + +} diff --git a/src/main/java/domain/piece/move/ElephantMoveRule.java b/src/main/java/domain/piece/move/ElephantMoveRule.java new file mode 100644 index 0000000000..0bd0b90e28 --- /dev/null +++ b/src/main/java/domain/piece/move/ElephantMoveRule.java @@ -0,0 +1,68 @@ +package domain.piece.move; + +import static domain.piece.move.Vector.DOWN; +import static domain.piece.move.Vector.LEFT; +import static domain.piece.move.Vector.LEFT_DOWN; +import static domain.piece.move.Vector.LEFT_UP; +import static domain.piece.move.Vector.RIGHT; +import static domain.piece.move.Vector.RIGHT_DOWN; +import static domain.piece.move.Vector.RIGHT_UP; +import static domain.piece.move.Vector.UP; + +import domain.intersection.Intersection; +import domain.piece.PieceType; +import domain.point.Point; +import java.util.List; + +public class ElephantMoveRule extends MoveRule { + + public ElephantMoveRule() { + super(PieceType.ELEPHANT, initializeDirections()); + } + + public static Directions initializeDirections() { + return new Directions(List.of( + new Direction(List.of(UP, LEFT_UP, LEFT_UP)), + new Direction(List.of(UP, RIGHT_UP, RIGHT_UP)), + new Direction(List.of(DOWN, LEFT_DOWN, LEFT_DOWN)), + new Direction(List.of(DOWN, RIGHT_DOWN, RIGHT_DOWN)), + new Direction(List.of(LEFT, LEFT_UP, LEFT_UP)), + new Direction(List.of(LEFT, LEFT_DOWN, LEFT_DOWN)), + new Direction(List.of(RIGHT, RIGHT_UP, RIGHT_UP)), + new Direction(List.of(RIGHT, RIGHT_DOWN, RIGHT_DOWN)) + )); + } + + public boolean support(Intersection from) { + return from.isSamePiece(pieceType); + } + + public List findPossiblePoints(Intersection from, Intersection to) { + return directions.findPoints(from.getPoint(), to.getPoint()); + } + + public boolean checkMoveRule(Intersection from, List path) { + Intersection to = path.getLast(); + validateIsSameTeam(from, to); + validateObstacleCondition(path); + return true; + } + + private void validateIsSameTeam(Intersection from, Intersection to) { + if (from.isSameTeam(to)) { + throw new IllegalArgumentException("같은 팀의 위치로 이동할 수 없습니다."); + } + } + + private void validateObstacleCondition(List path) { + List routeWithoutTarget = path.subList(0, path.size() - 1); + + boolean hasObstacle = routeWithoutTarget.stream() + .anyMatch(Intersection::hasPiece); + + if (hasObstacle) { + throw new IllegalArgumentException("이동 경로에 다른 기물이 있어 통과할 수 없습니다."); + } + } + +} diff --git a/src/main/java/domain/piece/move/GeneralMoveRule.java b/src/main/java/domain/piece/move/GeneralMoveRule.java new file mode 100644 index 0000000000..102cf63993 --- /dev/null +++ b/src/main/java/domain/piece/move/GeneralMoveRule.java @@ -0,0 +1,48 @@ +package domain.piece.move; + +import static domain.piece.move.Vector.DOWN; +import static domain.piece.move.Vector.LEFT; +import static domain.piece.move.Vector.RIGHT; +import static domain.piece.move.Vector.UP; + +import domain.intersection.Intersection; +import domain.piece.PieceType; +import domain.point.Point; +import java.util.List; + +public class GeneralMoveRule extends MoveRule { + + public GeneralMoveRule() { + super(PieceType.GENERAL, initializeDirections()); + } + + public static Directions initializeDirections() { + return new Directions(List.of( + new Direction(List.of(UP)), + new Direction(List.of(DOWN)), + new Direction(List.of(RIGHT)), + new Direction(List.of(LEFT))) + ); + } + + public boolean support(Intersection from) { + return from.isSamePiece(pieceType); + } + + public List findPossiblePoints(Intersection from, Intersection to) { + return directions.findPoints(from.getPoint(), to.getPoint()); + } + + public boolean checkMoveRule(Intersection from, List path) { + Intersection to = path.getLast(); + validateIsSameTeam(from, to); + return true; + } + + private void validateIsSameTeam(Intersection from, Intersection to) { + if (from.isSameTeam(to)) { + throw new IllegalArgumentException("같은 팀의 위치로 이동할 수 없습니다."); + } + } + +} diff --git a/src/main/java/domain/piece/move/GuardMoveRule.java b/src/main/java/domain/piece/move/GuardMoveRule.java new file mode 100644 index 0000000000..9a5bff42c5 --- /dev/null +++ b/src/main/java/domain/piece/move/GuardMoveRule.java @@ -0,0 +1,48 @@ +package domain.piece.move; + +import static domain.piece.move.Vector.DOWN; +import static domain.piece.move.Vector.LEFT; +import static domain.piece.move.Vector.RIGHT; +import static domain.piece.move.Vector.UP; + +import domain.intersection.Intersection; +import domain.piece.PieceType; +import domain.point.Point; +import java.util.List; + +public class GuardMoveRule extends MoveRule { + + public GuardMoveRule() { + super(PieceType.GUARD, initializeDirections()); + } + + public static Directions initializeDirections() { + return new Directions(List.of( + new Direction(List.of(UP)), + new Direction(List.of(DOWN)), + new Direction(List.of(RIGHT)), + new Direction(List.of(LEFT))) + ); + } + + public boolean support(Intersection from) { + return from.isSamePiece(pieceType); + } + + public List findPossiblePoints(Intersection from, Intersection to) { + return directions.findPoints(from.getPoint(), to.getPoint()); + } + + public boolean checkMoveRule(Intersection from, List path) { + Intersection to = path.getLast(); + validateIsSameTeam(from, to); + return true; + } + + private void validateIsSameTeam(Intersection from, Intersection to) { + if (from.isSameTeam(to)) { + throw new IllegalArgumentException("같은 팀의 위치로 이동할 수 없습니다."); + } + } + +} diff --git a/src/main/java/domain/piece/move/HorseMoveRule.java b/src/main/java/domain/piece/move/HorseMoveRule.java new file mode 100644 index 0000000000..158a26ae16 --- /dev/null +++ b/src/main/java/domain/piece/move/HorseMoveRule.java @@ -0,0 +1,68 @@ +package domain.piece.move; + +import static domain.piece.move.Vector.DOWN; +import static domain.piece.move.Vector.LEFT; +import static domain.piece.move.Vector.LEFT_DOWN; +import static domain.piece.move.Vector.LEFT_UP; +import static domain.piece.move.Vector.RIGHT; +import static domain.piece.move.Vector.RIGHT_DOWN; +import static domain.piece.move.Vector.RIGHT_UP; +import static domain.piece.move.Vector.UP; + +import domain.intersection.Intersection; +import domain.piece.PieceType; +import domain.point.Point; +import java.util.List; + +public class HorseMoveRule extends MoveRule { + + public HorseMoveRule() { + super(PieceType.HORSE, initializeDirections()); + } + + public static Directions initializeDirections() { + return new Directions(List.of( + new Direction(List.of(UP, LEFT_UP)), + new Direction(List.of(UP, RIGHT_UP)), + new Direction(List.of(DOWN, LEFT_DOWN)), + new Direction(List.of(DOWN, RIGHT_DOWN)), + new Direction(List.of(LEFT, LEFT_UP)), + new Direction(List.of(LEFT, LEFT_DOWN)), + new Direction(List.of(RIGHT, RIGHT_UP)), + new Direction(List.of(RIGHT, RIGHT_DOWN)) + )); + } + + public boolean support(Intersection from) { + return from.isSamePiece(pieceType); + } + + public List findPossiblePoints(Intersection from, Intersection to) { + return directions.findPoints(from.getPoint(), to.getPoint()); + } + + public boolean checkMoveRule(Intersection from, List path) { + Intersection to = path.getLast(); + validateIsSameTeam(from, to); + validateObstacleCondition(path); + return true; + } + + private void validateIsSameTeam(Intersection from, Intersection to) { + if (from.isSameTeam(to)) { + throw new IllegalArgumentException("같은 팀의 위치로 이동할 수 없습니다."); + } + } + + private void validateObstacleCondition(List path) { + List routeWithoutTarget = path.subList(0, path.size() - 1); + + boolean hasObstacle = routeWithoutTarget.stream() + .anyMatch(Intersection::hasPiece); + + if (hasObstacle) { + throw new IllegalArgumentException("이동 경로에 다른 기물이 있어 통과할 수 없습니다."); + } + } + +} diff --git a/src/main/java/domain/piece/move/MoveRule.java b/src/main/java/domain/piece/move/MoveRule.java new file mode 100644 index 0000000000..0bb05c3dac --- /dev/null +++ b/src/main/java/domain/piece/move/MoveRule.java @@ -0,0 +1,24 @@ +package domain.piece.move; + +import domain.intersection.Intersection; +import domain.piece.PieceType; +import domain.point.Point; +import java.util.List; + +public abstract class MoveRule { + + protected final PieceType pieceType; + protected final Directions directions; + + public MoveRule(PieceType pieceType, Directions directions) { + this.pieceType = pieceType; + this.directions = directions; + } + + public abstract boolean support(Intersection from); + + public abstract List findPossiblePoints(Intersection from, Intersection to); + + public abstract boolean checkMoveRule(Intersection from, List path); + +} diff --git a/src/main/java/domain/piece/move/SoliderMoveRule.java b/src/main/java/domain/piece/move/SoliderMoveRule.java new file mode 100644 index 0000000000..978c1811ba --- /dev/null +++ b/src/main/java/domain/piece/move/SoliderMoveRule.java @@ -0,0 +1,58 @@ +package domain.piece.move; + +import static domain.piece.move.Vector.DOWN; +import static domain.piece.move.Vector.LEFT; +import static domain.piece.move.Vector.RIGHT; +import static domain.piece.move.Vector.UP; + +import domain.intersection.Intersection; +import domain.piece.PieceType; +import domain.point.Point; +import java.util.List; + +public class SoliderMoveRule extends MoveRule { + + public SoliderMoveRule() { + super(PieceType.SOLDIER, initializeDirections()); + } + + public static Directions initializeDirections() { + return new Directions(List.of( + new Direction(List.of(DOWN)), + new Direction(List.of(RIGHT)), + new Direction(List.of(LEFT))) + ); + } + + public boolean support(Intersection from) { + return from.isSamePiece(pieceType); + } + + public List findPossiblePoints(Intersection from, Intersection to) { + Directions directions = getDirections(from); + return directions.findPoints(from.getPoint(), to.getPoint()); + } + + public boolean checkMoveRule(Intersection from, List path) { + Intersection to = path.getLast(); + validateIsSameTeam(from, to); + return true; + } + + private void validateIsSameTeam(Intersection from, Intersection to) { + if (from.isSameTeam(to)) { + throw new IllegalArgumentException("같은 팀의 위치로 이동할 수 없습니다."); + } + } + + public Directions getDirections(Intersection from) { + if (from.isChoIntersection()) { + return new Directions(List.of( + new Direction(List.of(UP)), + new Direction(List.of(RIGHT)), + new Direction(List.of(LEFT)))); + } + return directions; + } + +} diff --git a/src/main/java/domain/piece/move/Vector.java b/src/main/java/domain/piece/move/Vector.java new file mode 100644 index 0000000000..d00a4a8847 --- /dev/null +++ b/src/main/java/domain/piece/move/Vector.java @@ -0,0 +1,32 @@ +package domain.piece.move; + +public enum Vector { + + UP(-1, 0), + DOWN(1, 0), + LEFT(0, -1), + RIGHT(0, 1), + + LEFT_UP(-1, -1), + LEFT_DOWN(1, -1), + RIGHT_UP(-1, 1), + RIGHT_DOWN(1, 1), + ; + + private final int dy; + private final int dx; + + Vector(int dy, int dx) { + this.dy = dy; + this.dx = dx; + } + + public int dy() { + return dy; + } + + public int dx() { + return dx; + } + +} diff --git a/src/main/java/domain/point/Point.java b/src/main/java/domain/point/Point.java new file mode 100644 index 0000000000..ef367310b4 --- /dev/null +++ b/src/main/java/domain/point/Point.java @@ -0,0 +1,46 @@ +package domain.point; + +import domain.piece.move.Vector; + +public record Point( + int y, + int x +) { + + public Point(int y, int x) { + validate(y, x); + this.y = y; + this.x = x; + } + + public boolean canMake(int y, int x) { + return checkPointRange(this.y + y, this.x + x); + } + + public Point movePoint(int y, int x) { + return new Point(this.y + y, this.x + x); + } + + private void validate(int y, int x) { + if (checkPointRange(y, x)) { + return; + } + throw new IllegalArgumentException(); + } + + private boolean checkPointRange(int y, int x) { + return 0 <= y && y <= 9 && 0 <= x && x <= 8; + } + + public boolean isSameFile(Point other) { + return this.y == other.y; + } + + public boolean isSameRow(Point other) { + return this.x == other.x; + } + + public Point next(Vector vector) { + return new Point(y + vector.dy(), x + vector.dx()); + } +} diff --git a/src/main/java/domain/team/Team.java b/src/main/java/domain/team/Team.java new file mode 100644 index 0000000000..bb768702c4 --- /dev/null +++ b/src/main/java/domain/team/Team.java @@ -0,0 +1,32 @@ +package domain.team; + +public enum Team { + + CHO("초", "楚"), + HAN("한", "漢"), + ; + + private final String koreanTeamName; + private final String chineseTeamName; + + Team(String koreanTeamName, String chineseTeamName) { + this.koreanTeamName = koreanTeamName; + this.chineseTeamName = chineseTeamName; + } + + public Team nextTurn() { + if (this == CHO) { + return HAN; + } + return CHO; + } + + public String getKoreanTeamName() { + return koreanTeamName; + } + + public String getChineseTeamName() { + return chineseTeamName; + } + +} diff --git a/src/main/java/dto/BoardStatusDTO.java b/src/main/java/dto/BoardStatusDTO.java new file mode 100644 index 0000000000..6406bce1d4 --- /dev/null +++ b/src/main/java/dto/BoardStatusDTO.java @@ -0,0 +1,33 @@ +package dto; + +import domain.intersection.Intersection; +import domain.point.Point; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +public class BoardStatusDTO { + + private final static int MIN_INDEX = 0; + private final static int MAX_ROW = 10; + private final static int MAX_FILE = 9; + + private final List boardStatus; + + public BoardStatusDTO(final Map intersections) { + List pointInfos = IntStream.range(0, MAX_ROW) + .boxed() + .flatMap(row -> IntStream.range(MIN_INDEX, MAX_FILE) + .mapToObj(file -> { + final Point point = new Point(row, file); + final String pointInfo = intersections.get(point).getChineseCharacter(); + return new PointInfoDTO(pointInfo); + })) + .toList(); + this.boardStatus = List.copyOf(pointInfos); + } + + public List boardStatus() { + return List.copyOf(boardStatus); + } +} diff --git a/src/main/java/dto/InputPointDTO.java b/src/main/java/dto/InputPointDTO.java new file mode 100644 index 0000000000..22a9587512 --- /dev/null +++ b/src/main/java/dto/InputPointDTO.java @@ -0,0 +1,37 @@ +package dto; + +import domain.point.Point; + +public class InputPointDTO { + + private final Point point; + + public InputPointDTO(String input) { + validate(input); + this.point = parsePoint(input); + } + + private void validate(String input) { + try { + String[] tokens = input.trim().split("\\s+"); + String y = tokens[0]; + String x = tokens[1]; + Integer.parseInt(y); + Integer.parseInt(x); + } catch (RuntimeException e) { + throw new IllegalArgumentException("잘못된 입력 형식입니다."); + } + } + + private Point parsePoint(String input) { + String[] tokens = input.trim().split("\\s+"); + int integerY = Integer.parseInt(tokens[0]); + int integerX = Integer.parseInt(tokens[1]); + + return new Point(integerY, integerX); + } + + public Point getPoint() { + return this.point; + } +} diff --git a/src/main/java/dto/MoveDTO.java b/src/main/java/dto/MoveDTO.java new file mode 100644 index 0000000000..41fb65a74b --- /dev/null +++ b/src/main/java/dto/MoveDTO.java @@ -0,0 +1,23 @@ +package dto; + +import domain.point.Point; + +public class MoveDTO { + + private final Point from; + private final Point to; + + public MoveDTO(InputPointDTO from, InputPointDTO to) { + this.from = from.getPoint(); + this.to = to.getPoint(); + } + + public Point getFrom() { + return from; + } + + public Point getTo() { + return to; + } + +} diff --git a/src/main/java/dto/PointInfoDTO.java b/src/main/java/dto/PointInfoDTO.java new file mode 100644 index 0000000000..559c193863 --- /dev/null +++ b/src/main/java/dto/PointInfoDTO.java @@ -0,0 +1,14 @@ +package dto; + +public class PointInfoDTO { + + private final String pointInfo; + + public PointInfoDTO(String pointInfo) { + this.pointInfo = pointInfo; + } + + public String pointInfo() { + return pointInfo; + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 0000000000..c8c201b6f6 --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,42 @@ +package view; + +import dto.InputPointDTO; +import java.util.Scanner; + +public class InputView { + + private final String INPUT_MOVE_PIECE_POINT_MESSAGE = "이동할 기물의 좌표를 입력하세요. (입력 형식: y좌표 x좌표)\n"; + private final String INPUT_DESTINATION_POINT_MESSAGE = "기물의 목적지 좌표를 입력하세요. (입력 형식: y좌표 x좌표)\n"; + private final String HAN_WING_SETUP_MESSAGE = "한(漢)의 상차림을 입력하세요." + + "\n1. 마 - 상 - 마 - 상 (馬 - 象 - 馬 - 象)" + + "\n2. 마 - 상 - 상 - 마 (馬 - 象 - 象 - 馬)" + + "\n3. 상 - 마 - 상 - 마 (象 - 馬 - 象 - 馬)" + + "\n4. 상 - 마 - 마 - 상 (象 - 馬 - 馬 - 象)\n"; + private final String CHO_WING_SETUP_MESSAGE = "초(楚)의 상차림을 입력하세요." + + "\n1. 마 - 상 - 마 - 상 (馬 - 象 - 馬 - 象)" + + "\n2. 마 - 상 - 상 - 마 (馬 - 象 - 象 - 馬)" + + "\n3. 상 - 마 - 상 - 마 (象 - 馬 - 象 - 馬)" + + "\n4. 상 - 마 - 마 - 상 (象 - 馬 - 馬 - 象)\n"; + + Scanner scanner = new Scanner(System.in); + + public int inputHanWingSetup() { + System.out.print(HAN_WING_SETUP_MESSAGE); + return Integer.parseInt(scanner.nextLine().trim()); + } + + public int inputChoWingSetup() { + System.out.print(CHO_WING_SETUP_MESSAGE); + return Integer.parseInt(scanner.nextLine().trim()); + } + + public InputPointDTO inputMovePiecePoint() { + System.out.print(INPUT_MOVE_PIECE_POINT_MESSAGE); + return new InputPointDTO(scanner.nextLine()); + } + + public InputPointDTO inputDestinationPoint() { + System.out.print(INPUT_DESTINATION_POINT_MESSAGE); + return new InputPointDTO(scanner.nextLine()); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 0000000000..4ac322b688 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,50 @@ +package view; + +import domain.team.Team; +import dto.BoardStatusDTO; +import dto.PointInfoDTO; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class OutputView { + + private static final int MIN_INDEX = 0; + private static final int MAX_ROW = 10; + private static final int MAX_FILE = 9; + private static final String FULL_SPACE = " "; + private static final String HALF_SPACE = " "; + + public void printCurrentTurn(Team turn) { + System.out.print(turn.getKoreanTeamName() + "(" + turn.getChineseTeamName() + ")의 차례입니다.\n"); + } + + public void printCurrentBoardStatus(final BoardStatusDTO boardStatus) { + final List pointInfos = boardStatus.boardStatus(); + final String header = IntStream.range(MIN_INDEX, MAX_FILE) + .mapToObj(this::toFullWidthNumber) + .collect(Collectors.joining(HALF_SPACE)); + final String rows = IntStream.range(MIN_INDEX, MAX_ROW) + .mapToObj(row -> { + final String rowCells = IntStream.range(MIN_INDEX, MAX_FILE) + .mapToObj(file -> pointInfos.get(row * MAX_FILE + file).pointInfo()) + .collect(Collectors.joining(HALF_SPACE)); + return toFullWidthNumber(row) + HALF_SPACE + rowCells; + }) + .collect(Collectors.joining(System.lineSeparator())); + System.out.println(FULL_SPACE + HALF_SPACE + header); + System.out.println(rows); + } + + private String toFullWidthNumber(int number) { + return String.valueOf(number) + .chars() + .mapToObj(ch -> String.valueOf((char) ('0' + (ch - '0')))) + .collect(Collectors.joining()); + } + + public void printErrorMessage(String message) { + System.out.println(message); + } + +} diff --git a/src/test/java/domain/BoardTest.java b/src/test/java/domain/BoardTest.java new file mode 100644 index 0000000000..cfabc52dde --- /dev/null +++ b/src/test/java/domain/BoardTest.java @@ -0,0 +1,66 @@ +package domain; + +import domain.board.Formation; +import domain.board.JanggiBoard; +import domain.board.JanggiGenerator; +import domain.fixture.TestIntersectionGenerator; +import domain.intersection.Intersection; +import domain.piece.General; +import domain.point.Point; +import domain.team.Team; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public class BoardTest { + + public static final int DEFAULT_ELEPHANT_AND_HORSE_ROW = 0; + + @Test + void 장기판_기물_배치를_테스트한다() { + Point point = new Point(1, 4); + General general = new General(Team.HAN); + Intersection actualIntersection = new Intersection(point, general); + TestIntersectionGenerator testIntersectionGenerator = new TestIntersectionGenerator( + Arrays.asList(actualIntersection)); + + JanggiBoard janggiBoard = new JanggiBoard(testIntersectionGenerator); + Intersection expectedIntersection = janggiBoard.findIntersection(point); + + Assertions.assertThat(actualIntersection.isSamePiece(expectedIntersection)) + .isTrue(); + } + + @Test + void 차림이_선택되었을_때_상과_마를_정확한_위치에_배치해야_한다() { + Formation elephantHorseHorseElephant = Formation.ELEPHANT_HORSE_HORSE_ELEPHANT; + JanggiGenerator janggiGenerator = new JanggiGenerator( + elephantHorseHorseElephant, elephantHorseHorseElephant + ); + JanggiBoard janggiBoard = new JanggiBoard(janggiGenerator); + + List elephantAndHorsePoints = Stream.concat( + elephantHorseHorseElephant.elephantFormations().stream() + .map(x -> new Point(DEFAULT_ELEPHANT_AND_HORSE_ROW, x)), + elephantHorseHorseElephant.horseFormations().stream() + .map(x -> new Point(DEFAULT_ELEPHANT_AND_HORSE_ROW, x)) + ).toList(); + + List actual = elephantAndHorsePoints.stream() + .map(janggiBoard::findIntersection) + .toList(); + + List expected = + janggiGenerator.createElephantAndHorseByFormation(Team.HAN, Formation.ELEPHANT_HORSE_HORSE_ELEPHANT); + + for (int i = 0; i < actual.size(); i++) { + Intersection actualIntersection = actual.get(i); + Intersection expectedIntersection = expected.get(i); + + Assertions.assertThat(actualIntersection.isSamePiece(expectedIntersection)) + .isTrue(); + } + } +} diff --git a/src/test/java/domain/DirectionTest.java b/src/test/java/domain/DirectionTest.java new file mode 100644 index 0000000000..b5b001142d --- /dev/null +++ b/src/test/java/domain/DirectionTest.java @@ -0,0 +1,45 @@ +package domain; + +import domain.piece.move.Direction; +import domain.piece.move.Directions; +import domain.piece.move.Vector; +import domain.point.Point; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class DirectionTest { + + @Test + @DisplayName("주어진 경로를 순차적으로 이동했을 때 최종 목적지에 도달하는지 확인한다.") + void shouldReachTargetWhenFollowingDirections() { + Point current = new Point(0, 0); + Point target = new Point(2, 1); + + Direction directions = new Direction(List.of(Vector.DOWN, Vector.RIGHT_DOWN)); + + Assertions.assertThat(directions.canReach(current, target)) + .isTrue(); + } + + @Test + @DisplayName("주어진 경로의 좌표들을 최종적으로 반환하는지 확인한다.") + void returnAllPointsAlongDestination() { + Direction direction1 = new Direction(List.of(Vector.UP, Vector.LEFT_UP)); + Direction direction2 = new Direction(List.of(Vector.DOWN, Vector.RIGHT_DOWN)); + Point from = new Point(0, 0); + Point to = new Point(2, 1); + + Directions directions = new Directions(List.of(direction1, direction2)); + + List actual = directions.findPoints(from, to); + + Assertions.assertThat(actual).containsExactlyInAnyOrder( + new Point(1, 0), + new Point(2, 1) + ); + } + + +} diff --git a/src/test/java/domain/MoveTest.java b/src/test/java/domain/MoveTest.java new file mode 100644 index 0000000000..5bff35fb4f --- /dev/null +++ b/src/test/java/domain/MoveTest.java @@ -0,0 +1,40 @@ +package domain; + +import domain.board.JanggiBoard; +import domain.fixture.TestIntersectionGenerator; +import domain.intersection.Intersection; +import domain.piece.Chariot; +import domain.piece.Soldier; +import domain.point.Point; +import domain.team.Team; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class MoveTest { + + @Test + @DisplayName("이동이 끝난 뒤 출발지는 비어있고, 도착지는 기물이 존재한다.") + void shouldMovePieceToDestinationAndLeaveSourceEmpty() { + Point start = new Point(0, 0); + Point end = new Point(3, 0); + + Chariot chariot = new Chariot(Team.CHO); + Soldier soldier = new Soldier(Team.HAN); + + Intersection from = new Intersection(start, chariot); + Intersection to = new Intersection(end, soldier); + + JanggiBoard janggiBoard = new JanggiBoard(new TestIntersectionGenerator(List.of(from, to))); + + janggiBoard.tryToMove(start, end); + + Intersection expectedEmpty = Intersection.empty(start); + Intersection expectedChariot = new Intersection(end, chariot); + + Assertions.assertThat(from).isEqualTo(expectedEmpty); + Assertions.assertThat(to).isEqualTo(expectedChariot); + } + +} diff --git a/src/test/java/domain/PieceTest.java b/src/test/java/domain/PieceTest.java new file mode 100644 index 0000000000..ad3e4b0f16 --- /dev/null +++ b/src/test/java/domain/PieceTest.java @@ -0,0 +1,19 @@ +package domain; + +import domain.piece.Cannon; +import domain.team.Team; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public class PieceTest { + + @Test + void 소속팀이_달라도_기물은_동등하다() { + Cannon cannonCho = new Cannon(Team.CHO); + Cannon cannonHan = new Cannon(Team.HAN); + + Assertions.assertThat(cannonCho) + .isEqualTo(cannonHan); + } + +} diff --git a/src/test/java/domain/PointTest.java b/src/test/java/domain/PointTest.java new file mode 100644 index 0000000000..59bd371f35 --- /dev/null +++ b/src/test/java/domain/PointTest.java @@ -0,0 +1,172 @@ +package domain; + +import domain.piece.move.Vector; +import domain.point.Point; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class PointTest { + @Test + @DisplayName("좌표가 범위를 벗어나는 경우에 에러가 발생한다") + void shouldThrowExceptionWhenCoordinateIsOutOfBounds() { + int outOfIndexY = 10; + int outOfIndexX = 9; + + Assertions.assertThatThrownBy(() -> { + new Point(outOfIndexY, outOfIndexX); + }).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("좌표가 같으면 동등한 객체로 취급한다") + void shouldBeEqualWhenPointsAreSame() { + int y = 3; + int x = 3; + + Point actual = new Point(y, x); + Point expected = new Point(y, x); + + Assertions.assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("(0, 0)과 (0, 7)은 Y축이 같아야 한다") + void returnTrueWhenFilesMatch() { + int y = 0; + int seven = 7; + int zero = 0; + + Assertions.assertThat(new Point(y, seven).isSameFile(new Point(y, zero))) + .isTrue(); + } + + @Test + @DisplayName("(0, 0)과 (7, 0)은 X축이 같아야 한다") + void returnTrueWhenRowsMatch() { + int x = 0; + int seven = 7; + int zero = 0; + + Assertions.assertThat(new Point(seven, x).isSameRow(new Point(zero, x))) + .isTrue(); + } + + @Nested + @DisplayName("Point 위치 이동 검증") + class shouldUpdateDirectionWhenPointMove { + + @Test + @DisplayName("DOWN시 Y축이 1증가해야한다.") + void increaseRowWhenMovingDown() { + Point point = new Point(1, 1); + + Point actual = point.next(Vector.DOWN); + Point expected = new Point(2, 1); + + Assertions.assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("UP시 Y축이 1감소해야한다.") + void increaseRowWhenMovingUp() { + Point point = new Point(1, 1); + + Point actual = point.next(Vector.UP); + Point expected = new Point(0, 1); + + Assertions.assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("LEFT시 X축이 1감소해야한다.") + void decreaseFileWhenMovingLeft() { + Point point = new Point(1, 1); + + Point actual = point.next(Vector.LEFT); + Point expected = new Point(1, 0); + + Assertions.assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("RIGHT시 X축이 1증가해야한다.") + void increaseFileWhenMovingRight() { + Point point = new Point(1, 1); + + Point actual = point.next(Vector.RIGHT); + Point expected = new Point(1, 2); + + Assertions.assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("LEFT_UP시 X축은 1감소하고 Y축도 1감소한다.") + void decreaseRowAndFileWhenMovingLeftAndUp() { + Point point = new Point(1, 1); + + Point actual = point.next(Vector.LEFT_UP); + Point expected = new Point(0, 0); + + Assertions.assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("LEFT_DOWN시 X축은 1감소하고 Y축은 1증가한다.") + void increaseRowAndDecreaseFileWhenMovingLeftAndDown() { + Point point = new Point(1, 1); + + Point actual = point.next(Vector.LEFT_DOWN); + Point expected = new Point(2, 0); + + Assertions.assertThat(actual) + .isEqualTo(expected); + } + + + @Test + @DisplayName("RIGHT_UP시 X축은 1증가하고 Y축은 1감소한다.") + void decreaseRowAndIncreaseFileWhenMovingRightUp() { + Point point = new Point(1, 1); + + Point actual = point.next(Vector.RIGHT_UP); + Point expected = new Point(0, 2); + + Assertions.assertThat(actual) + .isEqualTo(expected); + } + + + @Test + @DisplayName("RIGHT_DOWN시 X축은 1증가하고 Y축은 1증가한다.") + void increaseRowAndFileWhenMovingRightDown() { + Point point = new Point(1, 1); + + Point actual = point.next(Vector.RIGHT_DOWN); + Point expected = new Point(2, 2); + + Assertions.assertThat(actual) + .isEqualTo(expected); + } + + @Test + @DisplayName("이동 시, 장기판 범위를 이탈하면 예외가 발생한다.") + void shouldThrowException_WhenPointMovingIsOutOfBound() { + Point point = new Point(0, 0); + + Assertions.assertThatThrownBy(() -> { + point.next(Vector.LEFT_UP); + }).isInstanceOf(IllegalArgumentException.class); + + } + + } + +} diff --git a/src/test/java/domain/fixture/TestIntersectionGenerator.java b/src/test/java/domain/fixture/TestIntersectionGenerator.java new file mode 100644 index 0000000000..e775bab2bd --- /dev/null +++ b/src/test/java/domain/fixture/TestIntersectionGenerator.java @@ -0,0 +1,17 @@ +package domain.fixture; + +import domain.board.IntersectionGenerator; +import domain.intersection.Intersection; +import java.util.List; + +public class TestIntersectionGenerator implements IntersectionGenerator { + List intersections; + + public TestIntersectionGenerator(List intersections) { + this.intersections = intersections; + } + + public List makeIntersection() { + return this.intersections; + } +} diff --git a/src/test/java/domain/rule/CannonMoveRuleTest.java b/src/test/java/domain/rule/CannonMoveRuleTest.java new file mode 100644 index 0000000000..ad1220f455 --- /dev/null +++ b/src/test/java/domain/rule/CannonMoveRuleTest.java @@ -0,0 +1,164 @@ +package domain.rule; + +import domain.intersection.Intersection; +import domain.piece.Cannon; +import domain.piece.Chariot; +import domain.piece.move.CannonMoveRule; +import domain.point.Point; +import domain.team.Team; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CannonMoveRuleTest { + + final Point start = new Point(0, 0); + final Point middlePoint1 = new Point(1, 0); + final Point middlePoint2 = new Point(2, 0); + final Point middlePoint3 = new Point(3, 0); + final Point middlePoint4 = new Point(4, 0); + final Point middlePoint5 = new Point(5, 0); + final Point middlePoint6 = new Point(6, 0); + final Point middlePoint7 = new Point(7, 0); + final Point middlePoint8 = new Point(8, 0); + final Point end = new Point(9, 0); + + final Intersection intersection1 = Intersection.empty(middlePoint1); + final Intersection intersection2 = Intersection.empty(middlePoint2); + final Intersection intersection3 = Intersection.empty(middlePoint3); + final Intersection intersection4 = Intersection.empty(middlePoint4); + final Intersection intersection5 = Intersection.empty(middlePoint5); + final Intersection intersection6 = Intersection.empty(middlePoint6); + final Intersection intersection7 = Intersection.empty(middlePoint7); + final Intersection intersection8 = Intersection.empty(middlePoint8); + + @Test + @DisplayName("도착지에 같은 팀이 있는 경우 예외가 발생한다.") + void shouldThrowExceptionWhenDestinationIsSameTeam() { + Team sameTeam = Team.CHO; + + Intersection from = new Intersection(start, new Cannon(sameTeam)); + Intersection onlyObstacleIntersection = new Intersection(middlePoint5, new Cannon(sameTeam)); + Intersection to = new Intersection(end, new Chariot(sameTeam)); + + CannonMoveRule cannonMoveRule = new CannonMoveRule(); + + Assertions.assertThatThrownBy(() -> { + cannonMoveRule.checkMoveRule(from, List.of(intersection1, + intersection2, + intersection3, + intersection4, + onlyObstacleIntersection, + intersection6, + intersection7, + intersection8, + to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 위치로 이동할 수 없습니다."); + } + + @Test + @DisplayName("포의 이동경로 장애물이 포이면 예외가 발생한다.") + void shouldThrowExceptionWhenObstacleIsCannon() { + Team sameTeam = Team.CHO; + Team anotherTeam = Team.HAN; + + Intersection from = new Intersection(start, new Cannon(sameTeam)); + Intersection cannonObstacle = new Intersection(middlePoint7, new Cannon(anotherTeam)); + Intersection to = Intersection.empty(end); + + CannonMoveRule cannonMoveRule = new CannonMoveRule(); + + Assertions.assertThatThrownBy(() -> { + cannonMoveRule.checkMoveRule(from, List.of(intersection1, + intersection2, + intersection3, + intersection4, + intersection5, + intersection6, + cannonObstacle, + intersection8, + to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("포는 포를 넘어갈 수 없습니다."); + } + + @Test + @DisplayName("포는 두개의 장애물을 넘어갈 때 예외가 발생한다.") + void shouldThrowExceptionWhenCannonJumpTwoObstacle() { + Team sameTeam = Team.CHO; + Team anotherTeam = Team.HAN; + + Intersection from = new Intersection(start, new Cannon(sameTeam)); + Intersection obstacle1 = new Intersection(middlePoint5, new Chariot(anotherTeam)); + Intersection obstacle2 = new Intersection(middlePoint6, new Chariot(anotherTeam)); + Intersection to = Intersection.empty(end); + + CannonMoveRule cannonMoveRule = new CannonMoveRule(); + + Assertions.assertThatThrownBy(() -> { + cannonMoveRule.checkMoveRule(from, List.of(intersection1, + intersection2, + intersection3, + intersection4, + obstacle1, + obstacle2, + intersection7, + intersection8, + to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("포는 반드시 기물 하나를 넘어야 합니다."); + } + + @Test + @DisplayName("포의 경로에 장애물이 없을 때 예외가 발생한다.") + void shouldThrowExceptionWhenCannonPathDoesntObstacle() { + Team sameTeam = Team.CHO; + + Intersection from = new Intersection(start, new Cannon(sameTeam)); + Intersection to = Intersection.empty(end); + + CannonMoveRule cannonMoveRule = new CannonMoveRule(); + + Assertions.assertThatThrownBy(() -> { + cannonMoveRule.checkMoveRule(from, List.of(intersection1, + intersection2, + intersection3, + intersection4, + intersection5, + intersection6, + intersection7, + intersection8, + to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("포는 반드시 기물 하나를 넘어야 합니다."); + } + + @Test + @DisplayName("포가 포를 공격할때 예외가 발생한다.") + void shouldThrowExceptionWhenCannonAttackCannon() { + Team sameTeam = Team.CHO; + Team anotherTeam = Team.HAN; + + Intersection from = new Intersection(start, new Cannon(sameTeam)); + Intersection obstacle = new Intersection(middlePoint6, new Chariot(sameTeam)); + Intersection to = new Intersection(end, new Cannon(anotherTeam)); + + CannonMoveRule cannonMoveRule = new CannonMoveRule(); + + Assertions.assertThatThrownBy(() -> { + cannonMoveRule.checkMoveRule(from, List.of(intersection1, + intersection2, + intersection3, + intersection4, + intersection5, + obstacle, + intersection7, + intersection8, + to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("포는 포를 공격할 수 없습니다."); + } + +} diff --git a/src/test/java/domain/rule/ChariotMoveRuleTest.java b/src/test/java/domain/rule/ChariotMoveRuleTest.java new file mode 100644 index 0000000000..abe290cb78 --- /dev/null +++ b/src/test/java/domain/rule/ChariotMoveRuleTest.java @@ -0,0 +1,126 @@ +package domain.rule; + +import domain.intersection.Intersection; +import domain.piece.Chariot; +import domain.piece.move.ChariotMoveRule; +import domain.point.Point; +import domain.team.Team; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ChariotMoveRuleTest { + + final Point start = new Point(0, 0); + final Point middlePoint1 = new Point(1, 0); + final Point middlePoint2 = new Point(2, 0); + final Point middlePoint3 = new Point(3, 0); + final Point middlePoint4 = new Point(4, 0); + final Point middlePoint5 = new Point(5, 0); + final Point middlePoint6 = new Point(6, 0); + final Point middlePoint7 = new Point(7, 0); + final Point middlePoint8 = new Point(8, 0); + final Point end = new Point(9, 0); + + final Intersection intersection1 = Intersection.empty(middlePoint1); + final Intersection intersection2 = Intersection.empty(middlePoint2); + final Intersection intersection3 = Intersection.empty(middlePoint3); + final Intersection intersection4 = Intersection.empty(middlePoint4); + final Intersection intersection5 = Intersection.empty(middlePoint5); + final Intersection intersection6 = Intersection.empty(middlePoint6); + final Intersection intersection7 = Intersection.empty(middlePoint7); + final Intersection intersection8 = Intersection.empty(middlePoint8); + + @Test + @DisplayName("도착지에 같은 팀이 있는 경우 예외가 발생한다.") + void shouldThrowExceptionWhenDestinationIsSameTeam() { + Team sameTeam = Team.CHO; + + Intersection from = new Intersection(start, new Chariot(sameTeam)); + Intersection to = new Intersection(end, new Chariot(sameTeam)); + + ChariotMoveRule chariotMoveRule = new ChariotMoveRule(); + + Assertions.assertThatThrownBy(() -> { + chariotMoveRule.checkMoveRule(from, List.of(intersection1, + intersection2, + intersection3, + intersection4, + intersection5, + intersection6, + intersection7, + intersection8, + to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 위치로 이동할 수 없습니다."); + } + + @Test + @DisplayName("차의 이동 경로에 장애물이 있으면 예외가 발생한다.") + void shouldThrowExceptionWhenPathHasObstacle() { + Team sameTeam = Team.CHO; + + Intersection from = new Intersection(start, new Chariot(sameTeam)); + Intersection obstacle = new Intersection(middlePoint7, new Chariot(sameTeam)); + Intersection to = Intersection.empty(end); + + ChariotMoveRule chariotMoveRule = new ChariotMoveRule(); + + Assertions.assertThatThrownBy(() -> { + chariotMoveRule.checkMoveRule(from, List.of(intersection1, + intersection2, + intersection3, + intersection4, + intersection5, + intersection6, + obstacle, + intersection8, + to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동 경로에 다른 기물이 있어 통과할 수 없습니다."); + } + + @Test + @DisplayName("차는 경로에 장애물이 없고 도착지가 비어 있으면 이동한다.") + void chariotCanMove_WhenNoObstacle_AndDestinationIsEmpty() { + Intersection from = new Intersection(start, new Chariot(Team.CHO)); + Intersection to = Intersection.empty(end); + + ChariotMoveRule chariotMoveRule = new ChariotMoveRule(); + + Assertions.assertThat( + chariotMoveRule.checkMoveRule(from, List.of(intersection1, + intersection2, + intersection3, + intersection4, + intersection5, + intersection6, + intersection7, + intersection8, + to))) + .isTrue(); + } + + @Test + @DisplayName("차는 경로에 장애물이 없고 도착지에 상대팀이 있으면 이동한다.") + void chariotCanMoveWhenNoObstacleAndDestinationIsOpponent() { + Intersection from = new Intersection(start, new Chariot(Team.CHO)); + Intersection to = new Intersection(end, new Chariot(Team.HAN)); + + ChariotMoveRule chariotMoveRule = new ChariotMoveRule(); + + Assertions.assertThat( + chariotMoveRule.checkMoveRule(from, List.of(intersection1, + intersection2, + intersection3, + intersection4, + intersection5, + intersection6, + intersection7, + intersection8, + to))) + .isTrue(); + } + +} diff --git a/src/test/java/domain/rule/ElephantMoveRuleTest.java b/src/test/java/domain/rule/ElephantMoveRuleTest.java new file mode 100644 index 0000000000..07b27170e2 --- /dev/null +++ b/src/test/java/domain/rule/ElephantMoveRuleTest.java @@ -0,0 +1,108 @@ +package domain.rule; + +import domain.intersection.Intersection; +import domain.piece.Elephant; +import domain.piece.move.ElephantMoveRule; +import domain.point.Point; +import domain.team.Team; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ElephantMoveRuleTest { + + @Test + @DisplayName("도착지에 같은 팀이 있는 경우 예외가 발생한다.") + void shouldThrowExceptionWhenDestinationIsSameTeam() { + Point start = new Point(0, 0); + Point middlePoint1 = new Point(1, 0); + Point middlePoint2 = new Point(2, 1); + Point end = new Point(3, 2); + + Elephant elephant = new Elephant(Team.CHO); + Elephant sameTeamPiece = new Elephant(Team.CHO); + + Intersection from = new Intersection(start, elephant); + Intersection middleIntersection1 = Intersection.empty(middlePoint1); + Intersection middleIntersection2 = Intersection.empty(middlePoint2); + Intersection to = new Intersection(end, sameTeamPiece); + + ElephantMoveRule elephantRule = new ElephantMoveRule(); + + Assertions.assertThatThrownBy(() -> { + elephantRule.checkMoveRule(from, List.of(middleIntersection1, middleIntersection2, to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 위치로 이동할 수 없습니다."); + } + + @Test + @DisplayName("상의 이동 경로에 장애물이 있으면 예외가 발생한다.") + void shouldThrowExceptionWhenPathHasObstacle() { + Point start = new Point(0, 0); + Point middlePoint1 = new Point(1, 0); + Point middlePoint2 = new Point(2, 1); + Point end = new Point(3, 2); + + Elephant elephant = new Elephant(Team.CHO); + Elephant obstacle = new Elephant(Team.CHO); + + Intersection from = new Intersection(start, elephant); + Intersection middleIntersection1 = new Intersection(middlePoint1, obstacle); + Intersection middleIntersection2 = Intersection.empty(middlePoint2); + Intersection to = Intersection.empty(end); + + ElephantMoveRule elephantMoveRule = new ElephantMoveRule(); + + Assertions.assertThatThrownBy(() -> { + elephantMoveRule.checkMoveRule(from, List.of(middleIntersection1, middleIntersection2, to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동 경로에 다른 기물이 있어 통과할 수 없습니다."); + } + + @Test + @DisplayName("상은 경로에 장애물이 없고 도착지가 비어 있으면 이동한다.") + void horseCanMove_WhenNoObstacle_AndDestinationIsEmpty() { + Point start = new Point(0, 0); + Point middlePoint1 = new Point(1, 0); + Point middlePoint2 = new Point(2, 1); + Point end = new Point(3, 2); + + Elephant elephant = new Elephant(Team.CHO); + + Intersection from = new Intersection(start, elephant); + Intersection middleIntersection1 = Intersection.empty(middlePoint1); + Intersection middleIntersection2 = Intersection.empty(middlePoint2); + Intersection to = Intersection.empty(end); + + ElephantMoveRule elephantMoveRule = new ElephantMoveRule(); + + Assertions.assertThat( + elephantMoveRule.checkMoveRule(from, List.of(middleIntersection1, middleIntersection2, to))) + .isTrue(); + } + + @Test + @DisplayName("상은 경로에 장애물이 없고 도착지에 상대팀이 있으면 이동한다.") + void elephantCanMoveWhenNoObstacleAndDestinationIsOpponent() { + Point start = new Point(0, 0); + Point middlePoint1 = new Point(1, 0); + Point middlePoint2 = new Point(2, 1); + Point end = new Point(3, 2); + + Elephant elephant = new Elephant(Team.CHO); + Elephant opponent = new Elephant(Team.HAN); + + Intersection from = new Intersection(start, elephant); + Intersection middleIntersection1 = Intersection.empty(middlePoint1); + Intersection middleIntersection2 = Intersection.empty(middlePoint2); + Intersection to = new Intersection(start, opponent); + + ElephantMoveRule elephantMoveRule = new ElephantMoveRule(); + + Assertions.assertThat( + elephantMoveRule.checkMoveRule(from, List.of(middleIntersection1, middleIntersection2, to))) + .isTrue(); + } + +} diff --git a/src/test/java/domain/rule/GeneralMoveRuleTest.java b/src/test/java/domain/rule/GeneralMoveRuleTest.java new file mode 100644 index 0000000000..a93b0d2129 --- /dev/null +++ b/src/test/java/domain/rule/GeneralMoveRuleTest.java @@ -0,0 +1,75 @@ +package domain.rule; + +import domain.intersection.Intersection; +import domain.piece.General; +import domain.piece.Soldier; +import domain.piece.move.GeneralMoveRule; +import domain.point.Point; +import domain.team.Team; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class GeneralMoveRuleTest { + + @Test + @DisplayName("도착지에 같은 팀이 있는 경우 예외가 발생한다.") + void shouldThrowExceptionWhenDestinationIsSameTeam() { + Point start = new Point(0, 0); + Point end = new Point(1, 0); + + Team sameTeam = Team.HAN; + General general = new General(sameTeam); + Soldier sameTeamPiece = new Soldier(sameTeam); + + Intersection from = new Intersection(start, general); + Intersection to = new Intersection(end, sameTeamPiece); + + GeneralMoveRule generalMoveRule = new GeneralMoveRule(); + + Assertions.assertThatThrownBy(() -> { + generalMoveRule.checkMoveRule(from, List.of(to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 위치로 이동할 수 없습니다."); + } + + @Test + @DisplayName("장군은 도착지에 상대팀이 있으면 이동할 수 있다.") + void canMoveGeneralWhenDestinationIsEmpty() { + Point start = new Point(1, 4); + Point end = new Point(2, 4); + + Team sameTeam = Team.HAN; + General general = new General(sameTeam); + + Intersection from = new Intersection(start, general); + Intersection to = Intersection.empty(end); + + GeneralMoveRule generalMoveRule = new GeneralMoveRule(); + + Assertions.assertThat(generalMoveRule.checkMoveRule(from, List.of(to))) + .isTrue(); + } + + @Test + @DisplayName("장군은 도착지에 상대팀이 있으면 이동할 수 있다.") + void canMoveGeneralWhenDestinationIsOpponent() { + Point start = new Point(1, 4); + Point end = new Point(2, 4); + + Team sameTeam = Team.HAN; + Team anotherTeam = Team.CHO; + General general = new General(sameTeam); + Soldier opponent = new Soldier(anotherTeam); + + Intersection from = new Intersection(start, general); + Intersection to = new Intersection(end, opponent); + + GeneralMoveRule generalMoveRule = new GeneralMoveRule(); + + Assertions.assertThat(generalMoveRule.checkMoveRule(from, List.of(to))) + .isTrue(); + } + +} diff --git a/src/test/java/domain/rule/GuardMoveRuleTest.java b/src/test/java/domain/rule/GuardMoveRuleTest.java new file mode 100644 index 0000000000..235b979690 --- /dev/null +++ b/src/test/java/domain/rule/GuardMoveRuleTest.java @@ -0,0 +1,75 @@ +package domain.rule; + +import domain.intersection.Intersection; +import domain.piece.Guard; +import domain.piece.Soldier; +import domain.piece.move.GuardMoveRule; +import domain.point.Point; +import domain.team.Team; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class GuardMoveRuleTest { + + @Test + @DisplayName("도착지에 같은 팀이 있는 경우 예외가 발생한다.") + void shouldThrowExceptionWhenDestinationIsSameTeam() { + Point start = new Point(0, 0); + Point end = new Point(1, 0); + + Team sameTeam = Team.HAN; + Guard guard = new Guard(sameTeam); + Soldier sameTeamPiece = new Soldier(sameTeam); + + Intersection from = new Intersection(start, guard); + Intersection to = new Intersection(end, sameTeamPiece); + + GuardMoveRule guardMoveRule = new GuardMoveRule(); + + Assertions.assertThatThrownBy(() -> { + guardMoveRule.checkMoveRule(from, List.of(to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 위치로 이동할 수 없습니다."); + } + + @Test + @DisplayName("사는 도착지에 상대팀이 있으면 이동할 수 있다.") + void canMoveGuardWhenDestinationIsEmpty() { + Point start = new Point(1, 4); + Point end = new Point(2, 4); + + Team sameTeam = Team.HAN; + Guard guard = new Guard(sameTeam); + + Intersection from = new Intersection(start, guard); + Intersection to = Intersection.empty(end); + + GuardMoveRule guardMoveRule = new GuardMoveRule(); + + Assertions.assertThat(guardMoveRule.checkMoveRule(from, List.of(to))) + .isTrue(); + } + + @Test + @DisplayName("사는 도착지에 상대팀이 있으면 이동할 수 있다.") + void canMoveGuardWhenDestinationIsOpponent() { + Point start = new Point(1, 4); + Point end = new Point(2, 4); + + Team sameTeam = Team.HAN; + Team anotherTeam = Team.CHO; + Guard guard = new Guard(sameTeam); + Soldier opponent = new Soldier(anotherTeam); + + Intersection from = new Intersection(start, guard); + Intersection to = new Intersection(end, opponent); + + GuardMoveRule guardMoveRule = new GuardMoveRule(); + + Assertions.assertThat(guardMoveRule.checkMoveRule(from, List.of(to))) + .isTrue(); + } + +} diff --git a/src/test/java/domain/rule/HorseMoveRuleTest.java b/src/test/java/domain/rule/HorseMoveRuleTest.java new file mode 100644 index 0000000000..7a59453606 --- /dev/null +++ b/src/test/java/domain/rule/HorseMoveRuleTest.java @@ -0,0 +1,99 @@ +package domain.rule; + +import domain.intersection.Intersection; +import domain.piece.Horse; +import domain.piece.move.HorseMoveRule; +import domain.point.Point; +import domain.team.Team; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class HorseMoveRuleTest { + + @Test + @DisplayName("도착지에 같은 팀이 있는 경우 예외가 발생한다.") + void shouldThrowExceptionWhenDestinationIsSameTeam() { + Point start = new Point(0, 0); + Point middlePoint = new Point(1, 0); + Point end = new Point(2, 1); + + Horse horse = new Horse(Team.CHO); + Horse sameTeamPiece = new Horse(Team.CHO); + + Intersection from = new Intersection(start, horse); + Intersection middleIntersection = Intersection.empty(middlePoint); + Intersection to = new Intersection(end, sameTeamPiece); + + HorseMoveRule horseRule = new HorseMoveRule(); + + Assertions.assertThatThrownBy(() -> { + horseRule.checkMoveRule(from, List.of(middleIntersection, to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 위치로 이동할 수 없습니다."); + } + + @Test + @DisplayName("마의 이동 경로에 장애물이 있으면 예외가 발생한다.") + void shouldThrowExceptionWhenPathHasObstacle() { + Point start = new Point(0, 0); + Point middlePoint = new Point(1, 0); + Point end = new Point(2, 1); + + Horse horse = new Horse(Team.CHO); + Horse obstacle = new Horse(Team.CHO); + + Intersection from = new Intersection(start, horse); + Intersection middleIntersection = new Intersection(middlePoint, obstacle); + Intersection to = Intersection.empty(end); + + HorseMoveRule horseRule = new HorseMoveRule(); + + Assertions.assertThatThrownBy(() -> { + horseRule.checkMoveRule(from, List.of(middleIntersection, to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동 경로에 다른 기물이 있어 통과할 수 없습니다."); + } + + @Test + @DisplayName("마는 경로에 장애물이 없고 도착지가 비어 있으면 이동한다.") + void horseCanMove_WhenNoObstacle_AndDestinationIsEmpty() { + Point start = new Point(0, 0); + Point middlePoint = new Point(1, 0); + Point end = new Point(2, 1); + + Horse horse = new Horse(Team.CHO); + + Intersection from = new Intersection(start, horse); + Intersection middleIntersection = Intersection.empty(middlePoint); + Intersection to = Intersection.empty(end); + + HorseMoveRule horseRule = new HorseMoveRule(); + + Assertions.assertThat(horseRule.checkMoveRule(from, List.of(middleIntersection, to))) + .isTrue(); + } + + @Test + @DisplayName("마는 경로에 장애물이 없고 도착지에 상대팀이 있으면 이동한다.") + void horseCanMoveWhenNoObstacleAndDestinationIsOpponent() { + Point start = new Point(0, 0); + Point middlePoint = new Point(1, 0); + Point end = new Point(2, 1); + + Horse horse = new Horse(Team.CHO); + Horse opponent = new Horse(Team.HAN); + + Intersection from = new Intersection(start, horse); + Intersection middleIntersection = Intersection.empty(middlePoint); + Intersection to = new Intersection(start, opponent); + + HorseMoveRule horseRule = new HorseMoveRule(); + + Assertions.assertThat(horseRule.checkMoveRule(from, List.of(middleIntersection, to))) + .isTrue(); + } + + +} diff --git a/src/test/java/domain/rule/SoliderMoveRuleTest.java b/src/test/java/domain/rule/SoliderMoveRuleTest.java new file mode 100644 index 0000000000..2564117c06 --- /dev/null +++ b/src/test/java/domain/rule/SoliderMoveRuleTest.java @@ -0,0 +1,79 @@ +package domain.rule; + +import domain.intersection.Intersection; +import domain.piece.Soldier; +import domain.piece.move.SoliderMoveRule; +import domain.piece.move.Vector; +import domain.point.Point; +import domain.team.Team; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class SoliderMoveRuleTest { + + @Test + @DisplayName("도착지에 같은 팀이 있는 경우 예외가 발생한다.") + void shouldThrowExceptionWhenDestinationIsSameTeam() { + Point start = new Point(0, 0); + Point end = new Point(1, 0); + + Team sameTeam = Team.HAN; + Soldier soldier = new Soldier(sameTeam); + Soldier sameTeamPiece = new Soldier(sameTeam); + + Intersection from = new Intersection(start, soldier); + Intersection to = new Intersection(end, sameTeamPiece); + + SoliderMoveRule soliderMoveRule = new SoliderMoveRule(); + + Assertions.assertThatThrownBy(() -> { + soliderMoveRule.checkMoveRule(from, List.of(to)); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("같은 팀의 위치로 이동할 수 없습니다."); + } + + @Test + @DisplayName("한 진영인 졸의 직진 방향은 내려감이다.") + void straightOfHanSoliderIsDown() { + Point start = new Point(0, 0); + Point end = start.next(Vector.DOWN); + + Team sameTeam = Team.HAN; + Team anotherTeam = Team.CHO; + Soldier soldier = new Soldier(sameTeam); + Soldier anotherTeamPiece = new Soldier(anotherTeam); + + Intersection from = new Intersection(start, soldier); + Intersection to = new Intersection(end, anotherTeamPiece); + + SoliderMoveRule soliderMoveRule = new SoliderMoveRule(); + + Assertions.assertThat(soliderMoveRule.checkMoveRule(from, List.of(to))) + .isTrue(); + + } + + @Test + @DisplayName("한 진영인 졸의 직진 방향은 올라감이다.") + void straightOfCHOSoliderIsUp() { + Point start = new Point(1, 0); + Point end = start.next(Vector.UP); + + Team sameTeam = Team.HAN; + Team anotherTeam = Team.CHO; + Soldier soldier = new Soldier(sameTeam); + Soldier anotherTeamPiece = new Soldier(anotherTeam); + + Intersection from = new Intersection(start, soldier); + Intersection to = new Intersection(end, anotherTeamPiece); + + SoliderMoveRule soliderMoveRule = new SoliderMoveRule(); + + Assertions.assertThat(soliderMoveRule.checkMoveRule(from, List.of(to))) + .isTrue(); + + } + +}