diff --git a/README.md b/README.md index 9775dda0ae..18aef1d75e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,67 @@ # java-janggi 장기 미션 저장소 + +--- + +## 💡 주요 게임 규칙 + +### 기물 이동 규칙 + +- **궁, 사** + - 궁궐 안에서만 방향 상관없이 1칸씩 이동 + - 뛰어넘기 불가 + - 시작 위치 고정 +- **상** + - 앞/옆/뒤 1칸 + 대각선 2칸 + - 뛰어넘기 불가 + - 마와 위치 변경 가능 +- **마** + - 앞/옆/뒤 1칸 + 대각선 1칸 + - 뛰어넘기 불가 + - 상과 위치 변경 가능 +- **차** + - 직선으로 원하는 만큼 이동 + - 뛰어넘기 불가 + - 시작 위치 고정 +- **포** + - 앞/옆/뒤 원하는 만큼 이동 + - 포를 제외한 기물을 1개 뛰어넘어야만 이동 + - 시작 위치 고정 +- **졸/병** + - 앞/옆으로만 1칸 전진 + - 뛰어넘기 불가 + - 시작 위치 고정 + +## 🚀 기능 요구 사항 + +### 1.1 **보드 초기화** + +게임 시작 시 장기판과 전체 기물을 올바른 위치에 초기화한다. + +- [x] 장기판 초기화 전략을 번호로 입력받는다. + - 상마마상, 상마상마, 마상마상, 마상상마 + - [x] 사용자 입력값 유효성 검사 + - [x] 공백인 경우 예외 처리 + - [x] 숫자가 아닌 문자일 경우 예외 처리 + - [x] 정수 범위를 벗어난 경우 예외 처리 + - [x] 도메인 규칙 검증 + - [x] 입력값이 선택지 범위에 포함되지 않는 경우 예외 처리 +- [x] 전략이 반영된 장기판 상태를 출력한다. + +### 1.2 기물 이동 + +- [x] 이동할 기물의 위치와 이동할 위치를 사용자로부터 입력받는다. + - [x] 사용자 입력값 유효성 검사 + - [x] 공백인 경우 예외 처리 + - [x] `,` 로 구분된 좌표 형식이 아닌 경우 예외 처리 + - [x] 좌표값이 숫자가 아닌 경우 예외 처리 + - [x] 좌표값이 정수 범위를 벗어난 경우 예외 처리 + - [x] 도메인 규칙 검증 + - [x] 좌표값이 보드판의 범위를 벗어난 경우 예외 처리 + - [x] 해당 좌표에 이동시키고자 하는 기물이 존재하지 않는 경우 예외 처리 +- [x] 기물을 이동시킨다. + - [x] 도메인 규칙 검증 + - [x] 기물 규칙에 따라 이동할 위치에 도달할 수 없는 경우 예외 처리 + - [x] 이동할 위치의 기존 기물은 제거된다. +- [x] 이동이 반영된 장기판 상태를 출력한다. diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/main/java/janggi/Application.java b/src/main/java/janggi/Application.java new file mode 100644 index 0000000000..45c52d9c1e --- /dev/null +++ b/src/main/java/janggi/Application.java @@ -0,0 +1,16 @@ +package janggi; + +import janggi.controller.JanggiFlow; +import janggi.view.ApplicationView; +import janggi.view.ConsoleReader; +import janggi.view.ConsoleWriter; + +public class Application { + + public static void main(String[] args) { + ApplicationView view = new ApplicationView(new ConsoleWriter(), new ConsoleReader()); + JanggiFlow janggi = new JanggiFlow(view); + + janggi.process(); + } +} diff --git a/src/main/java/janggi/controller/ArrangementOption.java b/src/main/java/janggi/controller/ArrangementOption.java new file mode 100644 index 0000000000..f2facf79af --- /dev/null +++ b/src/main/java/janggi/controller/ArrangementOption.java @@ -0,0 +1,67 @@ +package janggi.controller; + +import static janggi.domain.piece.PieceType.MA; +import static janggi.domain.piece.PieceType.SANG; + +import janggi.domain.Side; +import janggi.domain.piece.PieceType; +import janggi.strategy.ArrangementStrategy; +import janggi.strategy.MaSangArrangementStrategy; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public enum ArrangementOption { + MA_SANG_MA_SANG(1, "마상마상", List.of(MA, SANG, MA, SANG)), + SANG_MA_SANG_MA(2, "상마상마", List.of(SANG, MA, SANG, MA)), + SANG_MA_MA_SANG(3, "상마마상", List.of(SANG, MA, MA, SANG)), + MA_SANG_SANG_MA(4, "마상상마", List.of(MA, SANG, SANG, MA)); + + private static final Map STRATEGY_OPTIONS = + Collections.unmodifiableMap( + Arrays.stream(values()) + .collect(Collectors.toMap( + ArrangementOption::getOptionNumber, + ArrangementOption::getDisplayName, + (existing, replacement) -> existing, + LinkedHashMap::new + )) + ); + + private final int optionNumber; + private final String displayName; + private final List arrangement; + + ArrangementOption(int optionNumber, String displayName, List arrangement) { + this.optionNumber = optionNumber; + this.displayName = displayName; + this.arrangement = arrangement; + } + + public static ArrangementStrategy createStrategyOf(Side side, int optionNumber) { + return Arrays.stream(values()) + .filter(resolver -> resolver.optionNumber == optionNumber) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 배치 전략 번호입니다.")) + .toStrategy(side); + } + + public static Map getStrategyOptions() { + return STRATEGY_OPTIONS; + } + + private int getOptionNumber() { + return optionNumber; + } + + private String getDisplayName() { + return displayName; + } + + private ArrangementStrategy toStrategy(Side side) { + return MaSangArrangementStrategy.of(side, arrangement); + } +} diff --git a/src/main/java/janggi/controller/JanggiFlow.java b/src/main/java/janggi/controller/JanggiFlow.java new file mode 100644 index 0000000000..f0134c6205 --- /dev/null +++ b/src/main/java/janggi/controller/JanggiFlow.java @@ -0,0 +1,106 @@ +package janggi.controller; + +import janggi.domain.Location; +import janggi.domain.Side; +import janggi.domain.board.Board; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceType; +import janggi.strategy.ArrangementStrategy; +import janggi.strategy.BoardAssembler; +import janggi.view.ApplicationView; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +public class JanggiFlow { + + private final ApplicationView view; + + public JanggiFlow(ApplicationView view) { + this.view = view; + } + + public void process() { + ArrangementStrategy hanStrategy = repeatAskStrategyUntilSuccess(Side.HAN); + ArrangementStrategy choStrategy = repeatAskStrategyUntilSuccess(Side.CHO); + Board board = Board.create(BoardAssembler.from(List.of(hanStrategy, choStrategy))); + + Side current = Side.HAN; + while (board.isNotEmpty()) { + view.showBoardArray(convertBoardStatus(board)); + view.showCurrentSide(current.getName()); + + final Side turnSide = current; + retryUntilPieceIsSuccessfullyMoved(() -> { + Location from = repeatAskLocationOfPieceUntilSuccess(turnSide, board); + Location to = repeatAskLocationToMoveUntilSuccess(from, board); + board.move(from, to); + }); + current = current.switchSide(); + } + } + + private T retry(Supplier supplier) { + while (true) { + try { + return supplier.get(); + } catch (IllegalArgumentException e) { + view.showErrorMessage(e.getMessage()); + } + } + } + + private List> convertBoardStatus(Board board) { + List> boardIn2D = board.to2DArray(); + return boardIn2D.stream() + .map( + row -> row.stream() + .map(Piece::getPieceType) + .map(PieceType::getNameFormat) + .toList() + ).toList(); + } + + private ArrangementStrategy repeatAskStrategyUntilSuccess(Side side) { + return retry(() -> askStrategy(side)); + } + + private ArrangementStrategy askStrategy(Side side) { + Map strategyInfos = ArrangementOption.getStrategyOptions(); + int decisionNumber = view.promptForArrangementStrategyDecision(side.getName(), strategyInfos); + return ArrangementOption.createStrategyOf(side, decisionNumber); + } + + private Location repeatAskLocationOfPieceUntilSuccess(Side turnSide, Board board) { + return retry(() -> askLocationOfPiece(turnSide, board)); + } + + private Location askLocationOfPiece(Side current, Board board) { + List locationOfPiece = view.promptForLocationOfPiece(); + Location verifiedLocation = Location.from(locationOfPiece); + board.validateLocationOfPiece(current, verifiedLocation); + return verifiedLocation; + } + + private Location repeatAskLocationToMoveUntilSuccess(Location from, Board board) { + return retry(() -> askLocationToMove(from, board)); + } + + private Location askLocationToMove(Location startingLocation, Board board) { + List locationToMove = view.promptForLocationToMove(); + Location verifiedLocation = Location.from(locationToMove); + board.validateLocationToMove(startingLocation, verifiedLocation); + return verifiedLocation; + } + + private void retryUntilPieceIsSuccessfullyMoved(Runnable runnable) { + while (true) { + try { + runnable.run(); + break; + } catch (IllegalArgumentException e) { + view.showErrorMessage(e.getMessage()); + } + } + } +} diff --git a/src/main/java/janggi/domain/Location.java b/src/main/java/janggi/domain/Location.java new file mode 100644 index 0000000000..b2aba9233f --- /dev/null +++ b/src/main/java/janggi/domain/Location.java @@ -0,0 +1,33 @@ +package janggi.domain; + +import java.util.List; + +public record Location(int x, int y) { + + private static final int COORDINATION_COUNT = 2; + + public static Location from(List coordination) { + validateCount(coordination); + int row = coordination.get(0) - 1; + int col = coordination.get(1) - 1; + return new Location(row, col); + } + + private static void validateCount(List coordination) { + if (coordination.size() != COORDINATION_COUNT) { + throw new IllegalArgumentException("좌표의 개수는 " + COORDINATION_COUNT + "개 입니다."); + } + } + + public Location add(int dx, int dy) { + return new Location(x + dx, y + dy); + } + + public int calculateHorizontalDiff(Location to) { + return to.y - this.y; + } + + public int calculateVerticalDiff(Location to) { + return to.x - this.x; + } +} diff --git a/src/main/java/janggi/domain/Side.java b/src/main/java/janggi/domain/Side.java new file mode 100644 index 0000000000..00768b64f3 --- /dev/null +++ b/src/main/java/janggi/domain/Side.java @@ -0,0 +1,28 @@ +package janggi.domain; + +public enum Side { + + HAN("한"), + CHO("초"), + NONE("없음"); + + private final String name; + + Side(String name) { + this.name = name; + } + + public Side switchSide() { + if (this == HAN) { + return CHO; + } + if (this == CHO) { + return HAN; + } + throw new UnsupportedOperationException("진영이 존재하지 않아 진영을 반전시킬 수 없습니다."); + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/janggi/domain/board/Board.java b/src/main/java/janggi/domain/board/Board.java new file mode 100644 index 0000000000..055485583f --- /dev/null +++ b/src/main/java/janggi/domain/board/Board.java @@ -0,0 +1,124 @@ +package janggi.domain.board; + +import janggi.domain.Location; +import janggi.domain.Side; +import janggi.domain.piece.EmptyPiece; +import janggi.domain.piece.Piece; +import janggi.strategy.BoardAssembler; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Board { + + private static final int FIRST_ROW_INDEX = 0; + + private final Map boardState; + private final int height; + private final int width; + + private Board(Map boardState, int height, int width) { + this.boardState = boardState; + this.height = height; + this.width = width; + } + + public static Board create(BoardAssembler assembler) { + Piece[][] pieces = assembler.assemble(); + Map boardState = new HashMap<>(); + + int height = pieces.length; + int width = pieces[FIRST_ROW_INDEX].length; + + for (int row = 0; row < height; row++) { + mapRowToBoardState(boardState, pieces[row], row); + } + + return new Board(boardState, height, width); + } + + private static void mapRowToBoardState(Map boardState, Piece[] rowPieces, int row) { + for (int col = 0; col < rowPieces.length; col++) { + boardState.put(new Location(row, col), rowPieces[col]); + } + } + + public void validateLocationOfPiece(Side currentSide, Location locationOfPiece) { + validateLocation(locationOfPiece); + validatePieceExist(locationOfPiece); + Piece piece = boardState.get(locationOfPiece); + if (isNotSameSide(piece, currentSide)) { + throw new IllegalArgumentException("본인 팀의 기물만 선택할 수 있습니다."); + } + } + + private void validateLocation(Location location) { + if (!boardState.containsKey(location)) { + throw new IllegalArgumentException("해당 좌표는 보드판에 존재하지 않습니다."); + } + } + + private void validatePieceExist(Location location) { + if (boardState.get(location).isEmpty()) { + throw new IllegalArgumentException("해당 좌표에 기물이 존재하지 않습니다."); + } + } + + public void move(Location from, Location to) { + Piece piece = boardState.get(from); + List piecesOnPath = getPiecesOnRoute(piece, from, to); + + piece.detectCollision(piecesOnPath); + + executeMove(from, to, piece); + } + + private List getPiecesOnRoute(Piece piece, Location from, Location to) { + return piece.calculateRoute(from, to).stream() + .map(boardState::get) + .toList(); + } + + private void executeMove(Location from, Location to, Piece piece) { + boardState.put(to, piece); + boardState.put(from, EmptyPiece.getInstance()); + } + + public void validateLocationToMove(Location startingLocation, Location locationToMove) { + validateLocation(locationToMove); + validateMovementOccurrence(startingLocation, locationToMove); + } + + private void validateMovementOccurrence(Location from, Location to) { + if (from.equals(to)) { + throw new IllegalArgumentException("기물의 도착 위치는 출발 위치와 일치할 수 없습니다."); + } + } + + public boolean isNotEmpty() { + return !boardState.values().stream() + .allMatch(Piece::isEmpty); + } + + public List> to2DArray() { + List> pieces = new ArrayList<>(); + for (int row = 0; row < height; row++) { + pieces.add(createRows(row)); // 메서드 분리 + } + return List.copyOf(pieces); + } + + private List createRows(int row) { + List line = new ArrayList<>(); + for (int col = 0; col < width; col++) { + Piece piece = boardState.get(new Location(row, col)); + line.add(piece); + } + return List.copyOf(line); + } + + private boolean isNotSameSide(Piece piece, Side side) { + return !piece.isSameSide(side); + } +} diff --git a/src/main/java/janggi/domain/piece/ActivePiece.java b/src/main/java/janggi/domain/piece/ActivePiece.java new file mode 100644 index 0000000000..5b74ba60a7 --- /dev/null +++ b/src/main/java/janggi/domain/piece/ActivePiece.java @@ -0,0 +1,60 @@ +package janggi.domain.piece; + +import janggi.domain.Location; +import janggi.domain.Side; +import janggi.domain.rule.Movement; +import java.util.List; +import java.util.Optional; + +public class ActivePiece implements Piece { + + protected final PieceType pieceType; + protected final Side side; + private final Movement movement; + + public ActivePiece(PieceType pieceType, Side side, Movement movement) { + this.pieceType = pieceType; + this.side = side; + this.movement = movement; + } + + @Override + public List calculateRoute(Location from, Location to) { + Optional> calculatedRoute = movement.calculateRoute(from, to); + if (calculatedRoute.isEmpty()) { + throw new IllegalArgumentException( + String.format("%s의 기물 이동 규칙 위반: 해당 위치(%d, %d)에 도달할 수 없습니다.", + pieceType.getNameFormat(), + to.x(), + to.y()) + ); + } + + return calculatedRoute.get(); + } + + @Override + public void detectCollision(List piecesOnPath) { + movement.detectCollision(side, piecesOnPath); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean isPo() { + return this.pieceType == PieceType.PO; + } + + @Override + public boolean isSameSide(Side side) { + return this.side.equals(side); + } + + @Override + public PieceType getPieceType() { + return pieceType; + } +} diff --git a/src/main/java/janggi/domain/piece/EmptyPiece.java b/src/main/java/janggi/domain/piece/EmptyPiece.java new file mode 100644 index 0000000000..e97a6de40b --- /dev/null +++ b/src/main/java/janggi/domain/piece/EmptyPiece.java @@ -0,0 +1,45 @@ +package janggi.domain.piece; + +import janggi.domain.Location; +import janggi.domain.Side; +import java.util.List; + +@SuppressWarnings("java:S6548") +public class EmptyPiece implements Piece { + + private static final EmptyPiece INSTANCE = new EmptyPiece(); + + public static EmptyPiece getInstance() { + return INSTANCE; + } + + @Override + public List calculateRoute(Location from, Location to) { + throw new UnsupportedOperationException("빈 객체는 이동할 수 없습니다."); + } + + @Override + public void detectCollision(List piecesOnPath) { + throw new UnsupportedOperationException("빈 객체는 이동할 수 없습니다."); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean isPo() { + throw new UnsupportedOperationException("빈 객체는 포일 수 없습니다."); + } + + @Override + public boolean isSameSide(Side side) { + throw new UnsupportedOperationException("빈 객체는 진영이 존재하지 않습니다."); + } + + @Override + public PieceType getPieceType() { + return PieceType.EMPTY; + } +} diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java new file mode 100644 index 0000000000..8af9562c3a --- /dev/null +++ b/src/main/java/janggi/domain/piece/Piece.java @@ -0,0 +1,20 @@ +package janggi.domain.piece; + +import janggi.domain.Location; +import janggi.domain.Side; +import java.util.List; + +public interface Piece { + + List calculateRoute(Location from, Location to); + + void detectCollision(List piecesOnPath); + + boolean isEmpty(); + + boolean isPo(); + + boolean isSameSide(Side side); + + PieceType getPieceType(); +} diff --git a/src/main/java/janggi/domain/piece/PieceFactory.java b/src/main/java/janggi/domain/piece/PieceFactory.java new file mode 100644 index 0000000000..84ecae1fe7 --- /dev/null +++ b/src/main/java/janggi/domain/piece/PieceFactory.java @@ -0,0 +1,51 @@ +package janggi.domain.piece; + +import static janggi.domain.piece.PieceType.*; + +import janggi.domain.Side; +import janggi.domain.rule.ChaMovement; +import janggi.domain.rule.GungMovement; +import janggi.domain.rule.JolbyeongMovement; +import janggi.domain.rule.MaMovement; +import janggi.domain.rule.Movement; +import janggi.domain.rule.PoMovement; +import janggi.domain.rule.SaMovement; +import janggi.domain.rule.SangMovement; +import java.util.EnumMap; +import java.util.Map; + +@SuppressWarnings("java:S6548") +public class PieceFactory { + + private static final PieceFactory INSTANCE = new PieceFactory(); + + private final Map matchInfo; + + private PieceFactory() { + this.matchInfo = new EnumMap<>(PieceType.class); + matchInfo.put(CHA, ChaMovement.getInstance()); + matchInfo.put(MA, MaMovement.getInstance()); + matchInfo.put(SANG, SangMovement.getInstance()); + matchInfo.put(SA, SaMovement.getInstance()); + matchInfo.put(GUNG, GungMovement.getInstance()); + matchInfo.put(PO, PoMovement.getInstance()); + matchInfo.put(JOL, JolbyeongMovement.getInstanceBySide(Side.CHO)); + matchInfo.put(BYEONG, JolbyeongMovement.getInstanceBySide(Side.HAN)); + } + + public static PieceFactory getInstance() { + return INSTANCE; + } + + public Piece createActivePiece(PieceType type, Side side) { + Movement movement = mapMovement(type); + return new ActivePiece(type, side, movement); + } + + private Movement mapMovement(PieceType type) { + if (matchInfo.containsKey(type)) { + return matchInfo.get(type); + } + throw new IllegalArgumentException("이동 규칙이 정의되지 않은 기물입니다 : " + type); + } +} diff --git a/src/main/java/janggi/domain/piece/PieceType.java b/src/main/java/janggi/domain/piece/PieceType.java new file mode 100644 index 0000000000..d480415b4a --- /dev/null +++ b/src/main/java/janggi/domain/piece/PieceType.java @@ -0,0 +1,24 @@ +package janggi.domain.piece; + +public enum PieceType { + + EMPTY("ㆍ"), + GUNG("궁"), + SA("사"), + MA("마"), + SANG("상"), + CHA("차"), + PO("포"), + JOL("졸"), + BYEONG("병"); + + private final String nameFormat; + + PieceType(String nameFormat) { + this.nameFormat = nameFormat; + } + + public String getNameFormat() { + return this.nameFormat; + } +} diff --git a/src/main/java/janggi/domain/rule/ChaMovement.java b/src/main/java/janggi/domain/rule/ChaMovement.java new file mode 100644 index 0000000000..4125667a00 --- /dev/null +++ b/src/main/java/janggi/domain/rule/ChaMovement.java @@ -0,0 +1,39 @@ +package janggi.domain.rule; + +import janggi.domain.Location; +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import janggi.domain.rule.collision.CollisionDetector; +import janggi.domain.rule.collision.DefaultCollisionDetector; +import janggi.domain.rule.route.RouteProvider; +import janggi.domain.rule.route.StraightRouteProvider; +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("java:S6548") +public class ChaMovement implements Movement { + + private static final ChaMovement INSTANCE = new ChaMovement(); + + private final RouteProvider routeProvider; + private final CollisionDetector collisionDetector; + + private ChaMovement() { + this.routeProvider = StraightRouteProvider.getInstance(); + this.collisionDetector = DefaultCollisionDetector.getInstance(); + } + + public static ChaMovement getInstance() { + return INSTANCE; + } + + @Override + public Optional> calculateRoute(Location from, Location to) { + return routeProvider.calculateRoute(from, to); + } + + @Override + public void detectCollision(Side side, List piecesOnPath) { + collisionDetector.check(side, piecesOnPath); + } +} diff --git a/src/main/java/janggi/domain/rule/GungMovement.java b/src/main/java/janggi/domain/rule/GungMovement.java new file mode 100644 index 0000000000..5418286d4c --- /dev/null +++ b/src/main/java/janggi/domain/rule/GungMovement.java @@ -0,0 +1,39 @@ +package janggi.domain.rule; + +import janggi.domain.Location; +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import janggi.domain.rule.collision.CollisionDetector; +import janggi.domain.rule.collision.DefaultCollisionDetector; +import janggi.domain.rule.route.GungSeongRouteProvider; +import janggi.domain.rule.route.RouteProvider; +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("java:S6548") +public class GungMovement implements Movement { + + private static final GungMovement INSTANCE = new GungMovement(); + + private final RouteProvider routeProvider; + private final CollisionDetector collisionDetector; + + private GungMovement() { + this.routeProvider = GungSeongRouteProvider.getInstance(); + this.collisionDetector = DefaultCollisionDetector.getInstance(); + } + + public static GungMovement getInstance() { + return INSTANCE; + } + + @Override + public Optional> calculateRoute(Location from, Location to) { + return routeProvider.calculateRoute(from, to); + } + + @Override + public void detectCollision(Side side, List piecesOnPath) { + collisionDetector.check(side, piecesOnPath); + } +} diff --git a/src/main/java/janggi/domain/rule/JolbyeongMovement.java b/src/main/java/janggi/domain/rule/JolbyeongMovement.java new file mode 100644 index 0000000000..7c8bba24c6 --- /dev/null +++ b/src/main/java/janggi/domain/rule/JolbyeongMovement.java @@ -0,0 +1,66 @@ +package janggi.domain.rule; + +import static janggi.domain.rule.route.Direction.LEFT; +import static janggi.domain.rule.route.Direction.RIGHT; + +import janggi.domain.Location; +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import janggi.domain.rule.collision.CollisionDetector; +import janggi.domain.rule.collision.DefaultCollisionDetector; +import janggi.domain.rule.route.DefaultRouteProvider; +import janggi.domain.rule.route.Direction; +import janggi.domain.rule.route.Route; +import janggi.domain.rule.route.RouteProvider; +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("java:S6548") +public class JolbyeongMovement implements Movement { + + private static final JolbyeongMovement JOL_MOVEMENT_INSTANCE = new JolbyeongMovement(Side.CHO); + private static final JolbyeongMovement BYEONG_MOVEMENT_INSTANCE = new JolbyeongMovement(Side.HAN); + + private final RouteProvider routeProvider; + private final CollisionDetector collisionDetector = DefaultCollisionDetector.getInstance(); + + private JolbyeongMovement(Side side) { + List possibleRoutes = List.of( + Route.from(List.of(getFrontDirection(side))), + Route.from(List.of(LEFT)), + Route.from(List.of(RIGHT)) + ); + + this.routeProvider = new DefaultRouteProvider(possibleRoutes); + } + + private static Direction getFrontDirection(Side side) { + if (side == Side.HAN) { + return Direction.FRONT; + } + if (side == Side.CHO) { + return Direction.BACK; + } + throw new IllegalStateException("진영이 존재하지 않아 전진 방향을 정할 수 없습니다."); + } + + public static JolbyeongMovement getInstanceBySide(Side side) { + if (side == Side.HAN) { + return BYEONG_MOVEMENT_INSTANCE; + } + if (side == Side.CHO) { + return JOL_MOVEMENT_INSTANCE; + } + throw new IllegalArgumentException("진영이 존재하지 않아 이동 규칙을 정할 수 없습니다."); + } + + @Override + public Optional> calculateRoute(Location from, Location to) { + return routeProvider.calculateRoute(from, to); + } + + @Override + public void detectCollision(Side side, List piecesOnPath) { + collisionDetector.check(side, piecesOnPath); + } +} diff --git a/src/main/java/janggi/domain/rule/MaMovement.java b/src/main/java/janggi/domain/rule/MaMovement.java new file mode 100644 index 0000000000..0b202daffc --- /dev/null +++ b/src/main/java/janggi/domain/rule/MaMovement.java @@ -0,0 +1,59 @@ +package janggi.domain.rule; + +import static janggi.domain.rule.route.Direction.BACK; +import static janggi.domain.rule.route.Direction.BACK_LEFT; +import static janggi.domain.rule.route.Direction.BACK_RIGHT; +import static janggi.domain.rule.route.Direction.FRONT; +import static janggi.domain.rule.route.Direction.FRONT_LEFT; +import static janggi.domain.rule.route.Direction.FRONT_RIGHT; +import static janggi.domain.rule.route.Direction.LEFT; +import static janggi.domain.rule.route.Direction.RIGHT; + +import janggi.domain.Location; +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import janggi.domain.rule.collision.CollisionDetector; +import janggi.domain.rule.collision.DefaultCollisionDetector; +import janggi.domain.rule.route.DefaultRouteProvider; +import janggi.domain.rule.route.Route; +import janggi.domain.rule.route.RouteProvider; +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("java:S6548") +public class MaMovement implements Movement { + + private static final MaMovement INSTANCE = new MaMovement(); + + private final RouteProvider routeProvider; + private final CollisionDetector collisionDetector; + + private MaMovement() { + List possibleRoutes = List.of( + Route.from(List.of(FRONT, FRONT_LEFT)), + Route.from(List.of(FRONT, FRONT_RIGHT)), + Route.from(List.of(RIGHT, FRONT_RIGHT)), + Route.from(List.of(RIGHT, BACK_RIGHT)), + Route.from(List.of(LEFT, FRONT_LEFT)), + Route.from(List.of(LEFT, BACK_LEFT)), + Route.from(List.of(BACK, BACK_LEFT)), + Route.from(List.of(BACK, BACK_RIGHT)) + ); + this.routeProvider = new DefaultRouteProvider(possibleRoutes); + this.collisionDetector = DefaultCollisionDetector.getInstance(); + } + + public static MaMovement getInstance() { + return INSTANCE; + } + + @Override + public Optional> calculateRoute(Location from, Location to) { + return routeProvider.calculateRoute(from, to); + } + + @Override + public void detectCollision(Side side, List piecesOnPath) { + collisionDetector.check(side, piecesOnPath); + } +} diff --git a/src/main/java/janggi/domain/rule/Movement.java b/src/main/java/janggi/domain/rule/Movement.java new file mode 100644 index 0000000000..462fae1c77 --- /dev/null +++ b/src/main/java/janggi/domain/rule/Movement.java @@ -0,0 +1,14 @@ +package janggi.domain.rule; + +import janggi.domain.Location; +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import java.util.List; +import java.util.Optional; + +public interface Movement { + + Optional> calculateRoute(Location from, Location to); + + void detectCollision(Side side, List piecesOnPath); +} diff --git a/src/main/java/janggi/domain/rule/PoMovement.java b/src/main/java/janggi/domain/rule/PoMovement.java new file mode 100644 index 0000000000..588d27981b --- /dev/null +++ b/src/main/java/janggi/domain/rule/PoMovement.java @@ -0,0 +1,40 @@ +package janggi.domain.rule; + +import janggi.domain.Location; +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import janggi.domain.rule.collision.CollisionDetector; +import janggi.domain.rule.collision.PoCollisionDetector; +import janggi.domain.rule.route.RouteProvider; +import janggi.domain.rule.route.StraightRouteProvider; +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("java:S6548") +public class PoMovement implements Movement { + + private static final PoMovement INSTANCE = new PoMovement(); + + private final RouteProvider routeProvider; + private final CollisionDetector collisionDetector; + + private PoMovement() { + this.routeProvider = StraightRouteProvider.getInstance(); + this.collisionDetector = PoCollisionDetector.getInstance(); + } + + public static PoMovement getInstance() { + return INSTANCE; + } + + @Override + public Optional> calculateRoute(Location from, Location to) { + return routeProvider.calculateRoute(from, to); + } + + @Override + public void detectCollision(Side side, List piecesOnPath) { + collisionDetector.check(side, piecesOnPath); + } +} + diff --git a/src/main/java/janggi/domain/rule/SaMovement.java b/src/main/java/janggi/domain/rule/SaMovement.java new file mode 100644 index 0000000000..9b20cb56ed --- /dev/null +++ b/src/main/java/janggi/domain/rule/SaMovement.java @@ -0,0 +1,39 @@ +package janggi.domain.rule; + +import janggi.domain.Location; +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import janggi.domain.rule.collision.CollisionDetector; +import janggi.domain.rule.collision.DefaultCollisionDetector; +import janggi.domain.rule.route.GungSeongRouteProvider; +import janggi.domain.rule.route.RouteProvider; +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("java:S6548") +public class SaMovement implements Movement { + + private static final SaMovement INSTANCE = new SaMovement(); + + private final RouteProvider routeProvider; + private final CollisionDetector collisionDetector; + + private SaMovement() { + this.routeProvider = GungSeongRouteProvider.getInstance(); + this.collisionDetector = DefaultCollisionDetector.getInstance(); + } + + public static SaMovement getInstance() { + return INSTANCE; + } + + @Override + public Optional> calculateRoute(Location from, Location to) { + return routeProvider.calculateRoute(from, to); + } + + @Override + public void detectCollision(Side side, List piecesOnPath) { + collisionDetector.check(side, piecesOnPath); + } +} diff --git a/src/main/java/janggi/domain/rule/SangMovement.java b/src/main/java/janggi/domain/rule/SangMovement.java new file mode 100644 index 0000000000..d62c3744ee --- /dev/null +++ b/src/main/java/janggi/domain/rule/SangMovement.java @@ -0,0 +1,59 @@ +package janggi.domain.rule; + +import static janggi.domain.rule.route.Direction.BACK; +import static janggi.domain.rule.route.Direction.BACK_LEFT; +import static janggi.domain.rule.route.Direction.BACK_RIGHT; +import static janggi.domain.rule.route.Direction.FRONT; +import static janggi.domain.rule.route.Direction.FRONT_LEFT; +import static janggi.domain.rule.route.Direction.FRONT_RIGHT; +import static janggi.domain.rule.route.Direction.LEFT; +import static janggi.domain.rule.route.Direction.RIGHT; + +import janggi.domain.Location; +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import janggi.domain.rule.collision.CollisionDetector; +import janggi.domain.rule.collision.DefaultCollisionDetector; +import janggi.domain.rule.route.DefaultRouteProvider; +import janggi.domain.rule.route.Route; +import janggi.domain.rule.route.RouteProvider; +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("java:S6548") +public class SangMovement implements Movement { + + private static final SangMovement INSTANCE = new SangMovement(); + + private final RouteProvider routeProvider; + private final CollisionDetector collisionDetector; + + private SangMovement() { + List possibleRoutes = List.of( + Route.from(List.of(FRONT, FRONT_LEFT, FRONT_LEFT)), + Route.from(List.of(FRONT, FRONT_RIGHT, FRONT_RIGHT)), + Route.from(List.of(RIGHT, FRONT_RIGHT, FRONT_RIGHT)), + Route.from(List.of(RIGHT, BACK_RIGHT, BACK_RIGHT)), + Route.from(List.of(LEFT, FRONT_LEFT, FRONT_LEFT)), + Route.from(List.of(LEFT, BACK_LEFT, BACK_LEFT)), + Route.from(List.of(BACK, BACK_LEFT, BACK_LEFT)), + Route.from(List.of(BACK, BACK_RIGHT, BACK_RIGHT)) + ); + this.routeProvider = new DefaultRouteProvider(possibleRoutes); + this.collisionDetector = DefaultCollisionDetector.getInstance(); + } + + public static SangMovement getInstance() { + return INSTANCE; + } + + @Override + public Optional> calculateRoute(Location from, Location to) { + return routeProvider.calculateRoute(from, to); + } + + @Override + public void detectCollision(Side side, List piecesOnPath) { + collisionDetector.check(side, piecesOnPath); + } +} diff --git a/src/main/java/janggi/domain/rule/collision/CollisionDetector.java b/src/main/java/janggi/domain/rule/collision/CollisionDetector.java new file mode 100644 index 0000000000..5f7b4ae25b --- /dev/null +++ b/src/main/java/janggi/domain/rule/collision/CollisionDetector.java @@ -0,0 +1,10 @@ +package janggi.domain.rule.collision; + +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import java.util.List; + +public interface CollisionDetector { + + void check(Side side, List piecesOnPath); +} diff --git a/src/main/java/janggi/domain/rule/collision/DefaultCollisionDetector.java b/src/main/java/janggi/domain/rule/collision/DefaultCollisionDetector.java new file mode 100644 index 0000000000..c91d475163 --- /dev/null +++ b/src/main/java/janggi/domain/rule/collision/DefaultCollisionDetector.java @@ -0,0 +1,47 @@ +package janggi.domain.rule.collision; + +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import java.util.List; + +@SuppressWarnings("java:S6548") +public class DefaultCollisionDetector implements CollisionDetector { + + private static final DefaultCollisionDetector INSTANCE = new DefaultCollisionDetector(); + + private DefaultCollisionDetector() { + } + + public static DefaultCollisionDetector getInstance() { + return INSTANCE; + } + + @Override + public void check(Side side, List piecesOnPath) { + validateMiddlePath(piecesOnPath); + validateDestination(side, piecesOnPath); + } + + private void validateMiddlePath(List piecesOnPath) { + List middlePath = piecesOnPath.subList(0, piecesOnPath.size() - 1); + for (Piece pathPiece : middlePath) { + validateNoObstacle(pathPiece); + } + } + + private void validateNoObstacle(Piece pathPiece) { + if (!pathPiece.isEmpty()) { + throw new IllegalArgumentException("이동 경로에 기물이 존재합니다"); + } + } + + private void validateDestination(Side side, List piecesOnPath) { + Piece destinationPiece = piecesOnPath.getLast(); + if (destinationPiece.isEmpty()) { + return; + } + if (destinationPiece.isSameSide(side)) { + throw new IllegalArgumentException("도착 위치에 같은 팀이 존재합니다"); + } + } +} diff --git a/src/main/java/janggi/domain/rule/collision/PoCollisionDetector.java b/src/main/java/janggi/domain/rule/collision/PoCollisionDetector.java new file mode 100644 index 0000000000..59721a1d44 --- /dev/null +++ b/src/main/java/janggi/domain/rule/collision/PoCollisionDetector.java @@ -0,0 +1,63 @@ +package janggi.domain.rule.collision; + +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import java.util.List; + +@SuppressWarnings("java:S6548") +public class PoCollisionDetector implements CollisionDetector { + + private static final PoCollisionDetector INSTANCE = new PoCollisionDetector(); + private static final int REQUIRED_SCREEN_COUNT = 1; + + private PoCollisionDetector() { + } + + public static PoCollisionDetector getInstance() { + return INSTANCE; + } + + @Override + public void check(Side side, List piecesOnPath) { + validateOneObstacle(piecesOnPath); + validatePoExistence(piecesOnPath); + validateDestination(side, piecesOnPath); + } + + private void validateOneObstacle(List piecesOnPath) { + List middlePath = piecesOnPath.subList(0, piecesOnPath.size() - 1); + + long obstacleCount = middlePath.stream() + .filter(piece -> !piece.isEmpty()) + .count(); + + if (obstacleCount != REQUIRED_SCREEN_COUNT) { + throw new IllegalArgumentException("포는 반드시 하나의 기물을 넘어야 합니다."); + } + } + + private void validatePoExistence(List piecesOnPath) { + for (Piece piece : piecesOnPath) { + validateNonePo(piece); + } + } + + private void validateNonePo(Piece pieceOnPath) { + if (pieceOnPath.isEmpty()) { + return; + } + if (pieceOnPath.isPo()) { + throw new IllegalArgumentException("이동 경로 또는 도착지에 포가 존재할 수 없습니다."); + } + } + + private void validateDestination(Side side, List piecesOnPath) { + Piece destinationPiece = piecesOnPath.getLast(); + if (destinationPiece.isEmpty()) { + return; + } + if (destinationPiece.isSameSide(side)) { + throw new IllegalArgumentException("도착 위치에 같은 팀이 존재합니다"); + } + } +} diff --git a/src/main/java/janggi/domain/rule/route/DefaultRouteProvider.java b/src/main/java/janggi/domain/rule/route/DefaultRouteProvider.java new file mode 100644 index 0000000000..15b059dfe2 --- /dev/null +++ b/src/main/java/janggi/domain/rule/route/DefaultRouteProvider.java @@ -0,0 +1,19 @@ +package janggi.domain.rule.route; + +import janggi.domain.Location; +import java.util.List; +import java.util.Optional; + +public class DefaultRouteProvider extends RouteProvider { + + private final List possibleRoute; + + public DefaultRouteProvider(List possibleRoute) { + this.possibleRoute = possibleRoute; + } + + @Override + public Optional> calculateRoute(Location from, Location to) { + return findValidPath(from, to, possibleRoute); + } +} diff --git a/src/main/java/janggi/domain/rule/route/Direction.java b/src/main/java/janggi/domain/rule/route/Direction.java new file mode 100644 index 0000000000..d89612fd38 --- /dev/null +++ b/src/main/java/janggi/domain/rule/route/Direction.java @@ -0,0 +1,27 @@ +package janggi.domain.rule.route; + +import janggi.domain.Location; + +public enum Direction { + + FRONT(0, 1), + LEFT(-1, 0), + RIGHT(1, 0), + BACK(0, -1), + FRONT_LEFT(-1, 1), + FRONT_RIGHT(1, 1), + BACK_LEFT(-1, -1), + BACK_RIGHT(1, -1); + + private final int dx; + private final int dy; + + Direction(int dx, int dy) { + this.dx = dx; + this.dy = dy; + } + + public Location apply(Location location) { + return location.add(dx, dy); + } +} diff --git a/src/main/java/janggi/domain/rule/route/GungSeongRouteProvider.java b/src/main/java/janggi/domain/rule/route/GungSeongRouteProvider.java new file mode 100644 index 0000000000..9e15ce9c04 --- /dev/null +++ b/src/main/java/janggi/domain/rule/route/GungSeongRouteProvider.java @@ -0,0 +1,42 @@ +package janggi.domain.rule.route; + +import static janggi.domain.rule.route.Direction.BACK; +import static janggi.domain.rule.route.Direction.BACK_LEFT; +import static janggi.domain.rule.route.Direction.BACK_RIGHT; +import static janggi.domain.rule.route.Direction.FRONT; +import static janggi.domain.rule.route.Direction.FRONT_LEFT; +import static janggi.domain.rule.route.Direction.FRONT_RIGHT; +import static janggi.domain.rule.route.Direction.LEFT; +import static janggi.domain.rule.route.Direction.RIGHT; + +import janggi.domain.Location; +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("java:S6548") +public class GungSeongRouteProvider extends RouteProvider { + + private static final GungSeongRouteProvider INSTANCE = new GungSeongRouteProvider(); + private static final List POSSIBLE_ROUTES = List.of( + Route.from(List.of(FRONT)), + Route.from(List.of(LEFT)), + Route.from(List.of(RIGHT)), + Route.from(List.of(BACK)), + Route.from(List.of(FRONT_LEFT)), + Route.from(List.of(FRONT_RIGHT)), + Route.from(List.of(BACK_LEFT)), + Route.from(List.of(BACK_RIGHT)) + ); + + private GungSeongRouteProvider() { + } + + public static GungSeongRouteProvider getInstance() { + return INSTANCE; + } + + @Override + public Optional> calculateRoute(Location from, Location to) { + return findValidPath(from, to, POSSIBLE_ROUTES); + } +} diff --git a/src/main/java/janggi/domain/rule/route/Route.java b/src/main/java/janggi/domain/rule/route/Route.java new file mode 100644 index 0000000000..e969ebc86a --- /dev/null +++ b/src/main/java/janggi/domain/rule/route/Route.java @@ -0,0 +1,36 @@ +package janggi.domain.rule.route; + +import janggi.domain.Location; +import java.util.ArrayList; +import java.util.List; + +public class Route { + + private final List directions; + + private Route(List directions) { + this.directions = directions; + } + + public static Route from(List directions) { + return new Route(directions); + } + + public static Route of(Direction direction, int count) { + List way = new ArrayList<>(); + for (int i = 0; i < count; i++) { + way.add(direction); + } + return new Route(way); + } + + public List calculateLocationsOnPath(Location current) { + List locationsOnPath = new ArrayList<>(); + for (Direction direction : directions) { + current = direction.apply(current); + locationsOnPath.add(current); + } + + return locationsOnPath; + } +} diff --git a/src/main/java/janggi/domain/rule/route/RouteProvider.java b/src/main/java/janggi/domain/rule/route/RouteProvider.java new file mode 100644 index 0000000000..307b1beef6 --- /dev/null +++ b/src/main/java/janggi/domain/rule/route/RouteProvider.java @@ -0,0 +1,21 @@ +package janggi.domain.rule.route; + +import janggi.domain.Location; +import java.util.List; +import java.util.Optional; + +public abstract class RouteProvider { + + public abstract Optional> calculateRoute(Location from, Location to); + + protected Optional> findValidPath(Location from, Location to, List possibleRoutes) { + for (Route route : possibleRoutes) { + List locationsOnPath = route.calculateLocationsOnPath(from); + Location expectedDestination = locationsOnPath.getLast(); + if (expectedDestination.equals(to)) { + return Optional.of(locationsOnPath); + } + } + return Optional.empty(); + } +} diff --git a/src/main/java/janggi/domain/rule/route/StraightRouteProvider.java b/src/main/java/janggi/domain/rule/route/StraightRouteProvider.java new file mode 100644 index 0000000000..e54f3a747c --- /dev/null +++ b/src/main/java/janggi/domain/rule/route/StraightRouteProvider.java @@ -0,0 +1,43 @@ +package janggi.domain.rule.route; + +import static janggi.domain.rule.route.Direction.BACK; +import static janggi.domain.rule.route.Direction.FRONT; +import static janggi.domain.rule.route.Direction.LEFT; +import static janggi.domain.rule.route.Direction.RIGHT; + +import janggi.domain.Location; +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("java:S6548") +public class StraightRouteProvider extends RouteProvider { + + private static final StraightRouteProvider INSTANCE = new StraightRouteProvider(); + + private StraightRouteProvider() { + } + + public static StraightRouteProvider getInstance() { + return INSTANCE; + } + + @Override + public Optional> calculateRoute(Location from, Location to) { + int maxDistance = calculateMaxDistance(from, to); + + List possibleRoutes = List.of( + Route.of(FRONT, maxDistance), + Route.of(BACK, maxDistance), + Route.of(LEFT, maxDistance), + Route.of(RIGHT, maxDistance) + ); + + return findValidPath(from, to, possibleRoutes); + } + + private int calculateMaxDistance(Location from, Location to) { + int horizontalDiff = Math.abs(from.calculateHorizontalDiff(to)); + int verticalDiff = Math.abs(from.calculateVerticalDiff(to)); + return Math.max(horizontalDiff, verticalDiff); + } +} diff --git a/src/main/java/janggi/strategy/ArrangementStrategy.java b/src/main/java/janggi/strategy/ArrangementStrategy.java new file mode 100644 index 0000000000..f6ce1fc2a5 --- /dev/null +++ b/src/main/java/janggi/strategy/ArrangementStrategy.java @@ -0,0 +1,8 @@ +package janggi.strategy; + +import janggi.domain.piece.Piece; + +public interface ArrangementStrategy { + + void place(Piece[][] arrangement); +} diff --git a/src/main/java/janggi/strategy/BoardAssembler.java b/src/main/java/janggi/strategy/BoardAssembler.java new file mode 100644 index 0000000000..e5d0e86368 --- /dev/null +++ b/src/main/java/janggi/strategy/BoardAssembler.java @@ -0,0 +1,40 @@ +package janggi.strategy; + +import janggi.domain.piece.EmptyPiece; +import janggi.domain.piece.Piece; +import java.util.Arrays; +import java.util.List; + +public class BoardAssembler { + + private static final int DEFAULT_ROWS = 10; + private static final int DEFAULT_COLS = 9; + + private final List strategies; + + private BoardAssembler(List strategies) { + this.strategies = strategies; + } + + public static BoardAssembler from(List strategies) { + return new BoardAssembler(strategies); + } + + public Piece[][] assemble() { + Piece[][] arrangement = new Piece[DEFAULT_ROWS][DEFAULT_COLS]; + + setupEmptyPieces(arrangement); + + for (ArrangementStrategy strategy : strategies) { + strategy.place(arrangement); + } + + return arrangement; + } + + private void setupEmptyPieces(Piece[][] arrangement) { + for (Piece[] row : arrangement) { + Arrays.fill(row, EmptyPiece.getInstance()); + } + } +} diff --git a/src/main/java/janggi/strategy/MaSangArrangementStrategy.java b/src/main/java/janggi/strategy/MaSangArrangementStrategy.java new file mode 100644 index 0000000000..0f611b751a --- /dev/null +++ b/src/main/java/janggi/strategy/MaSangArrangementStrategy.java @@ -0,0 +1,115 @@ +package janggi.strategy; + +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceFactory; +import janggi.domain.piece.PieceType; +import java.util.List; + +public class MaSangArrangementStrategy implements ArrangementStrategy { + + private static final List COLS_OF_MA_SANG = List.of(1, 2, 6, 7); + private static final PieceFactory FACTORY = PieceFactory.getInstance(); + + private final Side side; + private final List maSangArrangement; + + private MaSangArrangementStrategy(Side side, List maSangArrangement) { + this.side = side; + this.maSangArrangement = maSangArrangement; + } + + public static MaSangArrangementStrategy of(Side side, List maSangArrangement) { + return new MaSangArrangementStrategy(side, maSangArrangement); + } + + @Override + public void place(Piece[][] arrangement) { + placeDefaultPieces(arrangement, side); + placeVariablePieces(arrangement, side); + } + + private void placeVariablePieces(Piece[][] arrangement, Side side) { + int boardMaxLength = arrangement.length; + int row = calculateInitialRow(boardMaxLength, side); + + for (int index = 0; index < COLS_OF_MA_SANG.size(); index++) { + int col = COLS_OF_MA_SANG.get(index); + PieceType pieceType = maSangArrangement.get(index); + arrangement[row][col] = FACTORY.createActivePiece(pieceType, side); + } + } + + private int calculateInitialRow(int boardMaxLength, Side side) { + if (side.equals(Side.HAN)) { + return 0; + } + if (side.equals(Side.CHO)) { + return boardMaxLength - 1; + } + throw new IllegalArgumentException("진영이 존재하지 않아 시작 행을 찾을 수 없습니다."); + } + + private void placeDefaultPieces(Piece[][] grid, Side side) { + for (DefaultPieceFactory factory : DefaultPieceFactory.values()) { + setUpPiece(grid, side, factory); + } + } + + private void setUpPiece(Piece[][] grid, Side side, DefaultPieceFactory factory) { + int height = grid.length; + for (int col : factory.getCols()) { + int row = factory.getRow(side, height); + grid[row][col] = factory.createPiece(side); + } + } + + private enum DefaultPieceFactory { + CHA(0, List.of(0, 8), PieceType.CHA), + SA(0, List.of(3, 5), PieceType.SA), + GUNG(1, List.of(4), PieceType.GUNG), + PO(2, List.of(1, 7), PieceType.PO), + JOLBYEONG(3, List.of(0, 2, 4, 6, 8), PieceType.JOL); + + private final int row; + private final List cols; + private final PieceType pieceType; + + DefaultPieceFactory(int row, List cols, PieceType pieceType) { + this.row = row; + this.cols = cols; + this.pieceType = pieceType; + } + + private Piece createPiece(Side side) { + if (this == JOLBYEONG) { + return FACTORY.createActivePiece(getJolOrByeongBySide(side), side); + } + return FACTORY.createActivePiece(this.pieceType, side); + } + + private PieceType getJolOrByeongBySide(Side side) { + if (side == Side.HAN) { + return PieceType.BYEONG; + } + if (side == Side.CHO) { + return PieceType.JOL; + } + throw new IllegalArgumentException("진영이 존재하지 않아 기물명을 정할 수 없습니다."); + } + + private int getRow(Side side, int height) { + if (side.equals(Side.HAN)) { + return row; + } + if (side.equals(Side.CHO)) { + return height - row - 1; + } + throw new UnsupportedOperationException("진영이 존재하지 않아 시작행을 선택할 수 없습니다."); + } + + private List getCols() { + return cols; + } + } +} diff --git a/src/main/java/janggi/view/ApplicationView.java b/src/main/java/janggi/view/ApplicationView.java new file mode 100644 index 0000000000..87e12eb4f5 --- /dev/null +++ b/src/main/java/janggi/view/ApplicationView.java @@ -0,0 +1,51 @@ +package janggi.view; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +public class ApplicationView { + + private final Output outputWriter; + private final Input inputReader; + + public ApplicationView(Output outputWriter, Input inputReader) { + this.outputWriter = outputWriter; + this.inputReader = inputReader; + } + + public int promptForArrangementStrategyDecision(String side, Map strategies) { + outputWriter.printPromptMessage(side + "팀의 초기화 전략 번호를 입력해주세요."); + + for (Entry strategy : strategies.entrySet()) { + String strategyDecisionOption = String.format("%d. %s", strategy.getKey(), strategy.getValue()); + outputWriter.printPromptMessage(strategyDecisionOption); + } + + return inputReader.readInt(); + } + + public void showBoardArray(List> stringMatrix) { + outputWriter.printStringMatrix(stringMatrix); + } + + public void showCurrentSide(String currentSide) { + outputWriter.printPromptMessage(currentSide + "팀의 차례입니다."); + } + + public List promptForLocationOfPiece() { + outputWriter.printPromptMessage("이동시킬 기물의 좌표를 입력해주세요. (,로 구분)"); + + return inputReader.readIntegers(); + } + + public List promptForLocationToMove() { + outputWriter.printPromptMessage("해당 기물이 이동할 좌표를 입력해주세요. (,로 구분)"); + + return inputReader.readIntegers(); + } + + public void showErrorMessage(String errorMessage) { + outputWriter.printErrorMessage(errorMessage); + } +} diff --git a/src/main/java/janggi/view/ConsoleReader.java b/src/main/java/janggi/view/ConsoleReader.java new file mode 100644 index 0000000000..525570c56b --- /dev/null +++ b/src/main/java/janggi/view/ConsoleReader.java @@ -0,0 +1,59 @@ +package janggi.view; + +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +public class ConsoleReader implements Input { + + private static final String NUMERIC_FORMAT_REGEX = "-?\\d+"; + + private final Scanner scanner; + + public ConsoleReader() { + this.scanner = new Scanner(System.in); + } + + @Override + public int readInt() { + String input = scanner.nextLine().trim(); + validateIsBlank(input); + validateIsNumeric(input); + return parseToInt(input); + } + + @Override + public List readIntegers() { + String input = scanner.nextLine().trim(); + validateIsBlank(input); + String[] strings = input.split("\\s*,\\s*"); + List integers = Arrays.stream(strings) + .map(s -> { + validateIsBlank(s); + validateIsNumeric(s); + return parseToInt(s); + }).toList(); + return List.copyOf(integers); + } + + private void validateIsBlank(String input) { + if (input.isBlank()) { + throw new IllegalArgumentException("공백은 허용되지 않습니다."); + } + } + + private void validateIsNumeric(String input) { + if (input.matches(NUMERIC_FORMAT_REGEX)) { + return; + } + throw new IllegalArgumentException("숫자가 아닌 문자를 입력할 수 없습니다."); + } + + private int parseToInt(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("정수 범위를 초과할 수 없습니다."); + } + } +} diff --git a/src/main/java/janggi/view/ConsoleWriter.java b/src/main/java/janggi/view/ConsoleWriter.java new file mode 100644 index 0000000000..f1d5886607 --- /dev/null +++ b/src/main/java/janggi/view/ConsoleWriter.java @@ -0,0 +1,51 @@ +package janggi.view; + +import java.util.ArrayList; +import java.util.List; + +public class ConsoleWriter implements Output { + + private static final String BLANK = " "; + private static final List FULL_WIDTH_NUMBERS = + List.of("", "1", "2", "3", "4", "5", "6", "7", "8", "9"); + + @Override + public void printPromptMessage(String promptMessage) { + System.out.println(promptMessage); + } + + @Override + public void printErrorMessage(String errorMessage) { + System.out.println("[ERROR] " + errorMessage); + System.out.println(); + } + + @Override + public void printStringMatrix(List> matrix) { + StringBuilder matrixSnapshot = new StringBuilder(); + + List colIndexInfo = createCoordinationIndexInfo(matrix.getFirst().size()); + matrixSnapshot.append(String.join("", colIndexInfo)).append("\n"); + + for (int row = 0; row < matrix.size(); row++) { + matrixSnapshot.append(String.format("%2d ", row + 1)); + + for (String cell : matrix.get(row)) { + matrixSnapshot.append(cell).append(" "); + } + matrixSnapshot.append("\n"); + } + + System.out.println(); + System.out.println(matrixSnapshot); + } + + private List createCoordinationIndexInfo(int length) { + List coordinationInfo = new ArrayList<>(); + coordinationInfo.add(BLANK); + for (int index = 1; index <= length; index++) { + coordinationInfo.add(FULL_WIDTH_NUMBERS.get(index) + " "); + } + return coordinationInfo; + } +} diff --git a/src/main/java/janggi/view/Input.java b/src/main/java/janggi/view/Input.java new file mode 100644 index 0000000000..f2783c9914 --- /dev/null +++ b/src/main/java/janggi/view/Input.java @@ -0,0 +1,10 @@ +package janggi.view; + +import java.util.List; + +public interface Input { + + int readInt(); + + List readIntegers(); +} diff --git a/src/main/java/janggi/view/Output.java b/src/main/java/janggi/view/Output.java new file mode 100644 index 0000000000..e539f5b9d6 --- /dev/null +++ b/src/main/java/janggi/view/Output.java @@ -0,0 +1,12 @@ +package janggi.view; + +import java.util.List; + +public interface Output { + + void printPromptMessage(String promptMessage); + + void printErrorMessage(String errorMessage); + + void printStringMatrix(List> matrix); +} diff --git a/src/test/java/.gitkeep b/src/test/java/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/test/java/janggi/domain/board/BoardTest.java b/src/test/java/janggi/domain/board/BoardTest.java new file mode 100644 index 0000000000..8dace052a5 --- /dev/null +++ b/src/test/java/janggi/domain/board/BoardTest.java @@ -0,0 +1,243 @@ +package janggi.domain.board; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import janggi.domain.Location; +import janggi.domain.Side; +import janggi.domain.piece.EmptyPiece; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceType; +import janggi.strategy.ArrangementStrategy; +import janggi.strategy.BoardAssembler; +import janggi.support.TestArrangementStrategy; +import janggi.support.TestPiece; +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class BoardTest { + + private static final Piece EMPTY = EmptyPiece.getInstance(); + + @Test + @DisplayName("보드가 전략에 맞춰 정상적으로 생성된다.") + void shouldReturnBoardWithSelectedArrangementStrategy() { + // given + Side currentSide = Side.HAN; + Piece testPiece = new TestPiece(PieceType.CHA, currentSide); + Map initialPieces = Map.of( + new Location(1, 1), testPiece, + new Location(2, 2), EMPTY + ); + ArrangementStrategy strategy = new TestArrangementStrategy(initialPieces); + + // when + BoardAssembler assembler = BoardAssembler.from(List.of(strategy, strategy)); + Board board = Board.create(assembler); + List> pieces = board.to2DArray(); + + // then + Assertions.assertThat(pieces.get(1).get(1)).isEqualTo(testPiece); + Assertions.assertThat(pieces.get(2).get(2)).isEqualTo(EMPTY); + } + + @Nested + class ValidateLocationExistenceTest { + @Test + @DisplayName("보드에 입력받은 좌표가 존재하면 예외를 발생시키지 않는다.") + void shouldNotThrowExceptionWhenLocationExists() { + // given + Side currentSide = Side.HAN; + Map initialPieces = Map.of( + new Location(1, 1), new TestPiece(PieceType.CHA, currentSide) + ); + ArrangementStrategy strategy = new TestArrangementStrategy(initialPieces); + + BoardAssembler assembler = BoardAssembler.from(List.of(strategy, strategy)); + Board board = Board.create(assembler); + Location location = new Location(1, 1); + + // when & then + assertDoesNotThrow(() -> board.validateLocationOfPiece(currentSide, location)); + } + + @Test + @DisplayName("보드에 입력받은 좌표가 존재하지 않으면 예외를 발생시킨다.") + void shouldThrowExceptionWhenLocationDoesNotExist() { + // given + Side currentSide = Side.HAN; + Map initialPieces = Map.of( + new Location(1, 1), new TestPiece(PieceType.CHA, currentSide) + ); + ArrangementStrategy strategy = new TestArrangementStrategy(initialPieces); + + BoardAssembler assembler = BoardAssembler.from(List.of(strategy, strategy)); + Board board = Board.create(assembler); + Location location = new Location(11, 11); + + // when & then + Assertions.assertThatThrownBy(() -> board.validateLocationOfPiece(currentSide, location)) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Nested + class ValidateLocationOfPieceTest { + @Test + @DisplayName("출발 좌표에 같은 팀 기물이 존재하면 예외를 발생시키지 않는다.") + void shouldNotThrowExceptionWhenPieceOnSameSideExistsAtLocation() { + // given + Side currentSide = Side.HAN; + Map initialPieces = Map.of( + new Location(1, 1), new TestPiece(PieceType.CHA, currentSide) + ); + ArrangementStrategy strategy = new TestArrangementStrategy(initialPieces); + + BoardAssembler assembler = BoardAssembler.from(List.of(strategy, strategy)); + Board board = Board.create(assembler); + Location location = new Location(1, 1); + + // when & then + assertDoesNotThrow(() -> board.validateLocationOfPiece(currentSide, location)); + } + + @Test + @DisplayName("출발 좌표에 상대 팀 기물이 존재하면 예외를 발생시킨다.") + void shouldThrowExceptionWhenPieceOnOtherSideExistsAtLocation() { + // given + Side currentSide = Side.HAN; + Side otherSide = Side.CHO; + Map initialPieces = Map.of( + new Location(1, 1), new TestPiece(PieceType.CHA, currentSide) + ); + ArrangementStrategy strategy = new TestArrangementStrategy(initialPieces); + + BoardAssembler assembler = BoardAssembler.from(List.of(strategy, strategy)); + Board board = Board.create(assembler); + Location location = new Location(1, 1); + + // when & then + Assertions.assertThatThrownBy(() -> board.validateLocationOfPiece(otherSide, location)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("출발 좌표에 기물이 존재하지 않으면 예외를 발생시킨다.") + void shouldThrowExceptionWhenPieceDoesNotExistsAtLocation() { + // given + Map initialPieces = Map.of( + new Location(5, 5), EMPTY + ); + ArrangementStrategy strategy = new TestArrangementStrategy(initialPieces); + + BoardAssembler assembler = BoardAssembler.from(List.of(strategy, strategy)); + Board board = Board.create(assembler); + Location location = new Location(5, 5); + + // when & then + Assertions.assertThatThrownBy(() -> board.validateLocationOfPiece(Side.HAN, location)) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Nested + class ValidateLocationToMoveTest { + @Test + @DisplayName("도착 좌표에 기물이 존재하지 않으면 예외를 발생시키지 않는다.") + void shouldNotThrowExceptionWhenPieceDoesNotExistsAtLocation() { + // given + Map initialPieces = Map.of( + new Location(1, 1), EMPTY + ); + ArrangementStrategy strategy = new TestArrangementStrategy(initialPieces); + + BoardAssembler assembler = BoardAssembler.from(List.of(strategy, strategy)); + Board board = Board.create(assembler); + Location from = new Location(0, 0); + Location to = new Location(1, 1); + + // when & then + Assertions.assertThatNoException() + .isThrownBy(() -> board.validateLocationToMove(from, to)); + } + + @Test + @DisplayName("도착 좌표에 상대 팀 기물이 존재하면 예외를 발생시키지 않는다.") + void shouldThrowExceptionWhenPieceOnOtherSideExistsAtLocation() { + // given + Side otherSide = Side.CHO; + Map initialPieces = Map.of( + new Location(1, 1), new TestPiece(PieceType.CHA, otherSide) + ); + ArrangementStrategy strategy = new TestArrangementStrategy(initialPieces); + + BoardAssembler assembler = BoardAssembler.from(List.of(strategy, strategy)); + Board board = Board.create(assembler); + Location from = new Location(0, 0); + Location to = new Location(1, 1); + + // when & then + Assertions.assertThatNoException() + .isThrownBy(() -> board.validateLocationToMove(from, to)); + } + } + + @Test + @DisplayName("이동할 기물의 위치와 도착지 좌표를 받아 기물을 이동시킨다.") + void shouldMovePieceToDestination() { + // given + Piece testPiece = new TestPiece(PieceType.CHA, Side.HAN); + Map initialPieces = Map.of( + new Location(1, 1), testPiece + ); + ArrangementStrategy strategy = new TestArrangementStrategy(initialPieces); + + BoardAssembler assembler = BoardAssembler.from(List.of(strategy, strategy)); + Board board = Board.create(assembler); + Location from = new Location(1, 1); + Location to = new Location(0, 0); + + // when + board.move(from, to); + List> board2DArray = board.to2DArray(); + + // then + Assertions.assertThat(board2DArray.get(from.y()).get(from.x()).isEmpty()).isTrue(); + Assertions.assertThat(board2DArray.get(to.y()).get(to.x())).isEqualTo(testPiece); + } + + @Test + @DisplayName("보드에 기물이 하나라도 있으면 true를 반환한다") + void shouldReturnTrueForNoneEmptyBoard() { + // given + Map initialPieces = Map.of( + new Location(1, 1), new TestPiece(PieceType.CHA, Side.HAN), + new Location(1, 2), EMPTY + ); + ArrangementStrategy strategy = new TestArrangementStrategy(initialPieces); + BoardAssembler assembler = BoardAssembler.from(List.of(strategy, strategy)); + Board board = Board.create(assembler); + + // when & then + Assertions.assertThat(board.isNotEmpty()).isTrue(); + } + + @Test + @DisplayName("보드에 기물이 존재하지 않으면 false를 반환한다.") + void shouldReturnTrueForEmptyBoard() { + // given + Map initialPieces = Map.of( + new Location(1, 1), EMPTY, + new Location(1, 2), EMPTY + ); + ArrangementStrategy strategy = new TestArrangementStrategy(initialPieces); + BoardAssembler assembler = BoardAssembler.from(List.of(strategy, strategy)); + Board board = Board.create(assembler); + + // when & then + Assertions.assertThat(board.isNotEmpty()).isFalse(); + } +} diff --git a/src/test/java/janggi/domain/rule/JolbyeongMovementTest.java b/src/test/java/janggi/domain/rule/JolbyeongMovementTest.java new file mode 100644 index 0000000000..386f36c071 --- /dev/null +++ b/src/test/java/janggi/domain/rule/JolbyeongMovementTest.java @@ -0,0 +1,73 @@ +package janggi.domain.rule; + +import janggi.domain.Location; +import janggi.domain.Side; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class JolbyeongMovementTest { + + @Nested + class CalculateRouteTest { + @Test + @DisplayName("한팀 졸병이 이동할 수 있는 위치로 경로를 계산하면, 이동 경로를 반환한다.") + void shouldReturnRouteForReachableLocationWhenTeamHan() { + // given + Location from = new Location(0, 0); + Location to = new Location(0, 1); + JolbyeongMovement movement = JolbyeongMovement.getInstanceBySide(Side.HAN); + + // when & then + Assertions.assertThat(movement.calculateRoute(from, to)).hasValue(List.of(to)); + } + + @Test + @DisplayName("초팀 졸병이 이동할 수 있는 위치로 경로를 계산하면, 이동 경로를 반환한다.") + void shouldReturnRouteForReachableLocationWhenTeamCho() { + // given + Location from = new Location(0, 1); + Location to = new Location(0, 0); + JolbyeongMovement movement = JolbyeongMovement.getInstanceBySide(Side.CHO); + + // when & then + Assertions.assertThat(movement.calculateRoute(from, to)).hasValue(List.of(to)); + } + + @Test + @DisplayName("한팀 졸병이 이동할 수 없는 위치로 경로를 계산하면, 빈 결과를 반환한다.") + void shouldReturnEmptyForUnReachableLocationWhenTeamHan() { + // given + Location from = new Location(0, 1); + Location to = new Location(0, 0); + JolbyeongMovement movement = JolbyeongMovement.getInstanceBySide(Side.HAN); + + // when & then + Assertions.assertThat(movement.calculateRoute(from, to)).isEmpty(); + } + + @ParameterizedTest + @DisplayName("초팀 졸병이 이동할 수 없는 위치로 경로를 계산하면, 빈 결과를 반환한다.") + @MethodSource("provideUnreachableCoordination") + void shouldReturnEmptyForUnReachableLocationWhenTeamCho(Location destination) { + // given + Location from = new Location(0, 0); + JolbyeongMovement movement = JolbyeongMovement.getInstanceBySide(Side.CHO); + + // when & then + Assertions.assertThat(movement.calculateRoute(from, destination)).isEmpty(); + } + + static List provideUnreachableCoordination() { + return List.of( + new Location(0,2), // 거리가 멀어서 도달할 수 없는 경우 + new Location(1,1), // 대각선으로 이동하는 경우 + new Location(0,1) // 뒤로 이동하는 경우 + ); + } + } +} diff --git a/src/test/java/janggi/domain/rule/collision/DefaultCollisionDetectorTest.java b/src/test/java/janggi/domain/rule/collision/DefaultCollisionDetectorTest.java new file mode 100644 index 0000000000..ab8e9990f5 --- /dev/null +++ b/src/test/java/janggi/domain/rule/collision/DefaultCollisionDetectorTest.java @@ -0,0 +1,62 @@ +package janggi.domain.rule.collision; + +import janggi.domain.Side; +import janggi.domain.piece.EmptyPiece; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceType; +import janggi.support.TestPiece; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DefaultCollisionDetectorTest { + + private static final DefaultCollisionDetector DEFAULT_COLLISION_DETECTOR = DefaultCollisionDetector.getInstance(); + private static final Piece EMPTY = EmptyPiece.getInstance(); + + @Test + @DisplayName("이동 경로에 장애물이 존재하지 않고, 마지막 경로에 위치한 기물이 존재하지 않으면 예외를 반환하지 않는다.") + void shouldNotThrowExceptionWhenNoPieceOnPathAndNoPieceOnDestination() { + // given + List piecesOnPath = List.of(EMPTY, EMPTY); + + // when & then + Assertions.assertThatNoException() + .isThrownBy(() -> DEFAULT_COLLISION_DETECTOR.check(Side.CHO, piecesOnPath)); + } + + @Test + @DisplayName("이동 경로에 장애물이 존재하지 않고, 마지막 경로에 위치한 기물이 상대팀이면 예외를 반환하지 않는다.") + void shouldNotThrowExceptionWhenNoPieceOnPathAndPieceOnDestinationIsOtherSide() { + // given + List piecesOnPath = List.of(EMPTY, EMPTY, new TestPiece(PieceType.CHA, Side.HAN)); + + // when & then + Assertions.assertThatNoException() + .isThrownBy(() -> DEFAULT_COLLISION_DETECTOR.check(Side.CHO, piecesOnPath)); + } + + @Test + @DisplayName("이동 경로에 장애물이 존재하는 경우 예외를 발생시킨다.") + void shouldThrowExceptionWhenPieceOnPathExist() { + // given + List piecesOnPath = List.of(new TestPiece(PieceType.CHA, Side.HAN), EMPTY, EMPTY); + + // when & then + Assertions.assertThatThrownBy(() -> DEFAULT_COLLISION_DETECTOR.check(Side.CHO, piecesOnPath)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("이동 경로에 장애물이 존재하지 않고, 마지막 경로에 위치한 기물이 우리팀이면 예외를 발생시킨다.") + void shouldThrowExceptionWhenNoPieceOnPathAndPieceOnDestinationIsMySide() { + // given + Side side = Side.HAN; + List piecesOnPath = List.of(EMPTY, EMPTY, new TestPiece(PieceType.CHA, side)); + + // when & then + Assertions.assertThatThrownBy(() -> DEFAULT_COLLISION_DETECTOR.check(side, piecesOnPath)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/janggi/domain/rule/collision/PoCollisionDetectorTest.java b/src/test/java/janggi/domain/rule/collision/PoCollisionDetectorTest.java new file mode 100644 index 0000000000..200348ff32 --- /dev/null +++ b/src/test/java/janggi/domain/rule/collision/PoCollisionDetectorTest.java @@ -0,0 +1,94 @@ +package janggi.domain.rule.collision; + +import janggi.domain.Side; +import janggi.domain.piece.EmptyPiece; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceType; +import janggi.support.TestPiece; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PoCollisionDetectorTest { + + private static final PoCollisionDetector PO_COLLISION_DETECTOR = PoCollisionDetector.getInstance(); + private static final Piece EMPTY = EmptyPiece.getInstance(); + + @Test + @DisplayName("이동 경로에 포가 아닌 장애물이 1개 존재하고, 마지막 경로 좌표에 기물이 존재하지 않으면 예외를 반환하지 않는다.") + void shouldNotThrowExceptionWhenOneNonPoObstacleAndEmptyDestination() { + // given + List piecesOnPath = List.of(EMPTY, new TestPiece(PieceType.CHA, Side.HAN), EMPTY); + + // when & then + Assertions.assertThatNoException() + .isThrownBy(() -> PO_COLLISION_DETECTOR.check(Side.CHO, piecesOnPath)); + } + + @Test + @DisplayName("이동 경로에 포가 아닌 장애물이 1개 존재하고, 마지막 경로에 위치한 기물이 상대팀이면 예외를 반환하지 않는다.") + void shouldNotThrowExceptionWhenOneNonPoObstacleAndEnemyAtDestination() { + // given + List piecesOnPath = List.of(EMPTY, new TestPiece(PieceType.CHA, Side.HAN), new TestPiece(PieceType.CHA, Side.HAN)); + + // when & then + Assertions.assertThatNoException() + .isThrownBy(() -> PO_COLLISION_DETECTOR.check(Side.CHO, piecesOnPath)); + } + + @Test + @DisplayName("이동 경로에 장애물이 2개 이상 존재하는 경우 예외를 발생시킨다.") + void shouldThrowExceptionWhenTwoOrMorePieceOnPathExist() { + // given + List piecesOnPath = List.of(new TestPiece(PieceType.CHA, Side.HAN), new TestPiece(PieceType.CHA, Side.HAN), EMPTY); + + // when & then + Assertions.assertThatThrownBy(() -> PO_COLLISION_DETECTOR.check(Side.CHO, piecesOnPath)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("이동 경로에 장애물이 존재하지 않는 경우 예외를 발생시킨다.") + void shouldThrowExceptionWhenNoObstacleOnPath() { + // given + List piecesOnPath = List.of(EMPTY, EMPTY, new TestPiece(PieceType.CHA, Side.HAN)); + + // when & then + Assertions.assertThatThrownBy(() -> PO_COLLISION_DETECTOR.check(Side.CHO, piecesOnPath)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("이동 경로에 포가 존재하는 경우 예외를 발생시킨다.") + void shouldThrowExceptionWhenPoOnPath() { + // given + List piecesOnPath = List.of(EMPTY, new TestPiece(PieceType.PO, Side.HAN), new TestPiece(PieceType.CHA, Side.HAN)); + + // when & then + Assertions.assertThatThrownBy(() -> PO_COLLISION_DETECTOR.check(Side.CHO, piecesOnPath)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("도착지에 상대팀의 포가 존재하는 경우 예외를 발생시킨다.") + void shouldThrowExceptionWhenPoOfOtherSideOnDestination() { + // given + List piecesOnPath = List.of(EMPTY, EMPTY, new TestPiece(PieceType.PO, Side.HAN)); + + // when & then + Assertions.assertThatThrownBy(() -> PO_COLLISION_DETECTOR.check(Side.CHO, piecesOnPath)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("도착지에 우리팀 기물이 존재하는 경우 예외를 발생시킨다.") + void shouldThrowExceptionWhenSameSidePieceOnDestination() { + // given + List piecesOnPath = List.of(EMPTY, EMPTY, new TestPiece(PieceType.CHA, Side.CHO)); + + // when & then + Assertions.assertThatThrownBy(() -> PO_COLLISION_DETECTOR.check(Side.CHO, piecesOnPath)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/janggi/domain/rule/route/DefaultRouteProviderTest.java b/src/test/java/janggi/domain/rule/route/DefaultRouteProviderTest.java new file mode 100644 index 0000000000..ae94454038 --- /dev/null +++ b/src/test/java/janggi/domain/rule/route/DefaultRouteProviderTest.java @@ -0,0 +1,46 @@ +package janggi.domain.rule.route; + +import janggi.domain.Location; +import java.util.List; +import java.util.Optional; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DefaultRouteProviderTest { + + @Test + @DisplayName("시작 위치에서 도착 위치까지 도달 가능하다면, 그 사이 경로 좌표를 반환한다.") + void shouldReturnLocationsWhenValidPathIsFound() { + // given + Location from = new Location(1,1); + Location to = new Location(1,5); + List possibleRoutes = List.of(Route.of(Direction.FRONT, 4)); + RouteProvider routeProvider = new DefaultRouteProvider(possibleRoutes); + + // when + Optional> locationsOfValidPath = routeProvider.calculateRoute(from, to); + + // then + Assertions.assertThat(locationsOfValidPath).isPresent(); + Assertions.assertThat(locationsOfValidPath.get()).containsExactly( + new Location(1,2), + new Location(1,3), + new Location(1,4), + new Location(1,5) + ); + } + + @Test + @DisplayName("시작 위치에서 도착 위치까지 도달 불가능한 경우 빈 결과를 반환한다.") + void shouldReturnEmptyWhenValidPathIsNotFound() { + // given + Location from = new Location(1,1); + Location to = new Location(5,1); + List possibleRoutes = List.of(Route.of(Direction.FRONT, 4)); + RouteProvider routeProvider = new DefaultRouteProvider(possibleRoutes); + + // when & then + Assertions.assertThat(routeProvider.calculateRoute(from, to)).isEmpty(); + } +} diff --git a/src/test/java/janggi/domain/rule/route/GungSeongRouteProviderTest.java b/src/test/java/janggi/domain/rule/route/GungSeongRouteProviderTest.java new file mode 100644 index 0000000000..0f0c58ede7 --- /dev/null +++ b/src/test/java/janggi/domain/rule/route/GungSeongRouteProviderTest.java @@ -0,0 +1,56 @@ +package janggi.domain.rule.route; + +import janggi.domain.Location; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class GungSeongRouteProviderTest { + + private static final GungSeongRouteProvider GUNG_SEONG_ROUTE_PROVIDER = GungSeongRouteProvider.getInstance(); + + @ParameterizedTest + @DisplayName("궁성 안에서 이동할 수 있는 위치로 경로를 계산하면, 이동 경로를 반환한다.") + @MethodSource("provideReachableCoordination") + void shouldReturnRouteForReachableLocation(List destination) { + // given + Location from = Location.from(List.of(4, 1)); + Location to = Location.from(destination); + + // when & then + Assertions.assertThat(GUNG_SEONG_ROUTE_PROVIDER.calculateRoute(from, to)).hasValue(List.of(to)); + } + + static List> provideReachableCoordination() { + return List.of( + List.of(4, 2), //상 + List.of(4, 0), //하 + List.of(3, 1), //좌 + List.of(5, 1), //우 + List.of(5, 2), //우대각 + List.of(3, 2) //좌대각 + ); + } + + @ParameterizedTest + @DisplayName("궁성을 벗어난 위치로 경로를 계산하면, 빈 결과를 반환한다.") + @MethodSource("provideUnreachableCoordination") + void shouldReturnEmptyForUnReachableLocation(List coordination) { + // given + Location from = Location.from(List.of(3, 2)); + Location to = Location.from(coordination); + + // when & then + Assertions.assertThat(GUNG_SEONG_ROUTE_PROVIDER.calculateRoute(from, to)).isEmpty(); + } + + static List> provideUnreachableCoordination() { + return List.of( + List.of(3, 4), + List.of(5, 2), + List.of(3, 5) + ); + } +} diff --git a/src/test/java/janggi/domain/rule/route/RouteTest.java b/src/test/java/janggi/domain/rule/route/RouteTest.java new file mode 100644 index 0000000000..ab3679742e --- /dev/null +++ b/src/test/java/janggi/domain/rule/route/RouteTest.java @@ -0,0 +1,37 @@ +package janggi.domain.rule.route; + +import janggi.domain.Location; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RouteTest { + + @Test + @DisplayName("현재 위치에서 주어진 Direction 들을 적용했을 때 지나는 경로를 반환한다.") + void shouldReturnRouteLocationsCalculatedByCurrentLocation() { + // given + Location current = Location.from(List.of(0,0)); + Route route = Route.from( + List.of( + Direction.FRONT, // 0,1 + Direction.FRONT_LEFT, // -1, 2 + Direction.FRONT_RIGHT, // 0, 3 + Direction.BACK // 0, 2 + ) + ); + List expected = List.of( + Location.from(List.of(0,1)), + Location.from(List.of(-1,2)), + Location.from(List.of(0,3)), + Location.from(List.of(0,2)) + ); + + // when + List routeLocations = route.calculateLocationsOnPath(current); + + // then + Assertions.assertThat(routeLocations).isEqualTo(expected); + } +} diff --git a/src/test/java/janggi/domain/rule/route/StraightRouteProviderTest.java b/src/test/java/janggi/domain/rule/route/StraightRouteProviderTest.java new file mode 100644 index 0000000000..22643269c2 --- /dev/null +++ b/src/test/java/janggi/domain/rule/route/StraightRouteProviderTest.java @@ -0,0 +1,66 @@ +package janggi.domain.rule.route; + +import janggi.domain.Location; +import java.util.List; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class StraightRouteProviderTest { + + private static final StraightRouteProvider STRAIGHT_ROUTE_PROVIDER = StraightRouteProvider.getInstance(); + + @ParameterizedTest + @DisplayName("직선으로 이동할 위치로 경로를 계산하면, 이동 경로를 반환한다.") + @MethodSource("provideReachableCoordination") + void shouldReturnRouteForReachableLocation(Location destination, List route) { + // given + Location from = new Location(2, 2); + + // when & then + Assertions.assertThat(STRAIGHT_ROUTE_PROVIDER.calculateRoute(from, destination)).hasValue(route); + } + + static Stream provideReachableCoordination() { + return Stream.of( + Arguments.of( + new Location(0, 2), // 상 + List.of(new Location(1, 2), new Location(0, 2)) + ), + Arguments.of( + new Location(5, 2), // 하 + List.of(new Location(3, 2), new Location(4, 2), new Location(5, 2)) + ), + Arguments.of( + new Location(2, 0), // 좌 + List.of(new Location(2, 1), new Location(2, 0)) + ), + Arguments.of( + new Location(2, 5), // 우 + List.of(new Location(2, 3), new Location(2, 4), new Location(2, 5)) + ) + ); + } + + @ParameterizedTest + @DisplayName("직선으로 이동이 불가능한 위치로 경로를 계산하면, 빈 결과를 반환한다.") + @MethodSource("provideUnreachableCoordination") + void shouldReturnEmptyForUnReachableLocation(Location destination) { + // given + Location from = new Location(1, 1); + + // when & then + Assertions.assertThat(STRAIGHT_ROUTE_PROVIDER.calculateRoute(from, destination)).isEmpty(); + } + + static List provideUnreachableCoordination() { + return List.of( + new Location(3, 3), + new Location(3, 2), + new Location(2, 2) + ); + } +} diff --git a/src/test/java/janggi/strategy/BoardAssemblerTest.java b/src/test/java/janggi/strategy/BoardAssemblerTest.java new file mode 100644 index 0000000000..4355d90455 --- /dev/null +++ b/src/test/java/janggi/strategy/BoardAssemblerTest.java @@ -0,0 +1,28 @@ +package janggi.strategy; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.domain.piece.EmptyPiece; +import janggi.domain.piece.Piece; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class BoardAssemblerTest { + + @Test + @DisplayName("조립기는 전략을 실행한 후, 기물이 없는 나머지 빈 칸을 EmptyPiece로 채운다.") + void shouldFillEmptySpaces() { + // given + ArrangementStrategy doNothingStrategy = arrangement -> {}; + + BoardAssembler assembler = BoardAssembler.from(List.of(doNothingStrategy, doNothingStrategy)); + + // when + Piece[][] board = assembler.assemble(); + + // then + assertThat(board[0][0]).isInstanceOf(EmptyPiece.class); + assertThat(board[5][5]).isInstanceOf(EmptyPiece.class); + } +} diff --git a/src/test/java/janggi/strategy/MaSangArrangementStrategyTest.java b/src/test/java/janggi/strategy/MaSangArrangementStrategyTest.java new file mode 100644 index 0000000000..b5de0011d2 --- /dev/null +++ b/src/test/java/janggi/strategy/MaSangArrangementStrategyTest.java @@ -0,0 +1,306 @@ +package janggi.strategy; + +import static org.assertj.core.api.Assertions.assertThat; + +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceType; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class MaSangArrangementStrategyTest { + + private static final List MA_SANG_MA_SANG = + List.of(PieceType.MA, PieceType.SANG, PieceType.MA, PieceType.SANG); + private static final List SANG_MA_SANG_MA = + List.of(PieceType.SANG, PieceType.MA, PieceType.SANG, PieceType.MA); + private static final List SANG_MA_MA_SANG = + List.of(PieceType.SANG, PieceType.MA, PieceType.MA, PieceType.SANG); + private static final List MA_SANG_SANG_MA = + List.of(PieceType.MA, PieceType.SANG, PieceType.SANG, PieceType.MA); + + @Test + @DisplayName("전략의 place를 호출하면 차, 포, 궁, 사, 졸 등 '기본 기물'이 올바른 위치에 배치된다.") + void shouldPlaceDefaultPieces() { + // given + Piece[][] board = new Piece[10][9]; + Side han = Side.HAN; + Side cho = Side.CHO; + MaSangArrangementStrategy hanStrategy = MaSangArrangementStrategy.of(han, MA_SANG_MA_SANG); + MaSangArrangementStrategy choStrategy = MaSangArrangementStrategy.of(cho, MA_SANG_MA_SANG); + + // when + hanStrategy.place(board); + choStrategy.place(board); + + // then: 한팀 + Piece chaOfHan = board[0][0]; + assertThat(chaOfHan.getPieceType()).isEqualTo(PieceType.CHA); + assertThat(chaOfHan.isSameSide(han)).isTrue(); + + Piece saOfHan = board[0][3]; + assertThat(saOfHan.getPieceType()).isEqualTo(PieceType.SA); + assertThat(saOfHan.isSameSide(han)).isTrue(); + + Piece gungOfHan = board[1][4]; + assertThat(gungOfHan.getPieceType()).isEqualTo(PieceType.GUNG); + assertThat(gungOfHan.isSameSide(han)).isTrue(); + + Piece poOfHan = board[2][1]; + assertThat(poOfHan.getPieceType()).isEqualTo(PieceType.PO); + assertThat(poOfHan.isSameSide(han)).isTrue(); + + Piece jolbyeongOfHan = board[3][0]; + assertThat(jolbyeongOfHan.getPieceType()).isEqualTo(PieceType.BYEONG); + assertThat(jolbyeongOfHan.isSameSide(han)).isTrue(); + + + // then: 초팀 + Piece chaOfCho = board[9][8]; + assertThat(chaOfCho.getPieceType()).isEqualTo(PieceType.CHA); + assertThat(chaOfCho.isSameSide(cho)).isTrue(); + + Piece gungOfCho = board[8][4]; + assertThat(gungOfCho.getPieceType()).isEqualTo(PieceType.GUNG); + assertThat(gungOfCho.isSameSide(cho)).isTrue(); + + Piece poOfCho = board[7][7]; + assertThat(poOfCho.getPieceType()).isEqualTo(PieceType.PO); + assertThat(poOfCho.isSameSide(cho)).isTrue(); + + Piece jolbyeongOfCho = board[6][8]; + assertThat(jolbyeongOfCho.getPieceType()).isEqualTo(PieceType.JOL); + assertThat(jolbyeongOfCho.isSameSide(cho)).isTrue(); + } + + @Nested + class MaSangMaSangTest { + @Test + @DisplayName("Han 팀의 Sang 객체와 Ma 객체를 MaSangMaSang의 위치에 생성해 넣어준다.") + void shouldPlaceMaSangMaSangWhenSideHan() { + // given + Piece[][] grid = new Piece[10][9]; + Side side = Side.HAN; + MaSangArrangementStrategy strategy = MaSangArrangementStrategy.of(side, MA_SANG_MA_SANG); + + // when + strategy.place(grid); + Piece leftMa = grid[0][1]; + Piece leftSang = grid[0][2]; + Piece rightMa = grid[0][6]; + Piece rightSang = grid[0][7]; + + // then + Assertions.assertThat(leftMa.getPieceType()).isEqualTo(PieceType.MA); + Assertions.assertThat(leftSang.getPieceType()).isEqualTo(PieceType.SANG); + Assertions.assertThat(rightMa.getPieceType()).isEqualTo(PieceType.MA); + Assertions.assertThat(rightSang.getPieceType()).isEqualTo(PieceType.SANG); + + Assertions.assertThat(leftMa.isSameSide(side)).isTrue(); + Assertions.assertThat(leftSang.isSameSide(side)).isTrue(); + Assertions.assertThat(rightMa.isSameSide(side)).isTrue(); + Assertions.assertThat(rightSang.isSameSide(side)).isTrue(); + } + + @Test + @DisplayName("Cho 팀의 Sang 객체와 Ma 객체를 MaSangMaSang의 위치에 생성해 넣어준다.") + void shouldPlaceMaSangMaSangWhenSideCho() { + // given + Piece[][] grid = new Piece[10][9]; + Side side = Side.CHO; + MaSangArrangementStrategy strategy = MaSangArrangementStrategy.of(side, MA_SANG_MA_SANG); + + // when + strategy.place(grid); + Piece leftMa = grid[9][1]; + Piece leftSang = grid[9][2]; + Piece rightMa = grid[9][6]; + Piece rightSang = grid[9][7]; + + // then + Assertions.assertThat(leftMa.getPieceType()).isEqualTo(PieceType.MA); + Assertions.assertThat(leftSang.getPieceType()).isEqualTo(PieceType.SANG); + Assertions.assertThat(rightMa.getPieceType()).isEqualTo(PieceType.MA); + Assertions.assertThat(rightSang.getPieceType()).isEqualTo(PieceType.SANG); + + Assertions.assertThat(leftMa.isSameSide(side)).isTrue(); + Assertions.assertThat(leftSang.isSameSide(side)).isTrue(); + Assertions.assertThat(rightMa.isSameSide(side)).isTrue(); + Assertions.assertThat(rightSang.isSameSide(side)).isTrue(); + } + } + + @Nested + class SangMaSangMaTest { + @Test + @DisplayName("Han 팀의 Sang 객체와 Ma 객체를 SangMaSangMa의 위치에 생성해 넣어준다.") + void shouldPlaceSangMaSangMaWhenSideHan() { + // given + Piece[][] grid = new Piece[10][9]; + Side side = Side.HAN; + MaSangArrangementStrategy strategy = MaSangArrangementStrategy.of(side, SANG_MA_SANG_MA); + + // when + strategy.place(grid); + Piece leftSang = grid[0][1]; + Piece leftMa = grid[0][2]; + Piece rightSang = grid[0][6]; + Piece rightMa = grid[0][7]; + + // then + Assertions.assertThat(leftSang.getPieceType()).isEqualTo(PieceType.SANG); + Assertions.assertThat(leftMa.getPieceType()).isEqualTo(PieceType.MA); + Assertions.assertThat(rightSang.getPieceType()).isEqualTo(PieceType.SANG); + Assertions.assertThat(rightMa.getPieceType()).isEqualTo(PieceType.MA); + + Assertions.assertThat(leftSang.isSameSide(side)).isTrue(); + Assertions.assertThat(leftMa.isSameSide(side)).isTrue(); + Assertions.assertThat(rightSang.isSameSide(side)).isTrue(); + Assertions.assertThat(rightMa.isSameSide(side)).isTrue(); + } + + @Test + @DisplayName("Cho 팀의 Sang 객체와 Ma 객체를 SangMaSangMa의 위치에 생성해 넣어준다.") + void shouldPlaceSangMaSangMaWhenSideCho() { + // given + Piece[][] grid = new Piece[10][9]; + Side side = Side.CHO; + MaSangArrangementStrategy strategy = MaSangArrangementStrategy.of(side, SANG_MA_SANG_MA); + + // when + strategy.place(grid); + Piece leftSang = grid[9][1]; + Piece leftMa = grid[9][2]; + Piece rightSang = grid[9][6]; + Piece rightMa = grid[9][7]; + + // then + Assertions.assertThat(leftSang.getPieceType()).isEqualTo(PieceType.SANG); + Assertions.assertThat(leftMa.getPieceType()).isEqualTo(PieceType.MA); + Assertions.assertThat(rightSang.getPieceType()).isEqualTo(PieceType.SANG); + Assertions.assertThat(rightMa.getPieceType()).isEqualTo(PieceType.MA); + + Assertions.assertThat(leftSang.isSameSide(side)).isTrue(); + Assertions.assertThat(leftMa.isSameSide(side)).isTrue(); + Assertions.assertThat(rightSang.isSameSide(side)).isTrue(); + Assertions.assertThat(rightMa.isSameSide(side)).isTrue(); + } + } + + @Nested + class SangMaMaSangTest { + @Test + @DisplayName("Han 팀의 Sang 객체와 Ma 객체를 SangMaMaSang의 위치에 생성해 넣어준다.") + void shouldPlaceSangMaMaSangWhenSideHan() { + // given + Piece[][] grid = new Piece[10][9]; + Side side = Side.HAN; + MaSangArrangementStrategy strategy = MaSangArrangementStrategy.of(side, SANG_MA_MA_SANG); + + // when + strategy.place(grid); + Piece leftSang = grid[0][1]; + Piece leftMa = grid[0][2]; + Piece rightMa = grid[0][6]; + Piece rightSang = grid[0][7]; + + // then + Assertions.assertThat(leftSang.getPieceType()).isEqualTo(PieceType.SANG); + Assertions.assertThat(leftMa.getPieceType()).isEqualTo(PieceType.MA); + Assertions.assertThat(rightMa.getPieceType()).isEqualTo(PieceType.MA); + Assertions.assertThat(rightSang.getPieceType()).isEqualTo(PieceType.SANG); + + Assertions.assertThat(leftSang.isSameSide(side)).isTrue(); + Assertions.assertThat(leftMa.isSameSide(side)).isTrue(); + Assertions.assertThat(rightMa.isSameSide(side)).isTrue(); + Assertions.assertThat(rightSang.isSameSide(side)).isTrue(); + } + + @Test + @DisplayName("Cho 팀의 Sang 객체와 Ma 객체를 SangMaMaSang의 위치에 생성해 넣어준다.") + void shouldPlaceSangMaMaSangWhenSideCho() { + // given + Piece[][] grid = new Piece[10][9]; + Side side = Side.CHO; + MaSangArrangementStrategy strategy = MaSangArrangementStrategy.of(side, SANG_MA_MA_SANG); + + // when + strategy.place(grid); + Piece leftSang = grid[9][1]; + Piece leftMa = grid[9][2]; + Piece rightMa = grid[9][6]; + Piece rightSang = grid[9][7]; + + // then + Assertions.assertThat(leftSang.getPieceType()).isEqualTo(PieceType.SANG); + Assertions.assertThat(leftMa.getPieceType()).isEqualTo(PieceType.MA); + Assertions.assertThat(rightMa.getPieceType()).isEqualTo(PieceType.MA); + Assertions.assertThat(rightSang.getPieceType()).isEqualTo(PieceType.SANG); + + Assertions.assertThat(leftSang.isSameSide(side)).isTrue(); + Assertions.assertThat(leftMa.isSameSide(side)).isTrue(); + Assertions.assertThat(rightMa.isSameSide(side)).isTrue(); + Assertions.assertThat(rightSang.isSameSide(side)).isTrue(); + } + } + + @Nested + class MaSangSangMaTest { + @Test + @DisplayName("Han 팀의 Sang 객체와 Ma 객체를 MaSangSangMa의 위치에 생성해 넣어준다.") + void shouldPlaceMaSangSangMaWhenSideHan() { + // given + Piece[][] grid = new Piece[10][9]; + Side side = Side.HAN; + MaSangArrangementStrategy strategy = MaSangArrangementStrategy.of(side, MA_SANG_SANG_MA); + + // when + strategy.place(grid); + Piece leftMa = grid[0][1]; + Piece leftSang = grid[0][2]; + Piece rightSang = grid[0][6]; + Piece rightMa = grid[0][7]; + + // then + Assertions.assertThat(leftMa.getPieceType()).isEqualTo(PieceType.MA); + Assertions.assertThat(leftSang.getPieceType()).isEqualTo(PieceType.SANG); + Assertions.assertThat(rightSang.getPieceType()).isEqualTo(PieceType.SANG); + Assertions.assertThat(rightMa.getPieceType()).isEqualTo(PieceType.MA); + + Assertions.assertThat(leftMa.isSameSide(side)).isTrue(); + Assertions.assertThat(leftSang.isSameSide(side)).isTrue(); + Assertions.assertThat(rightSang.isSameSide(side)).isTrue(); + Assertions.assertThat(rightMa.isSameSide(side)).isTrue(); + } + + @Test + @DisplayName("Cho 팀의 Sang 객체와 Ma 객체를 MaSangSangMa의 위치에 생성해 넣어준다.") + void shouldPlaceMaSangSangMaWhenSideCho() { + // given + Piece[][] grid = new Piece[10][9]; + Side side = Side.CHO; + MaSangArrangementStrategy strategy = MaSangArrangementStrategy.of(side, MA_SANG_SANG_MA); + + // when + strategy.place(grid); + Piece leftMa = grid[9][1]; + Piece leftSang = grid[9][2]; + Piece rightSang = grid[9][6]; + Piece rightMa = grid[9][7]; + + // then + Assertions.assertThat(leftMa.getPieceType()).isEqualTo(PieceType.MA); + Assertions.assertThat(leftSang.getPieceType()).isEqualTo(PieceType.SANG); + Assertions.assertThat(rightSang.getPieceType()).isEqualTo(PieceType.SANG); + Assertions.assertThat(rightMa.getPieceType()).isEqualTo(PieceType.MA); + + Assertions.assertThat(leftMa.isSameSide(side)).isTrue(); + Assertions.assertThat(leftSang.isSameSide(side)).isTrue(); + Assertions.assertThat(rightSang.isSameSide(side)).isTrue(); + Assertions.assertThat(rightMa.isSameSide(side)).isTrue(); + } + } +} diff --git a/src/test/java/janggi/support/TestArrangementStrategy.java b/src/test/java/janggi/support/TestArrangementStrategy.java new file mode 100644 index 0000000000..7ebd120bd3 --- /dev/null +++ b/src/test/java/janggi/support/TestArrangementStrategy.java @@ -0,0 +1,25 @@ +package janggi.support; + +import janggi.domain.Location; +import janggi.domain.piece.Piece; +import janggi.strategy.ArrangementStrategy; +import java.util.Map; + +public class TestArrangementStrategy implements ArrangementStrategy { + private final Map customPieces; + + public TestArrangementStrategy(Map customPieces) { + this.customPieces = customPieces; + } + + @Override + public void place(Piece[][] arrangement) { + placeVariablePieces(arrangement); + } + + protected void placeVariablePieces(Piece[][] arrangement) { + customPieces.forEach((loc, piece) -> { + arrangement[loc.x()][loc.y()] = piece; + }); + } +} diff --git a/src/test/java/janggi/support/TestPiece.java b/src/test/java/janggi/support/TestPiece.java new file mode 100644 index 0000000000..c63c076951 --- /dev/null +++ b/src/test/java/janggi/support/TestPiece.java @@ -0,0 +1,48 @@ +package janggi.support; + +import janggi.domain.Location; +import janggi.domain.Side; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceType; +import java.util.List; + +public class TestPiece implements Piece { + + private final PieceType pieceType; + private final Side side; + + public TestPiece(PieceType pieceType, Side side) { + this.pieceType = pieceType; + this.side = side; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean isPo() { + return this.pieceType.equals(PieceType.PO); + } + + @Override + public boolean isSameSide(Side side) { + return this.side.equals(side); + } + + @Override + public PieceType getPieceType() { + return pieceType; + } + + @Override + public List calculateRoute(Location from, Location to) { + return List.of(); + } + + @Override + public void detectCollision(List piecesOnPath) { + return; + } +}