diff --git a/README.md b/README.md index 4a9cf0f..e17dbb1 100644 --- a/README.md +++ b/README.md @@ -1 +1,127 @@ # java-chess 게임 + +## 구현해야할 목록 + +### 입력 + +- [x] 명령어 입력 + - [x] 예외: `null` 입력 + +### 출력 + +- [x] 시작 안내 문구 출력 +- [x] 체스판 스냅샷 출력 +- [x] 차례 출력 +- [x] 점수 출력 +- [x] 승자 출력 + +### 서비스 + +#### `ChessService` + +- [x] `Command`에 따라 기능 수행 +- [x] 게임 현재 상황 DTO + +### 도메인 + +#### `Operation` + +- [x] 명령어 검증 + - [x] start: 게임 실행 + - [x] status: 팀별 점수 + - [x] move: 기물 이동 + - [x] end: 강제 종료 + +#### `Board` + +- [x] 기물 이동 + - [x] 예외 + - [x] 움직일 기물이 자신 팀 소유가 아닌 경우 + - [x] `source`와 `target`이 같은 위치인 경우 + - [x] 본인의 기물을 공격하는 경우 + - [x] 이동 경로가 다른 기물에 가로막힌 경우 + - [x] `King`: `target`이 적이 공격 가능한 위치인 경우 + - [x] `Pawn`: `target`에 적이 없는데 대각선으로 이동하는 경우 + - [x] 자신의 기물 이동 + - [x] 공격인 경우 적 기물 제거 + - [x] 차례 변경 +- [x] 현재 차례 관리 +- [x] 위치 정보 + - [x] 특정 위치가 비었는지 확인 + - [x] 특정 위치에 있는 기물 확인 +- [x] 각 팀 점수 계산 +- [x] 승자 확인 + - [x] 예외: 두 `King`이 모두 살아 있는 경우 + +#### `Team` + +- [x] 기물 이동 +- [x] 기물 이동 경로 검색 +- [x] 현위치에서 공격 가능한 모든 경로 검색 +- [x] 적에게 공격받은 경우 기물 제거 +- [x] 기물 검색 +- [x] 점수 계산 + +#### `Piece` + +- [x] 이동 경로 검색 +- [x] 현위치에서 공격 가능한 모든 경로 검색 +- [x] 색상 확인 +- [x] 타입 확인 + - [x] `Pawn` + - [x] `King` + +#### `MovePattern` + +- [x] 각 기물 타입에 맞는 패턴 검색 + - [x] `Pawn`은 색상에 맞는 패턴 검색 +- [x] 본인 패턴 중 `gap`에 맞는 `MoveUnit` 검색 +- [x] 본인 패턴으로 공격 가능한 모든 경로 검색 + +#### `MoveLimit` + +- [x] 한 칸만 이동 가능한지 확인 + +#### `MoveUnits` + +- [x] 본인 리스트에서 매칭되는 `MoveUnit` 검색 + - [x] 예외: 매칭되는 `MoveUnit`이 없는 경우 +- [x] 이동 단위 별로 이동 가능한 모든 경로 검색 + +#### `MoveUnit` + +- [x] `MoveUnit`의 좌표가 `gap`에 매칭되는지 확인 + +#### `Position` + +- [x] 64개 위치 캐싱 및 검색 +- [x] `Gap` 계산 +- [x] 이동 단위로 타겟까지 이동할 때 지나가는 모든 위치 검색 +- [x] 이동 단위로 이동 가능한 체스판의 모든 위치 검색 +- [x] 이동 단위로 한 번 더 이동 가능한지 확인 +- [x] 이동 단위로 이동 + +## 이동 규칙 + +- [x] 폰 (1 or 0.5) + - [x] 적 방향 직선 1칸 이동 + - [x] 처음 이동 시에는 2칸 이동 가능 + - [x] 공격 시에는 적 방향 좌, 우 대각선 1칸 이동 + +- [x] 룩 (5) + - [x] 모든 직선 방향으로 원하는 만큼 이동 + +- [x] 나이트 (2.5) + - [x] 모든 직선 방향 1칸 + 이동한 직선 방향의 좌, 우 대각선 1칸으로 이동 + - [x] 진행 방향이 가로막혀도 적, 아군 상관없이 뛰어넘을 수 있음 + +- [x] 비숍 (3) + - [x] 모든 대각선 방향으로 원하는 만큼 이동 + +- [x] 퀸 (9) + - [x] 모든 방향으로 원하는 만큼 이동 + +- [x] 킹 + - [x] 모든 방향 1칸 이동 + - [x] 상대의 공격 범위로는 이동 불가능 + diff --git a/build.gradle b/build.gradle index d41dd5b..50af46f 100644 --- a/build.gradle +++ b/build.gradle @@ -18,4 +18,5 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' testCompile "org.assertj:assertj-core:3.14.0" + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.4.2' } diff --git a/src/main/java/chess/ConsoleChessApplication.java b/src/main/java/chess/ConsoleChessApplication.java new file mode 100644 index 0000000..73a0402 --- /dev/null +++ b/src/main/java/chess/ConsoleChessApplication.java @@ -0,0 +1,17 @@ +package chess; + +import chess.controller.ConsoleChessController; +import chess.view.ConsoleInputView; +import chess.view.ConsoleOutputView; +import chess.view.InputView; +import chess.view.OutputView; + +public class ConsoleChessApplication { + + public static void main(String[] args) { + OutputView outputView = new ConsoleOutputView(); + InputView inputView = new ConsoleInputView(); + ConsoleChessController consoleChessController = new ConsoleChessController(inputView, outputView); + consoleChessController.run(); + } +} diff --git a/src/main/java/chess/WebUIChessApplication.java b/src/main/java/chess/WebUIChessApplication.java deleted file mode 100644 index dacaab9..0000000 --- a/src/main/java/chess/WebUIChessApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -package chess; - -import spark.ModelAndView; -import spark.template.handlebars.HandlebarsTemplateEngine; - -import java.util.HashMap; -import java.util.Map; - -import static spark.Spark.get; - -public class WebUIChessApplication { - - public static void main(String[] args) { - get("/", (req, res) -> { - Map model = new HashMap<>(); - return render(model, "index.html"); - }); - } - - private static String render(Map model, String templatePath) { - return new HandlebarsTemplateEngine().render(new ModelAndView(model, templatePath)); - } - -} diff --git a/src/main/java/chess/controller/ConsoleChessController.java b/src/main/java/chess/controller/ConsoleChessController.java new file mode 100644 index 0000000..6f9eb4d --- /dev/null +++ b/src/main/java/chess/controller/ConsoleChessController.java @@ -0,0 +1,75 @@ +package chess.controller; + +import chess.domain.command.Command; +import chess.dto.Scores; +import chess.dto.TurnDto; +import chess.exception.ForcedTerminationException; +import chess.exception.ScoresRequestedException; +import chess.service.ChessService; +import chess.view.InputView; +import chess.view.OutputView; + +import java.util.Map; + +public class ConsoleChessController { + + private final InputView inputView; + private final OutputView outputView; + private final ChessService chessService; + + public ConsoleChessController(final InputView inputView, final OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + this.chessService = new ChessService(); + } + + public void run() { + outputView.printGuide(); + + while (chessService.isGameRunning()) { + try { + runOneTurn(); + printBoard(); + } catch (ForcedTerminationException e) { + outputView.printMessage(e.getMessage()); + break; + } + } + + printFinalResult(); + } + + private void runOneTurn() { + try { + TurnDto currentTurn = chessService.getCurrentTurnDto(); + outputView.printTurn(currentTurn); + + Command command = new Command(inputView.getCommand()); + chessService.run(command); + } catch (UnsupportedOperationException | IllegalArgumentException e) { + outputView.printMessage(e.getMessage()); + } catch (ScoresRequestedException e) { + Scores scores = chessService.getScores(); + outputView.printScores(scores); + } + } + + private void printFinalResult() { + printBoard(); + printWinner(); + } + + private void printBoard() { + Map boardDto = chessService.getBoardDto(); + outputView.printBoard(boardDto); + } + + private void printWinner() { + try { + String winner = chessService.getWinnerDto(); + outputView.printWinner(winner); + } catch (IllegalStateException e) { + outputView.printMessage(e.getMessage()); + } + } +} diff --git a/src/main/java/chess/domain/board/File.java b/src/main/java/chess/domain/board/File.java new file mode 100644 index 0000000..758f17f --- /dev/null +++ b/src/main/java/chess/domain/board/File.java @@ -0,0 +1,49 @@ +package chess.domain.board; + +import java.util.Arrays; + +public enum File { + + a(1), + b(2), + c(3), + d(4), + e(5), + f(6), + g(7), + h(8); + + private final int index; + + File(final int index) { + this.index = index; + } + + public static File of(final int fileIndex) { + return Arrays.stream(File.values()) + .filter(file -> hasSameIndex(fileIndex, file)) + .findAny() + .orElseThrow(IllegalArgumentException::new); + } + + private static boolean hasSameIndex(final int fileIndex, final File file) { + return file.getIndex() == fileIndex; + } + + public int getIndex() { + return index; + } + + public int calculateGap(final File file) { + return this.index - file.index; + } + + public File add(final int amount) { + return File.of(this.index + amount); + } + + public boolean canMove(final int amount) { + int fileIndex = index + amount; + return fileIndex >= a.index && fileIndex <= h.index; + } +} diff --git a/src/main/java/chess/domain/board/Position.java b/src/main/java/chess/domain/board/Position.java new file mode 100644 index 0000000..fa6e4f0 --- /dev/null +++ b/src/main/java/chess/domain/board/Position.java @@ -0,0 +1,122 @@ +package chess.domain.board; + +import chess.domain.piece.move.Gap; +import chess.domain.piece.move.MoveUnit; + +import java.util.*; + +public class Position { + + private static final Map POSITIONS = Collections.unmodifiableMap(createPositions()); + + private static Map createPositions() { + Map positions = new HashMap<>(); + + Arrays.stream(File.values()) + .forEach(file -> Arrays.stream(Rank.values()) + .forEach(rank -> positions.put(createKey(file, rank), new Position(file, rank)))); + + return positions; + } + + private static String createKey(final File file, final Rank rank) { + return file.name() + rank.getIndex(); + } + + public static Collection names() { + return POSITIONS.keySet(); + } + + public static Position from(final File file, final Rank rank) { + return of(createKey(file, rank)); + } + + public static Position of(String key) { + key = key.toLowerCase(); + + if (!POSITIONS.containsKey(key)) { + throw new IllegalArgumentException("위치 입력값이 잘못되었습니다."); + } + + return POSITIONS.get(key); + } + + private final File file; + private final Rank rank; + + private Position(final File file, final Rank rank) { + this.file = file; + this.rank = rank; + } + + public Gap calculateGap(final Position position) { + int fileGap = calculateFileGap(position); + int rankGap = calculateRankGap(position); + + return new Gap(fileGap, rankGap); + } + + private int calculateFileGap(final Position position) { + return file.calculateGap(position.getFile()); + } + + private int calculateRankGap(final Position position) { + return rank.calculateGap(position.getRank()); + } + + public List findPassingPositions(final Position target, final MoveUnit moveUnit) { + List positions = new ArrayList<>(); + + Position current = this; + while (!target.equals(current)) { + current = current.move(moveUnit); + positions.add(current); + } + + positions.remove(target); + return positions; + } + + public List findReachablePositions(final MoveUnit moveUnit) { + List positions = new ArrayList<>(); + + Position current = this; + while (current.isMovable(moveUnit)) { + current = current.move(moveUnit); + positions.add(current); + } + + return positions; + } + + public boolean isMovable(final MoveUnit moveUnit) { + return rank.canMove(moveUnit.getRank()) && file.canMove(moveUnit.getFile()); + } + + public Position move(final MoveUnit moveUnit) { + File nextFile = this.file.add(moveUnit.getFile()); + Rank nextRank = this.rank.add(moveUnit.getRank()); + + return Position.from(nextFile, nextRank); + } + + public boolean isSame(Position position) { + return this == position; + } + + public File getFile() { + return file; + } + + public Rank getRank() { + return rank; + } + + public boolean isWhitePawnInitialRank() { + return this.rank.isWhiteInitialRank(); + } + + public boolean isBlackPawnInitialRank() { + return this.rank.isBlackInitialRank(); + } +} diff --git a/src/main/java/chess/domain/board/Rank.java b/src/main/java/chess/domain/board/Rank.java new file mode 100644 index 0000000..3b58100 --- /dev/null +++ b/src/main/java/chess/domain/board/Rank.java @@ -0,0 +1,61 @@ +package chess.domain.board; + +import java.util.Arrays; + +public enum Rank { + + R8(8), + R7(7), + R6(6), + R5(5), + R4(4), + R3(3), + R2(2), + R1(1); + + private static final Rank WHITE_INITIAL_RANK = R2; + private static final Rank BLACK_INITIAL_RANK = R7; + + private final int index; + + Rank(final int index) { + this.index = index; + } + + public static Rank of(final int rankIndex) { + return Arrays.stream(Rank.values()) + .filter(rank -> hasSameIndex(rankIndex, rank)) + .findAny() + .orElseThrow(IllegalArgumentException::new); + } + + private static boolean hasSameIndex(final int rankIndex, final Rank rank) { + return rank.index == rankIndex; + } + + public int getIndex() { + return index; + } + + public int calculateGap(final Rank rank) { + return this.index - rank.index; + } + + public Rank add(final int amount) { + return Rank.of(this.index + amount); + } + + public boolean canMove(final int amount) { + int rankIndex = index + amount; + return rankIndex >= R1.index && rankIndex <= R8.index; + } + + public boolean isWhiteInitialRank() { + return this == WHITE_INITIAL_RANK; + } + + public boolean isBlackInitialRank() { + return this == BLACK_INITIAL_RANK; + } +} + diff --git a/src/main/java/chess/domain/command/Command.java b/src/main/java/chess/domain/command/Command.java new file mode 100644 index 0000000..53c19a6 --- /dev/null +++ b/src/main/java/chess/domain/command/Command.java @@ -0,0 +1,52 @@ +package chess.domain.command; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Command { + + private static final String DELIMITER = " "; + private static final int OPERATION_INDEX = 0; + + private final Operation operation; + private final List parameters; + + public Command(final String commandLine) { + List chunks = Arrays.stream(commandLine.split(DELIMITER)) + .map(String::trim) + .map(String::toLowerCase) + .collect(Collectors.toList()); + + if (chunks.isEmpty()) { + throw new IllegalArgumentException("명령어를 입력해주세요."); + } + + this.operation = Operation.of(chunks.get(OPERATION_INDEX)); + this.parameters = chunks.subList(OPERATION_INDEX + 1, chunks.size()); + } + + public MoveParameters getMoveParameters() { + if (parameters.size() != 2) { + throw new IllegalArgumentException("기물 이동 위치를 정확하게 입력해주세요."); + } + + return new MoveParameters(parameters); + } + + public boolean isStart() { + return operation.isStart(); + } + + public boolean isEnd() { + return operation.isEnd(); + } + + public boolean isMove() { + return operation.isMove(); + } + + public boolean isStatus() { + return operation.isStatus(); + } +} diff --git a/src/main/java/chess/domain/command/MoveParameters.java b/src/main/java/chess/domain/command/MoveParameters.java new file mode 100644 index 0000000..d9e3617 --- /dev/null +++ b/src/main/java/chess/domain/command/MoveParameters.java @@ -0,0 +1,35 @@ +package chess.domain.command; + +import chess.domain.board.Position; + +import java.util.List; + +public class MoveParameters { + + private static final int SOURCE_INDEX = 0; + private static final int TARGET_INDEX = 1; + + private final Position source; + private final Position target; + + public MoveParameters(final List parameters) { + this(parameters.get(SOURCE_INDEX), parameters.get(TARGET_INDEX)); + } + + public MoveParameters(final String source, final String target) { + this(Position.of(source), Position.of(target)); + } + + private MoveParameters(final Position source, final Position target) { + this.source = source; + this.target = target; + } + + public Position getSource() { + return source; + } + + public Position getTarget() { + return target; + } +} diff --git a/src/main/java/chess/domain/command/Operation.java b/src/main/java/chess/domain/command/Operation.java new file mode 100644 index 0000000..3ba2729 --- /dev/null +++ b/src/main/java/chess/domain/command/Operation.java @@ -0,0 +1,57 @@ +package chess.domain.command; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public enum Operation { + + START("start"), + END("end"), + MOVE("move"), + STATUS("status"); + + private static final Map operationNames = createMap(); + + private static Map createMap() { + Map map = new HashMap<>(); + + Arrays.stream(Operation.values()) + .forEach(operation -> map.put(operation.keyword, operation)); + + return Collections.unmodifiableMap(map); + } + + public static Operation of(String input) { + Operation operation = operationNames.get(input); + + if (operation == null) { + throw new UnsupportedOperationException("유효하지 않은 명령어입니다."); + } + + return operation; + } + + private final String keyword; + + Operation(String keyword) { + this.keyword = keyword; + } + + public boolean isStart() { + return this == START; + } + + public boolean isEnd() { + return this == END; + } + + public boolean isMove() { + return this == MOVE; + } + + public boolean isStatus() { + return this == STATUS; + } +} diff --git a/src/main/java/chess/domain/game/ChessGame.java b/src/main/java/chess/domain/game/ChessGame.java new file mode 100644 index 0000000..3550817 --- /dev/null +++ b/src/main/java/chess/domain/game/ChessGame.java @@ -0,0 +1,144 @@ +package chess.domain.game; + +import chess.domain.board.Position; +import chess.domain.command.MoveParameters; +import chess.domain.piece.move.Path; +import chess.domain.piece.type.Piece; +import chess.domain.team.Color; +import chess.domain.team.Team; +import chess.dto.Scores; +import chess.exception.EmptyPositionException; + +import static chess.domain.team.Color.BLACK; +import static chess.domain.team.Color.WHITE; + +public class ChessGame { + + private final Team whiteTeam; + private final Team blackTeam; + private final Turn currentTurn; + + public ChessGame() { + this.whiteTeam = Team.white(); + this.blackTeam = Team.black(); + this.currentTurn = Turn.initialTurn(); + } + + public void move(final MoveParameters moveParameters) { + Position source = moveParameters.getSource(); + Position target = moveParameters.getTarget(); + validateParameters(source, target); + + movePiece(source, target); + currentTurn.next(); + } + + private void validateParameters(final Position source, final Position target) { + validateSourceOwner(source); + validateSamePosition(source, target); + validateTargetOwner(target); + validateKingMovable(source, target); + } + + private void validateSourceOwner(final Position source) { + if (currentTeam().hasNoPieceOn(source)) { + throw new IllegalArgumentException("자신의 기물만 움직일 수 있습니다."); + } + } + + private void validateSamePosition(final Position source, final Position target) { + if (source.isSame(target)) { + throw new IllegalArgumentException("출발 위치와 도착 위치가 같을 수 없습니다."); + } + } + + private void validateTargetOwner(final Position target) { + if (currentTeam().hasPieceOn(target)) { + throw new IllegalArgumentException("자신의 기물이 있는 곳으로 이동할 수 없습니다."); + } + } + + private void validateKingMovable(final Position source, final Position target) { + if (currentTeam().hasKingOn(source) && canEnemyAttack(target)) { + throw new IllegalArgumentException("킹은 상대방이 공격 가능한 위치로 이동할 수 없습니다."); + } + } + + private boolean canEnemyAttack(final Position target) { + return enemyTeam().findAttackPaths(target).stream() + .anyMatch(path -> path.isNotBlockedBy(whiteTeam) && path.isNotBlockedBy(blackTeam)); + } + + private void movePiece(final Position source, final Position target) { + if (currentTeam().isPawnAttacking(source, target) && enemyTeam().hasNoPieceOn(target)) { + throw new IllegalArgumentException("폰은 공격 대상이 있는 경우에만 대각선으로 이동할 수 있습니다."); + } + + Path path = currentTeam().findMovePath(source, target); + validatePathNotBlocked(path); + + enemyTeam().removePiece(target); + currentTeam().movePiece(source, target); + } + + private void validatePathNotBlocked(final Path path) { + if (path.isBlockedBy(whiteTeam) || path.isBlockedBy(blackTeam)) { + throw new IllegalArgumentException("다른 기물을 통과하여 이동할 수 없습니다."); + } + } + + public Piece findPieceBy(final Position position) { + if (isEmpty(position)) { + throw new EmptyPositionException(); + } + + if (whiteTeam.hasPieceOn(position)) { + return whiteTeam.findPieceBy(position); + } + return blackTeam.findPieceBy(position); + } + + private boolean isEmpty(Position position) { + return whiteTeam.hasNoPieceOn(position) && blackTeam.hasNoPieceOn(position); + } + + public Scores getScores() { + double whiteScore = whiteTeam.calculateScores(); + double blackScore = blackTeam.calculateScores(); + + return new Scores(whiteScore, blackScore); + } + + public Color getWinner() { + if (isBothKingAlive()) { + throw new IllegalStateException("King이 잡히지 않아 승자가 없습니다."); + } + + if (whiteTeam.isKingDead()) { + return BLACK; + } + return WHITE; + } + + public boolean isBothKingAlive() { + return whiteTeam.isKingAlive() && blackTeam.isKingAlive(); + } + + public Turn getCurrentTurn() { + return currentTurn; + } + + private Team currentTeam() { + if (currentTurn.isWhite()) { + return whiteTeam; + } + return blackTeam; + } + + private Team enemyTeam() { + if (currentTurn.isWhite()) { + return blackTeam; + } + return whiteTeam; + } +} diff --git a/src/main/java/chess/domain/game/Player.java b/src/main/java/chess/domain/game/Player.java new file mode 100644 index 0000000..b0d2feb --- /dev/null +++ b/src/main/java/chess/domain/game/Player.java @@ -0,0 +1,38 @@ +package chess.domain.game; + +import chess.domain.team.Color; + +public class Player { + + private static final String DEFAULT_WHITE_NAME = "WHITE"; + private static final String DEFAULT_BLACK_NAME = "BLACK"; + + private final String name; + + public static Player of(final String name) { + return new Player(name); + } + + public static Player defaultPlayer(final Color color) { + if (color.isWhite()) { + return defaultWhite(); + } + return defaultBlack(); + } + + private static Player defaultWhite() { + return Player.of(DEFAULT_WHITE_NAME); + } + + private static Player defaultBlack() { + return Player.of(DEFAULT_BLACK_NAME); + } + + private Player(final String name) { + this.name = name; + } + + public String name() { + return name; + } +} diff --git a/src/main/java/chess/domain/game/Turn.java b/src/main/java/chess/domain/game/Turn.java new file mode 100644 index 0000000..e47358f --- /dev/null +++ b/src/main/java/chess/domain/game/Turn.java @@ -0,0 +1,52 @@ +package chess.domain.game; + +import chess.domain.team.Color; + +import java.util.*; + +public class Turn { + + private final Map> players; + private Color color; + private int count; + + public static Turn initialTurn(final Map> players) { + return new Turn(players); + } + + public static Turn initialTurn() { + return initialTurn(defaultPlayers()); + } + + private Turn(final Map> players) { + this.players = Collections.unmodifiableMap(players); + this.color = Color.WHITE; + this.count = 0; + } + + private static Map> defaultPlayers() { + Map> map = new EnumMap<>(Color.class); + Arrays.stream(Color.values()) + .forEach(color -> map.put(color, Collections.singletonList(Player.defaultPlayer(color)))); + return map; + } + + public void next() { + color = color.flip(); + count++; + } + + public boolean isWhite() { + return color.isWhite(); + } + + public Color team() { + return color; + } + + public Player player() { + List candidates = this.players.get(color); + int index = count / 2 % candidates.size(); + return candidates.get(index); + } +} diff --git a/src/main/java/chess/domain/piece/move/Gap.java b/src/main/java/chess/domain/piece/move/Gap.java new file mode 100644 index 0000000..b05e586 --- /dev/null +++ b/src/main/java/chess/domain/piece/move/Gap.java @@ -0,0 +1,20 @@ +package chess.domain.piece.move; + +public class Gap { + + private final int fileGap; + private final int rankGap; + + public Gap(int fileGap, int rankGap) { + this.fileGap = fileGap; + this.rankGap = rankGap; + } + + public int file() { + return fileGap; + } + + public int rank() { + return rankGap; + } +} diff --git a/src/main/java/chess/domain/piece/move/MoveLimit.java b/src/main/java/chess/domain/piece/move/MoveLimit.java new file mode 100644 index 0000000..225a4a8 --- /dev/null +++ b/src/main/java/chess/domain/piece/move/MoveLimit.java @@ -0,0 +1,17 @@ +package chess.domain.piece.move; + +public enum MoveLimit { + + FINITE(true), + INFINITE(false); + + private final boolean isFinite; + + MoveLimit(boolean isFinite) { + this.isFinite = isFinite; + } + + public boolean isFinite() { + return isFinite; + } +} diff --git a/src/main/java/chess/domain/piece/move/MovePattern.java b/src/main/java/chess/domain/piece/move/MovePattern.java new file mode 100644 index 0000000..5d089a1 --- /dev/null +++ b/src/main/java/chess/domain/piece/move/MovePattern.java @@ -0,0 +1,56 @@ +package chess.domain.piece.move; + +import chess.domain.board.Position; +import chess.domain.team.Color; + +import java.util.Collection; + +import static chess.domain.piece.move.MoveLimit.FINITE; +import static chess.domain.piece.move.MoveLimit.INFINITE; +import static chess.domain.piece.move.MoveUnits.*; + +public enum MovePattern { + + KING(CARDINAL_AND_DIAGONAL, FINITE), + QUEEN(CARDINAL_AND_DIAGONAL, INFINITE), + BISHOP(DIAGONAL, INFINITE), + ROOK(CARDINAL, INFINITE), + KNIGHT(MoveUnits.KNIGHT, FINITE), + WHITE_PAWN(MoveUnits.WHITE_PAWN, FINITE), + BLACK_PAWN(MoveUnits.BLACK_PAWN, FINITE), + WHITE_PAWN_ATTACK(MoveUnits.WHITE_PAWN_ATTACK, FINITE), + BLACK_PAWN_ATTACK(MoveUnits.BLACK_PAWN_ATTACK, FINITE); + + public static MovePattern pawnPattern(final Color color) { + if (color.isWhite()) { + return WHITE_PAWN; + } + return BLACK_PAWN; + } + + public static MovePattern pawnAttackPattern(final Color color) { + if (color.isWhite()) { + return WHITE_PAWN_ATTACK; + } + return BLACK_PAWN_ATTACK; + } + + private final MoveUnits moveUnits; + private final MoveLimit moveLimit; + + MovePattern(final MoveUnits moveUnits, final MoveLimit moveLimit) { + this.moveUnits = moveUnits; + this.moveLimit = moveLimit; + } + + public MoveUnit findMatchMoveUnit(final Gap gap) { + return moveUnits.findMatchMoveUnit(gap, moveLimit); + } + + public Collection findAttackPaths(final Position source) { + if (moveLimit.isFinite()) { + return moveUnits.findReachableFinitePaths(source); + } + return moveUnits.findReachableInfinitePaths(source); + } +} diff --git a/src/main/java/chess/domain/piece/move/MoveUnit.java b/src/main/java/chess/domain/piece/move/MoveUnit.java new file mode 100644 index 0000000..ac1b4f3 --- /dev/null +++ b/src/main/java/chess/domain/piece/move/MoveUnit.java @@ -0,0 +1,85 @@ +package chess.domain.piece.move; + +public enum MoveUnit { + + NORTH_EAST(1, 1), + SOUTH_EAST(1, -1), + NORTH_WEST(-1, 1), + SOUTH_WEST(-1, -1), + NORTH(0, 1), + SOUTH(0, -1), + EAST(1, 0), + WEST(-1, 0), + + WHITE_PAWN_INITIAL(0, 2), + BLACK_PAWN_INITIAL(0, -2), + + NORTH_EAST_LEFT(1, 2), + NORTH_EAST_RIGHT(2, 1), + NORTH_WEST_LEFT(-2, 1), + NORTH_WEST_RIGHT(-1, 2), + SOUTH_EAST_LEFT(2, -1), + SOUTH_EAST_RIGHT(1, -2), + SOUTH_WEST_LEFT(-1, -2), + SOUTH_WEST_RIGHT(-2, -1); + + public static MoveUnit whitePawnMove() { + return NORTH; + } + + public static MoveUnit blackPawnMove() { + return SOUTH; + } + + private final int file; + private final int rank; + + MoveUnit(final int file, final int rank) { + this.file = file; + this.rank = rank; + } + + public boolean matches(final Gap gap, final MoveLimit moveLimit) { + if (moveLimit.isFinite()) { + return this.file == gap.file() && this.rank == gap.rank(); + } + + if (this.file == 0) { + return this.file == gap.file() && (gap.rank() % this.rank == 0) && hasSameSign(gap); + } + + if (this.rank == 0) { + return this.rank == gap.rank() && (gap.file() % this.file == 0) && hasSameSign(gap); + } + + return isMultiple(gap) && hasSameSign(gap); + } + + private boolean isMultiple(final Gap gap) { + return hasSameRate(gap) && isDivisible(gap); + } + + private boolean isDivisible(final Gap gap) { + return (gap.file() % this.file == 0) && (gap.rank() % this.rank == 0); + } + + private boolean hasSameRate(final Gap gap) { + return (gap.file() / this.file) == (gap.rank() / this.rank); + } + + private boolean hasSameSign(final Gap gap) { + return (file ^ gap.file()) >= 0 && (rank ^ gap.rank()) >= 0; + } + + public boolean isInitialPawn() { + return this == WHITE_PAWN_INITIAL || this == BLACK_PAWN_INITIAL; + } + + public int getFile() { + return file; + } + + public int getRank() { + return rank; + } +} diff --git a/src/main/java/chess/domain/piece/move/MoveUnits.java b/src/main/java/chess/domain/piece/move/MoveUnits.java new file mode 100644 index 0000000..fc29d9f --- /dev/null +++ b/src/main/java/chess/domain/piece/move/MoveUnits.java @@ -0,0 +1,65 @@ +package chess.domain.piece.move; + +import chess.domain.board.Position; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.stream.Collectors; + +import static chess.domain.piece.move.MoveUnit.*; + +public enum MoveUnits { + + CARDINAL(NORTH, SOUTH, WEST, EAST), + DIAGONAL(NORTH_EAST, NORTH_WEST, SOUTH_EAST, SOUTH_WEST), + CARDINAL_AND_DIAGONAL(NORTH, SOUTH, WEST, EAST, NORTH_EAST, NORTH_WEST, SOUTH_EAST, SOUTH_WEST), + KNIGHT(NORTH_EAST_LEFT, NORTH_EAST_RIGHT, NORTH_WEST_LEFT, NORTH_WEST_RIGHT, SOUTH_EAST_LEFT, SOUTH_EAST_RIGHT, SOUTH_WEST_LEFT, SOUTH_WEST_RIGHT), + WHITE_PAWN(WHITE_PAWN_INITIAL, NORTH_EAST, NORTH_WEST, NORTH), + WHITE_PAWN_ATTACK(NORTH_EAST, NORTH_WEST), + BLACK_PAWN(BLACK_PAWN_INITIAL, SOUTH_EAST, SOUTH_WEST, SOUTH), + BLACK_PAWN_ATTACK(SOUTH_EAST, SOUTH_WEST); + + private final Collection moveUnits; + + MoveUnits(MoveUnit... moveUnits) { + this.moveUnits = Collections.unmodifiableCollection( + Arrays.stream(moveUnits) + .collect(Collectors.toSet()) + ); + } + + public MoveUnit findMatchMoveUnit(final Gap gap, final MoveLimit moveLimit) { + return moveUnits.stream() + .filter(moveUnit -> moveUnit.matches(gap, moveLimit)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("해당 방향으로 이동할 수 없습니다.")); + } + + public Collection findReachableFinitePaths(final Position source) { + Collection paths = new HashSet<>(); + + moveUnits.stream() + .filter(source::isMovable) + .forEach(moveUnit -> { + Path path = buildPath(source, moveUnit); + paths.add(path); + }); + + return paths; + } + + private Path buildPath(Position source, MoveUnit moveUnit) { + Position target = source.move(moveUnit); + return new Path(source, target); + } + + public Collection findReachableInfinitePaths(final Position source) { + return moveUnits.stream() + .filter(source::isMovable) + .map(source::findReachablePositions) + .map(Path::new) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/chess/domain/piece/move/Path.java b/src/main/java/chess/domain/piece/move/Path.java new file mode 100644 index 0000000..81f571b --- /dev/null +++ b/src/main/java/chess/domain/piece/move/Path.java @@ -0,0 +1,67 @@ +package chess.domain.piece.move; + +import chess.domain.board.Position; +import chess.domain.team.Team; + +import java.util.*; + +public class Path { + + private final List positions; + + public Path(final Position position) { + this(Collections.singletonList(position)); + } + + public Path(final Position... positions) { + this(Arrays.asList(positions)); + } + + public Path(final List positions) { + this.positions = Collections.unmodifiableList(new ArrayList<>(positions)); + } + + public boolean contains(final Position position) { + return positions.contains(position); + } + + public Path getPositionsUntilTarget(final Position target) { + List positionsBeforeTarget = new ArrayList<>(); + + for (Position position : positions) { + if (position.isSame(target)) { + break; + } + positionsBeforeTarget.add(position); + } + + return new Path(positionsBeforeTarget); + } + + public boolean isNotBlockedBy(final Team team) { + return positions.stream() + .noneMatch(team::hasPieceOn); + } + + public boolean isBlockedBy(final Team team) { + return positions.stream() + .anyMatch(team::hasPieceOn); + } + + public boolean isNotEmpty() { + return !positions.isEmpty(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Path path = (Path) o; + return positions.equals(path.positions); + } + + @Override + public int hashCode() { + return Objects.hash(positions); + } +} diff --git a/src/main/java/chess/domain/piece/type/Bishop.java b/src/main/java/chess/domain/piece/type/Bishop.java new file mode 100644 index 0000000..68cce3d --- /dev/null +++ b/src/main/java/chess/domain/piece/type/Bishop.java @@ -0,0 +1,11 @@ +package chess.domain.piece.type; + +import chess.domain.piece.move.MovePattern; +import chess.domain.team.Color; + +public class Bishop extends Piece { + + public Bishop(final Color color) { + super(MovePattern.BISHOP, color); + } +} diff --git a/src/main/java/chess/domain/piece/type/King.java b/src/main/java/chess/domain/piece/type/King.java new file mode 100644 index 0000000..05cddc0 --- /dev/null +++ b/src/main/java/chess/domain/piece/type/King.java @@ -0,0 +1,11 @@ +package chess.domain.piece.type; + +import chess.domain.piece.move.MovePattern; +import chess.domain.team.Color; + +public class King extends Piece { + + public King(final Color color) { + super(MovePattern.KING, color); + } +} diff --git a/src/main/java/chess/domain/piece/type/Knight.java b/src/main/java/chess/domain/piece/type/Knight.java new file mode 100644 index 0000000..108eae7 --- /dev/null +++ b/src/main/java/chess/domain/piece/type/Knight.java @@ -0,0 +1,11 @@ +package chess.domain.piece.type; + +import chess.domain.piece.move.MovePattern; +import chess.domain.team.Color; + +public class Knight extends Piece { + + public Knight(final Color color) { + super(MovePattern.KNIGHT, color); + } +} diff --git a/src/main/java/chess/domain/piece/type/Pawn.java b/src/main/java/chess/domain/piece/type/Pawn.java new file mode 100644 index 0000000..ca75f34 --- /dev/null +++ b/src/main/java/chess/domain/piece/type/Pawn.java @@ -0,0 +1,75 @@ +package chess.domain.piece.type; + +import chess.domain.board.Position; +import chess.domain.piece.move.Gap; +import chess.domain.piece.move.MovePattern; +import chess.domain.piece.move.MoveUnit; +import chess.domain.piece.move.Path; +import chess.domain.team.Color; + +import java.util.Collection; +import java.util.List; + +public class Pawn extends Piece { + + private final MovePattern attackPattern; + + public Pawn(final Color color) { + super(MovePattern.pawnPattern(color), color); + this.attackPattern = MovePattern.pawnAttackPattern(color); + } + + @Override + public Path findMovePath(final Position source, final Position target) { + Gap gap = target.calculateGap(source); + MoveUnit moveUnit = movePattern.findMatchMoveUnit(gap); + + if (moveUnit.isInitialPawn()) { + validateInitialMoveAvailable(source); + moveUnit = resetMoveUnit(); + } + + List passingPositions = source.findPassingPositions(target, moveUnit); + return new Path(passingPositions); + } + + private void validateInitialMoveAvailable(Position source) { + if (isInitialMoveUnavailable(source)) { + throw new IllegalArgumentException("최초 이동 시에만 2칸 이동할 수 있습니다."); + } + } + + private MoveUnit resetMoveUnit() { + if (color.isWhite()) { + return MoveUnit.whitePawnMove(); + } + return MoveUnit.blackPawnMove(); + } + + private boolean isInitialMoveUnavailable(final Position source) { + return !isWhiteInitialMoveAvailable(source) && !isBlackInitialMoveAvailable(source); + } + + private boolean isWhiteInitialMoveAvailable(final Position source) { + return color.isWhite() && source.isWhitePawnInitialRank(); + } + + private boolean isBlackInitialMoveAvailable(final Position source) { + return color.isBlack() && source.isBlackPawnInitialRank(); + } + + @Override + public Collection findAttackPaths(final Position source) { + return attackPattern.findAttackPaths(source); + } + + public boolean isAttacking(final Position source, final Position target) { + try { + Gap gap = target.calculateGap(source); + attackPattern.findMatchMoveUnit(gap); + } catch (IllegalArgumentException e) { + return false; + } + return true; + } +} diff --git a/src/main/java/chess/domain/piece/type/Piece.java b/src/main/java/chess/domain/piece/type/Piece.java new file mode 100644 index 0000000..3d7dab5 --- /dev/null +++ b/src/main/java/chess/domain/piece/type/Piece.java @@ -0,0 +1,51 @@ +package chess.domain.piece.type; + +import chess.domain.board.Position; +import chess.domain.piece.move.Gap; +import chess.domain.piece.move.MovePattern; +import chess.domain.piece.move.MoveUnit; +import chess.domain.piece.move.Path; +import chess.domain.team.Color; + +import java.util.Collection; +import java.util.List; + +public abstract class Piece { + + protected final MovePattern movePattern; + protected final Color color; + + Piece(final MovePattern movePattern, final Color color) { + this.movePattern = movePattern; + this.color = color; + } + + public Path findMovePath(final Position source, final Position target) { + Gap gap = target.calculateGap(source); + + MoveUnit moveUnit = movePattern.findMatchMoveUnit(gap); + List passingPositions = source.findPassingPositions(target, moveUnit); + + return new Path(passingPositions); + } + + public Collection findAttackPaths(final Position source) { + return movePattern.findAttackPaths(source); + } + + public boolean isWhite() { + return color.isWhite(); + } + + public boolean isPawn() { + return PieceType.isPawn(this); + } + + public boolean isNotPawn() { + return !isPawn(); + } + + public boolean isKing() { + return PieceType.isKing(this); + } +} diff --git a/src/main/java/chess/domain/piece/type/PieceFactory.java b/src/main/java/chess/domain/piece/type/PieceFactory.java new file mode 100644 index 0000000..81d29cc --- /dev/null +++ b/src/main/java/chess/domain/piece/type/PieceFactory.java @@ -0,0 +1,52 @@ +package chess.domain.piece.type; + +import chess.domain.board.File; +import chess.domain.board.Position; +import chess.domain.board.Rank; +import chess.domain.team.Color; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static chess.domain.team.Color.BLACK; +import static chess.domain.team.Color.WHITE; + +public class PieceFactory { + + private PieceFactory() { + } + + public static Map initialPieces(Color color) { + if (color.isWhite()) { + return whitePieces(); + } + return blackPieces(); + } + + private static Map whitePieces() { + return initializePieces(Rank.R1, Rank.R2, WHITE); + } + + private static Map blackPieces() { + return initializePieces(Rank.R8, Rank.R7, BLACK); + } + + private static Map initializePieces(final Rank rank, final Rank pawnRank, final Color color) { + Map board = new HashMap<>(); + + board.put(Position.from(File.a, rank), new Rook(color)); + board.put(Position.from(File.b, rank), new Knight(color)); + board.put(Position.from(File.c, rank), new Bishop(color)); + board.put(Position.from(File.d, rank), new Queen(color)); + board.put(Position.from(File.e, rank), new King(color)); + board.put(Position.from(File.f, rank), new Bishop(color)); + board.put(Position.from(File.g, rank), new Knight(color)); + board.put(Position.from(File.h, rank), new Rook(color)); + + Arrays.stream(File.values()) + .forEach(file -> board.put(Position.from(file, pawnRank), new Pawn(color))); + + return board; + } +} diff --git a/src/main/java/chess/domain/piece/type/PieceType.java b/src/main/java/chess/domain/piece/type/PieceType.java new file mode 100644 index 0000000..563f90e --- /dev/null +++ b/src/main/java/chess/domain/piece/type/PieceType.java @@ -0,0 +1,75 @@ +package chess.domain.piece.type; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public enum PieceType { + + KING("k", 0), + QUEEN("q", 9), + ROOK("r", 5), + BISHOP("b", 3), + KNIGHT("n", 2.5), + PAWN("p", 1); + + private static final Map, PieceType> PIECE_TYPE_MAP = createPieceTypeMap(); + private static final int DUPLICATION_THRESHOLD = 1; + private static final double PAWN_SCORE_ON_DUPLICATION = 0.5; + + private final String name; + private final double score; + + PieceType(final String name, final double score) { + this.name = name; + this.score = score; + } + + private static Map, PieceType> createPieceTypeMap() { + Map, PieceType> map = new HashMap<>(); + map.put(King.class, KING); + map.put(Queen.class, QUEEN); + map.put(Rook.class, ROOK); + map.put(Bishop.class, BISHOP); + map.put(Knight.class, KNIGHT); + map.put(Pawn.class, PAWN); + return Collections.unmodifiableMap(map); + } + + public static PieceType of(final Piece piece) { + return PIECE_TYPE_MAP.get(piece.getClass()); + } + + public static String findNameBy(final Piece piece) { + PieceType pieceType = PieceType.of(piece); + + if (piece.isWhite()) { + return pieceType.name.toLowerCase(); + } + return pieceType.name.toUpperCase(); + } + + public static double findScoreBy(final Piece piece) { + PieceType pieceType = PieceType.of(piece); + return pieceType.score; + } + + public static boolean isKing(final Piece piece) { + return PieceType.of(piece) == KING; + } + + public static boolean isPawn(final Piece piece) { + return PieceType.of(piece) == PAWN; + } + + public static boolean isNotPawn(final Piece piece) { + return !isPawn(piece); + } + + public static double sumPawnScores(final int count) { + if (count > DUPLICATION_THRESHOLD) { + return count * PAWN_SCORE_ON_DUPLICATION; + } + return PAWN.score; + } +} diff --git a/src/main/java/chess/domain/piece/type/Queen.java b/src/main/java/chess/domain/piece/type/Queen.java new file mode 100644 index 0000000..6a051b5 --- /dev/null +++ b/src/main/java/chess/domain/piece/type/Queen.java @@ -0,0 +1,11 @@ +package chess.domain.piece.type; + +import chess.domain.piece.move.MovePattern; +import chess.domain.team.Color; + +public class Queen extends Piece { + + public Queen(final Color color) { + super(MovePattern.QUEEN, color); + } +} diff --git a/src/main/java/chess/domain/piece/type/Rook.java b/src/main/java/chess/domain/piece/type/Rook.java new file mode 100644 index 0000000..c88dc6a --- /dev/null +++ b/src/main/java/chess/domain/piece/type/Rook.java @@ -0,0 +1,11 @@ +package chess.domain.piece.type; + +import chess.domain.piece.move.MovePattern; +import chess.domain.team.Color; + +public class Rook extends Piece { + + public Rook(final Color color) { + super(MovePattern.ROOK, color); + } +} diff --git a/src/main/java/chess/domain/team/Color.java b/src/main/java/chess/domain/team/Color.java new file mode 100644 index 0000000..a716fb4 --- /dev/null +++ b/src/main/java/chess/domain/team/Color.java @@ -0,0 +1,22 @@ +package chess.domain.team; + +public enum Color { + + WHITE, + BLACK; + + public boolean isWhite() { + return this == WHITE; + } + + public boolean isBlack() { + return this == BLACK; + } + + public Color flip() { + if (isWhite()) { + return BLACK; + } + return WHITE; + } +} diff --git a/src/main/java/chess/domain/team/Team.java b/src/main/java/chess/domain/team/Team.java new file mode 100644 index 0000000..796323f --- /dev/null +++ b/src/main/java/chess/domain/team/Team.java @@ -0,0 +1,134 @@ +package chess.domain.team; + +import chess.domain.board.File; +import chess.domain.board.Position; +import chess.domain.piece.move.Path; +import chess.domain.piece.type.Pawn; +import chess.domain.piece.type.Piece; +import chess.domain.piece.type.PieceFactory; +import chess.domain.piece.type.PieceType; + +import java.util.Collection; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class Team { + + private final Map pieces; + + public static Team white() { + return new Team(Color.WHITE); + } + + public static Team black() { + return new Team(Color.BLACK); + } + + private Team(final Color color) { + pieces = new HashMap<>(PieceFactory.initialPieces(color)); + } + + public void movePiece(final Position source, final Position target) { + Piece sourcePiece = findPieceBy(source); + pieces.remove(source); + pieces.put(target, sourcePiece); + } + + public Path findMovePath(final Position source, final Position target) { + Piece sourcePiece = findPieceBy(source); + return sourcePiece.findMovePath(source, target); + } + + public Collection findAttackPaths(final Position target) { + return pieces.entrySet().stream() + .map(entry -> entry.getValue().findAttackPaths(entry.getKey())) + .flatMap(Collection::stream) + .filter(path -> path.contains(target)) + .map(path -> path.getPositionsUntilTarget(target)) + .filter(Path::isNotEmpty) + .collect(Collectors.toList()); + } + + public void removePiece(final Position position) { + if (hasNoPieceOn(position)) { + return; + } + + pieces.remove(position); + } + + public Piece findPieceBy(final Position position) { + if (hasNoPieceOn(position)) { + throw new IllegalArgumentException("해당 위치에 기물이 존재하지 않습니다."); + } + + return pieces.get(position); + } + + public boolean hasPieceOn(final Position position) { + return pieces.containsKey(position); + } + + public boolean hasNoPieceOn(final Position position) { + return !hasPieceOn(position); + } + + public boolean hasKingOn(final Position position) { + Piece piece = findPieceBy(position); + return piece.isKing(); + } + + private boolean hasPawnOn(final Position position) { + Piece piece = findPieceBy(position); + return piece.isPawn(); + } + + public boolean isKingDead() { + return pieces.values().stream() + .noneMatch(Piece::isKing); + } + + public boolean isKingAlive() { + return pieces.values().stream() + .anyMatch(Piece::isKing); + } + + public boolean isPawnAttacking(final Position source, final Position target) { + Piece piece = findPieceBy(source); + if (piece.isNotPawn()) { + return false; + } + + Pawn pawn = (Pawn) piece; + return pawn.isAttacking(source, target); + } + + public double calculateScores() { + double pawnScores = calculatePawnScores(); + double scoresExceptPawn = calculateScoresExceptPawn(); + + return pawnScores + scoresExceptPawn; + } + + private double calculatePawnScores() { + Map pawnCount = new EnumMap<>(File.class); + + pieces.keySet().stream() + .filter(this::hasPawnOn) + .map(Position::getFile) + .forEach(file -> pawnCount.put(file, pawnCount.getOrDefault(file, 0) + 1)); + + return pawnCount.values().stream() + .mapToDouble(PieceType::sumPawnScores) + .sum(); + } + + private double calculateScoresExceptPawn() { + return pieces.values().stream() + .filter(PieceType::isNotPawn) + .mapToDouble(PieceType::findScoreBy) + .sum(); + } +} diff --git a/src/main/java/chess/dto/BoardDto.java b/src/main/java/chess/dto/BoardDto.java new file mode 100644 index 0000000..a9a2dd1 --- /dev/null +++ b/src/main/java/chess/dto/BoardDto.java @@ -0,0 +1,40 @@ +package chess.dto; + +import chess.domain.board.Position; +import chess.domain.game.ChessGame; +import chess.domain.piece.type.Piece; +import chess.domain.piece.type.PieceType; +import chess.exception.EmptyPositionException; + +import java.util.HashMap; +import java.util.Map; + +public class BoardDto { + + private BoardDto() { + } + + private static final String EMPTY_PIECE = "."; + + public static Map of(ChessGame chessGame) { + Map pieceOnPositions = new HashMap<>(); + + Position.names().forEach(positionKey -> { + String pieceName = findPieceName(chessGame, positionKey); + pieceOnPositions.put(positionKey, pieceName); + }); + + return pieceOnPositions; + } + + private static String findPieceName(ChessGame chessGame, String positionKey) { + Position position = Position.of(positionKey); + + try { + Piece piece = chessGame.findPieceBy(position); + return PieceType.findNameBy(piece); + } catch (EmptyPositionException e) { + return EMPTY_PIECE; + } + } +} diff --git a/src/main/java/chess/dto/Scores.java b/src/main/java/chess/dto/Scores.java new file mode 100644 index 0000000..322e3f6 --- /dev/null +++ b/src/main/java/chess/dto/Scores.java @@ -0,0 +1,20 @@ +package chess.dto; + +public class Scores { + + private final double whiteScore; + private final double blackScore; + + public Scores(final double whiteScore, final double blackScore) { + this.whiteScore = whiteScore; + this.blackScore = blackScore; + } + + public double getWhiteScore() { + return whiteScore; + } + + public double getBlackScore() { + return blackScore; + } +} diff --git a/src/main/java/chess/dto/TurnDto.java b/src/main/java/chess/dto/TurnDto.java new file mode 100644 index 0000000..40d406d --- /dev/null +++ b/src/main/java/chess/dto/TurnDto.java @@ -0,0 +1,30 @@ +package chess.dto; + +import chess.domain.game.Player; +import chess.domain.game.Turn; +import chess.domain.team.Color; + +public class TurnDto { + + private final String team; + private final String playerName; + + private TurnDto(String team, String playerName) { + this.team = team; + this.playerName = playerName; + } + + public static TurnDto of(Turn currentTurn) { + Color color = currentTurn.team(); + Player player = currentTurn.player(); + return new TurnDto(color.name(), player.name()); + } + + public String getTeam() { + return team; + } + + public String getPlayerName() { + return playerName; + } +} diff --git a/src/main/java/chess/exception/EmptyPositionException.java b/src/main/java/chess/exception/EmptyPositionException.java new file mode 100644 index 0000000..debb3de --- /dev/null +++ b/src/main/java/chess/exception/EmptyPositionException.java @@ -0,0 +1,4 @@ +package chess.exception; + +public class EmptyPositionException extends RuntimeException { +} diff --git a/src/main/java/chess/exception/ForcedTerminationException.java b/src/main/java/chess/exception/ForcedTerminationException.java new file mode 100644 index 0000000..454726c --- /dev/null +++ b/src/main/java/chess/exception/ForcedTerminationException.java @@ -0,0 +1,10 @@ +package chess.exception; + +public class ForcedTerminationException extends RuntimeException { + + private static final String message = "사용자 입력에 의해 게임이 강제 종료되었습니다."; + + public ForcedTerminationException() { + super(message); + } +} diff --git a/src/main/java/chess/exception/ScoresRequestedException.java b/src/main/java/chess/exception/ScoresRequestedException.java new file mode 100644 index 0000000..4524816 --- /dev/null +++ b/src/main/java/chess/exception/ScoresRequestedException.java @@ -0,0 +1,4 @@ +package chess.exception; + +public class ScoresRequestedException extends RuntimeException { +} diff --git a/src/main/java/chess/service/ChessService.java b/src/main/java/chess/service/ChessService.java new file mode 100644 index 0000000..af2bad1 --- /dev/null +++ b/src/main/java/chess/service/ChessService.java @@ -0,0 +1,70 @@ +package chess.service; + +import chess.domain.command.Command; +import chess.domain.command.MoveParameters; +import chess.domain.game.ChessGame; +import chess.domain.team.Color; +import chess.dto.BoardDto; +import chess.dto.Scores; +import chess.dto.TurnDto; +import chess.exception.ForcedTerminationException; +import chess.exception.ScoresRequestedException; + +import java.util.Map; + +public class ChessService { + + private final ChessGame chessGame; + + public ChessService() { + this.chessGame = new ChessGame(); + } + + public void run(Command command) { + if (command.isStart()) { + return; + } + + if (command.isEnd()) { + throw new ForcedTerminationException(); + } + + if (command.isStatus()) { + throw new ScoresRequestedException(); + } + + if (command.isMove()) { + MoveParameters parameters = command.getMoveParameters(); + movePiece(parameters); + } + } + + public boolean isGameRunning() { + return chessGame.isBothKingAlive(); + } + + public boolean isGameFinished() { + return !isGameRunning(); + } + + public void movePiece(MoveParameters parameters) { + chessGame.move(parameters); + } + + public Scores getScores() { + return chessGame.getScores(); + } + + public Map getBoardDto() { + return BoardDto.of(chessGame); + } + + public TurnDto getCurrentTurnDto() { + return TurnDto.of(chessGame.getCurrentTurn()); + } + + public String getWinnerDto() { + Color color = chessGame.getWinner(); + return color.name(); + } +} diff --git a/src/main/java/chess/view/ConsoleInputView.java b/src/main/java/chess/view/ConsoleInputView.java new file mode 100644 index 0000000..67a2f31 --- /dev/null +++ b/src/main/java/chess/view/ConsoleInputView.java @@ -0,0 +1,21 @@ +package chess.view; + +import java.util.Scanner; + +public class ConsoleInputView implements InputView { + + private static final Scanner scanner = new Scanner(System.in); + + @Override + public String getCommand() { + String input = scanner.nextLine(); + validateNull(input); + return input.trim(); + } + + private void validateNull(final String input) { + if (input == null) { + throw new IllegalArgumentException("잘못된 입력입니다."); + } + } +} diff --git a/src/main/java/chess/view/ConsoleOutputView.java b/src/main/java/chess/view/ConsoleOutputView.java new file mode 100644 index 0000000..b3df4ab --- /dev/null +++ b/src/main/java/chess/view/ConsoleOutputView.java @@ -0,0 +1,82 @@ +package chess.view; + +import chess.dto.Scores; +import chess.dto.TurnDto; + +import java.util.Map; + +public class ConsoleOutputView implements OutputView { + + private static final String HEADER = "> "; + private static final String TURN_FORMAT = HEADER + "%s팀의 %s 차례입니다.%n"; + private static final String WINNER_FORMAT = HEADER + "%s의 승리입니다. 축하합니다.%n"; + + private static final int FILE = 0; + private static final int RANK = 1; + private static final String NEW_LINE = "\n"; + + @Override + public void printGuide() { + System.out.println(HEADER + "체스 게임을 실행합니다."); + System.out.println(HEADER + "게임 시작 : start"); + System.out.println(HEADER + "게임 종료 : end"); + System.out.println(HEADER + "게임 이동 : move source위치 target위치 - 예. move b2 b3"); + } + + @Override + public void printBoard(final Map boardDto) { + StringBuilder sb = new StringBuilder(); + + boardDto.keySet().stream() + .sorted(this::comparePositionName) + .forEach(positionName -> { + String pieceName = boardDto.get(positionName); + sb.append(pieceName); + }); + + insertNewLineAtTheEndOfEachRank(sb); + + System.out.println(sb); + } + + private int comparePositionName(String o1, String o2) { + if (o1.charAt(RANK) == o2.charAt(RANK)) { + return Character.compare(o1.charAt(FILE), o2.charAt(FILE)); + } + return Character.compare(o2.charAt(RANK), o1.charAt(RANK)); + } + + private void insertNewLineAtTheEndOfEachRank(StringBuilder sb) { + for (int rank = 1; rank <= 7; rank++) { + sb.insert(endOf(rank), NEW_LINE); + } + } + + private int endOf(int rank) { + return rank * 8 + rank - 1; + } + + @Override + public void printScores(final Scores scores) { + System.out.println(HEADER + "WHITE 점수: " + scores.getWhiteScore()); + System.out.println(HEADER + "BLACK 점수: " + scores.getBlackScore()); + } + + @Override + public void printTurn(TurnDto currentTurn) { + String team = currentTurn.getTeam(); + String playerName = currentTurn.getPlayerName(); + System.out.printf(TURN_FORMAT, team, playerName); + } + + @Override + public void printWinner(String winnerName) { + System.out.printf(WINNER_FORMAT, winnerName); + } + + @Override + public void printMessage(String message) { + System.out.println(HEADER + message); + System.out.println(); + } +} diff --git a/src/main/java/chess/view/InputView.java b/src/main/java/chess/view/InputView.java new file mode 100644 index 0000000..3504758 --- /dev/null +++ b/src/main/java/chess/view/InputView.java @@ -0,0 +1,5 @@ +package chess.view; + +public interface InputView { + String getCommand(); +} diff --git a/src/main/java/chess/view/OutputView.java b/src/main/java/chess/view/OutputView.java new file mode 100644 index 0000000..947fd37 --- /dev/null +++ b/src/main/java/chess/view/OutputView.java @@ -0,0 +1,20 @@ +package chess.view; + +import chess.dto.Scores; +import chess.dto.TurnDto; + +import java.util.Map; + +public interface OutputView { + void printGuide(); + + void printBoard(Map boardDto); + + void printScores(Scores scores); + + void printTurn(TurnDto currentTurn); + + void printWinner(String winner); + + void printMessage(String message); +} diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html deleted file mode 100644 index 7c0febd..0000000 --- a/src/main/resources/templates/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - 체스 - - -

- Hello, world! -

- - diff --git a/src/test/java/chess/domain/board/PositionTest.java b/src/test/java/chess/domain/board/PositionTest.java new file mode 100644 index 0000000..f23d641 --- /dev/null +++ b/src/test/java/chess/domain/board/PositionTest.java @@ -0,0 +1,146 @@ +package chess.domain.board; + +import chess.domain.piece.move.Gap; +import chess.domain.piece.move.MoveUnit; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class PositionTest { + + @Test + @DisplayName("가로, 세로 인자에 해당하는 위치를 반환한다.") + void from_file_and_rank() { + //given + File file = File.a; + Rank rank = Rank.R1; + + //when + Position position = Position.from(file, rank); + + //then + assertThat(position).extracting("file", "rank") + .containsOnly(file, rank); + } + + @Test + @DisplayName("문자열 키에 해당하는 위치를 반환한다.") + void of_valid_key() { + //given + File file = File.a; + Rank rank = Rank.R1; + String key = file.name() + rank.getIndex(); + + //when + Position position = Position.of(key); + + //then + assertThat(position).extracting("file", "rank") + .containsOnly(file, rank); + } + + @ParameterizedTest + @ValueSource(strings = {"a0", "j1"}) + @DisplayName("유효하지 않은 키로 검색하면 예외를 던진다.") + void of_invalid_key(String key) { + //given, when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> Position.of(key)) + .withMessage("위치 입력값이 잘못되었습니다."); + } + + @Test + @DisplayName("모든 위치 이름을 반환한다.") + void names() { + //given, when + Collection names = Position.names(); + + //then + assertThat(names).hasSize(64); + } + + @Test + @DisplayName("두 위치가 주어지면 좌표 차이(target-source)를 계산한다.") + void calculate_gap() { + // given + Position source = Position.of("a1"); + Position target = Position.of("f3"); + + // when + Gap gap = target.calculateGap(source); + + // then + assertThat(gap.file()).isEqualTo(5); + assertThat(gap.rank()).isEqualTo(2); + } + + @Test + @DisplayName("거쳐가는 모든 위치를 반환한다.") + void find_passing_positions() { + // given + Position source = Position.of("d4"); + Position target = Position.of("d7"); + MoveUnit moveUnit = MoveUnit.NORTH; + + // when + Collection positions = source.findPassingPositions(target, moveUnit); + + // then + assertThat(positions) + .hasSize(2) + .containsExactlyInAnyOrder(Position.of("d5"), Position.of("d6")); + } + + @Test + @DisplayName("이동 가능한 모든 위치를 반환한다.") + void find_reachable_positions() { + // given + Position source = Position.of("d4"); + MoveUnit moveUnit = MoveUnit.SOUTH; + List expected = Arrays.asList(Position.of("d3"), Position.of("d2"), Position.of("d1")); + + // when + List reachablePositions = source.findReachablePositions(moveUnit); + + // then + assertThat(reachablePositions).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({"d4, NORTH, true", "h2, EAST, false"}) + @DisplayName("주어진 좌표만큼 이동 가능한지 확인한다.") + void is_movable(String source, MoveUnit moveUnit, boolean expected) { + // given + Position position = Position.of(source); + + // when + boolean movable = position.isMovable(moveUnit); + + // then + assertThat(movable).isSameAs(expected); + } + + @ParameterizedTest + @CsvSource({"a2, NORTH, a3", "b7, SOUTH_WEST, a6", "e5, SOUTH_EAST_LEFT, g4"}) + @DisplayName("주어진 좌표만큼 이동한 위치를 반환한다.") + void move(String sourceKey, MoveUnit moveUnit, String targetKey) { + // given + Position source = Position.of(sourceKey); + Position target = Position.of(targetKey); + + // when + Position result = source.move(moveUnit); + + // then + assertThat(result).isSameAs(target); + } +} diff --git a/src/test/java/chess/domain/game/ChessGameTest.java b/src/test/java/chess/domain/game/ChessGameTest.java new file mode 100644 index 0000000..9813b81 --- /dev/null +++ b/src/test/java/chess/domain/game/ChessGameTest.java @@ -0,0 +1,162 @@ +package chess.domain.game; + +import chess.domain.board.Position; +import chess.domain.command.MoveParameters; +import chess.domain.piece.type.Pawn; +import chess.domain.piece.type.Piece; +import chess.domain.piece.type.Rook; +import chess.domain.team.Color; +import chess.dto.Scores; +import chess.exception.EmptyPositionException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ChessGameTest { + + @ParameterizedTest + @CsvSource({"b3, b4, 자신의 기물만 움직일 수 있습니다.", // 빈칸 + "a7, a6, 자신의 기물만 움직일 수 있습니다.", // 적 기물 + "a1, a2, 자신의 기물이 있는 곳으로 이동할 수 없습니다.", + "a1, a1, 출발 위치와 도착 위치가 같을 수 없습니다.", + "a1, a3, 다른 기물을 통과하여 이동할 수 없습니다.", + "c2, d3, 폰은 공격 대상이 있는 경우에만 대각선으로 이동할 수 있습니다."}) + @DisplayName("이동할 수 없는 경우 예외가 발생한다.") + void move_exception(String source, String target, String exceptionMessage) { + //given + ChessGame chessGame = new ChessGame(); + MoveParameters moveParameters = new MoveParameters(source, target); + + //when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> chessGame.move(moveParameters)) + .withMessage(exceptionMessage); + } + + @ParameterizedTest + @CsvSource({"e2, d2", "e2, e1"}) + @DisplayName("상대방이 킹의 목적지를 공격 가능한 경우 예외가 발생한다.") + void move_king_invalid_target(String source, String target) { + //given + ChessGame chessGame = new ChessGame(); + chessGame.move(new MoveParameters("e2", "e4")); + chessGame.move(new MoveParameters("c7", "c5")); + chessGame.move(new MoveParameters("d2", "d4")); + chessGame.move(new MoveParameters("d8", "a5")); + chessGame.move(new MoveParameters("e1", "e2")); + chessGame.move(new MoveParameters("h7", "h5")); + MoveParameters moveParameters = new MoveParameters(source, target); + + //when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> chessGame.move(moveParameters)) + .withMessage("킹은 상대방이 공격 가능한 위치로 이동할 수 없습니다."); + } + + @Test + @DisplayName("위치에 있는 기물을 반환한다.") + void find_by() { + //given + Position whitePawnPosition = Position.of("d2"); + Position blackRookPosition = Position.of("a8"); + + //when + ChessGame chessGame = new ChessGame(); + Piece whitePawn = chessGame.findPieceBy(whitePawnPosition); + Piece blackRook = chessGame.findPieceBy(blackRookPosition); + + //then + assertThat(whitePawn).isInstanceOf(Pawn.class); + assertThat(whitePawn.isWhite()).isTrue(); + assertThat(blackRook).isInstanceOf(Rook.class); + assertThat(blackRook.isWhite()).isFalse(); + } + + @Test + @DisplayName("위치에 기물이 존재하지 않으면 예외를 던진다.") + void find_by_empty() { + //given + Position emptyPosition = Position.of("d5"); + + //when + ChessGame chessGame = new ChessGame(); + + //then + assertThrows(EmptyPositionException.class, () -> chessGame.findPieceBy(emptyPosition)); + } + + @Test + @DisplayName("각 팀의 점수를 계산한다.") + void get_scores() { + //given + ChessGame chessGame = new ChessGame(); + chessGame.move(new MoveParameters("a2", "a3")); + chessGame.move(new MoveParameters("e7", "e5")); + chessGame.move(new MoveParameters("d2", "d4")); + chessGame.move(new MoveParameters("e5", "e4")); + chessGame.move(new MoveParameters("b2", "b3")); + chessGame.move(new MoveParameters("a7", "a6")); + chessGame.move(new MoveParameters("f2", "f3")); + chessGame.move(new MoveParameters("e4", "f3")); + + //when + Scores scores = chessGame.getScores(); + + //then + assertThat(scores.getWhiteScore()).isEqualTo(37); + assertThat(scores.getBlackScore()).isEqualTo(37); + } + + @Test + @DisplayName("흑팀 킹이 죽으면 백팀을 승자로 반환한다.") + void get_winner_white() { + //given + ChessGame chessGame = new ChessGame(); + chessGame.move(new MoveParameters("e2", "e4")); + chessGame.move(new MoveParameters("f7", "f5")); + chessGame.move(new MoveParameters("d1", "h5")); + chessGame.move(new MoveParameters("a7", "a5")); + chessGame.move(new MoveParameters("h5", "e8")); + + //when + Color winner = chessGame.getWinner(); + + //then + assertThat(winner.isWhite()).isTrue(); + } + + @Test + @DisplayName("백팀 킹이 죽으면 흑팀을 승자로 반환한다.") + void get_winner_black() { + //given + ChessGame chessGame = new ChessGame(); + chessGame.move(new MoveParameters("f2", "f4")); + chessGame.move(new MoveParameters("e7", "e5")); + chessGame.move(new MoveParameters("a2", "a4")); + chessGame.move(new MoveParameters("d8", "h4")); + chessGame.move(new MoveParameters("b2", "b4")); + chessGame.move(new MoveParameters("h4", "e1")); + + //when + Color winner = chessGame.getWinner(); + + //then + assertThat(winner.isBlack()).isTrue(); + } + + @Test + @DisplayName("흑백 킹이 모두 살아있는 경우 승자를 요청하면 예외를 반환한다.") + void get_winner_exception() { + //given + ChessGame chessGame = new ChessGame(); + + //when, then + assertThatIllegalStateException() + .isThrownBy(chessGame::getWinner) + .withMessage("King이 잡히지 않아 승자가 없습니다."); + } +} diff --git a/src/test/java/chess/domain/game/TurnTest.java b/src/test/java/chess/domain/game/TurnTest.java new file mode 100644 index 0000000..8107f13 --- /dev/null +++ b/src/test/java/chess/domain/game/TurnTest.java @@ -0,0 +1,52 @@ +package chess.domain.game; + +import chess.domain.team.Color; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TurnTest { + + @Test + @DisplayName("플레이 차례인 플레이어를 반환한다.") + void player_turn() { + // given + Turn turn = Turn.initialTurn(); + + // when + turn.next(); + Player player = turn.player(); + + // then + assertThat(player.name()).isEqualTo("BLACK"); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8}) + @DisplayName("플레이어가 여러 명일 때 플레이 차례인 플레이어를 반환한다.") + void player_turn_multiple(int count) { + // given + List names = Arrays.asList("w1", "b1", "w2", "b2"); + Map> players = new EnumMap<>(Color.class); + players.put(Color.WHITE, Arrays.asList(Player.of(names.get(0)), Player.of(names.get(2)))); + players.put(Color.BLACK, Arrays.asList(Player.of(names.get(1)), Player.of(names.get(3)))); + Turn turn = Turn.initialTurn(players); + + // when + for (int i = 0; i < count; i++) { + turn.next(); + } + Player player = turn.player(); + + // then + assertThat(player.name()).isEqualTo(names.get(count % 4)); + } +} diff --git a/src/test/java/chess/domain/piece/move/MoveUnitsTest.java b/src/test/java/chess/domain/piece/move/MoveUnitsTest.java new file mode 100644 index 0000000..8024710 --- /dev/null +++ b/src/test/java/chess/domain/piece/move/MoveUnitsTest.java @@ -0,0 +1,38 @@ +package chess.domain.piece.move; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static chess.domain.piece.move.MoveUnit.NORTH_EAST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class MoveUnitsTest { + + @Test + @DisplayName("매칭되는 이동 단위를 반환한다.") + void find_match_move_unit() { + // given + MoveUnits moveUnits = MoveUnits.DIAGONAL; + Gap gap = new Gap(2, 2); + + // when + MoveUnit moveUnit = moveUnits.findMatchMoveUnit(gap, MoveLimit.INFINITE); + + // then + assertThat(moveUnit).isSameAs(NORTH_EAST); + } + + @Test + @DisplayName("매칭되는 이동 단위가 없는 경우 예외를 던진다.") + void find_match_move_unit_exception() { + // given + MoveUnits moveUnits = MoveUnits.KNIGHT; + Gap gap = new Gap(2, 4); + + // when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> moveUnits.findMatchMoveUnit(gap, MoveLimit.FINITE)) + .withMessage("해당 방향으로 이동할 수 없습니다."); + } +} \ No newline at end of file diff --git a/src/test/java/chess/domain/piece/move/PathTest.java b/src/test/java/chess/domain/piece/move/PathTest.java new file mode 100644 index 0000000..fa53432 --- /dev/null +++ b/src/test/java/chess/domain/piece/move/PathTest.java @@ -0,0 +1,42 @@ +package chess.domain.piece.move; + +import chess.domain.board.Position; +import chess.domain.team.Team; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PathTest { + + @Test + @DisplayName("주어진 경로에서 타겟 이전까지의 경로만 반환한다.") + void get_positions_until_target() { + // given + Path path = new Path(Position.of("d1"), Position.of("d2"), Position.of("d3"), Position.of("d4")); + Position target = Position.of("d3"); + Path expected = new Path(Position.of("d1"), Position.of("d2")); + + // when + Path positionsUntilTarget = path.getPositionsUntilTarget(target); + + // then + assertThat(positionsUntilTarget).isEqualTo(expected); + } + + @Test + @DisplayName("경로가 기물로 막혀있는지 확인한다.") + void is_blocked_by() { + // given + Path path = new Path(Position.of("c4"), Position.of("c3"), Position.of("c2")); + Team team = Team.white(); + + // when + boolean blockedBy = path.isBlockedBy(team); + boolean notBlockedBy = path.isNotBlockedBy(team); + + // then + assertThat(blockedBy).isTrue(); + assertThat(notBlockedBy).isFalse(); + } +} \ No newline at end of file diff --git a/src/test/java/chess/domain/piece/type/BishopTest.java b/src/test/java/chess/domain/piece/type/BishopTest.java new file mode 100644 index 0000000..af62af0 --- /dev/null +++ b/src/test/java/chess/domain/piece/type/BishopTest.java @@ -0,0 +1,69 @@ +package chess.domain.piece.type; + +import chess.domain.board.Position; +import chess.domain.piece.move.Path; +import chess.domain.team.Color; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.Arrays; +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class BishopTest { + + @ParameterizedTest + @CsvSource({"b6, c5", "b2, c3", "f2, e3", "f6, e5"}) + @DisplayName("출발과 도착 위치가 주어지면 지나가는 경로를 반환한다.") + void find_paths_success(String targetPosition, String expectedPosition) { + //given + Position source = Position.of("d4"); + Position target = Position.of(targetPosition); + Piece piece = new Bishop(Color.WHITE); + Path expected = new Path(Position.of(expectedPosition)); + + //when + Path path = piece.findMovePath(source, target); + + //then + assertThat(path).isEqualTo(expected); + } + + @Test + @DisplayName("도착 위치가 이동할 수 없는 경로일 경우 예외가 발생한다.") + void find_paths_invalid_target() { + //given + Position source = Position.of("c1"); + Position target = Position.of("f5"); + Piece piece = new Bishop(Color.WHITE); + + //when //then + assertThatIllegalArgumentException().isThrownBy(() -> piece.findMovePath(source, target)); + } + + @Test + @DisplayName("입력받은 위치에서 공격 가능한 위치들을 반환해준다.") + void find_available_attack_positions() { + //given + Position source = Position.of("d4"); + Piece bishop = new Bishop(Color.WHITE); + Collection expected = Arrays.asList( + new Path(Position.of("c3"), Position.of("b2"), Position.of("a1")), + new Path(Position.of("e5"), Position.of("f6"), Position.of("g7"), Position.of("h8")), + new Path(Position.of("c5"), Position.of("b6"), Position.of("a7")), + new Path(Position.of("e3"), Position.of("f2"), Position.of("g1")) + ); + + //when + Collection availableAttackPaths = bishop.findAttackPaths(source); + + //then + assertThat(availableAttackPaths) + .hasSize(expected.size()) + .containsAll(expected); + } +} diff --git a/src/test/java/chess/domain/piece/type/KingTest.java b/src/test/java/chess/domain/piece/type/KingTest.java new file mode 100644 index 0000000..83e9334 --- /dev/null +++ b/src/test/java/chess/domain/piece/type/KingTest.java @@ -0,0 +1,72 @@ +package chess.domain.piece.type; + +import chess.domain.board.Position; +import chess.domain.piece.move.Path; +import chess.domain.team.Color; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class KingTest { + + @ParameterizedTest + @ValueSource(strings = {"c3", "c4", "c5", "e3", "e4", "e5", "d3", "d5"}) + @DisplayName("출발과 도착 위치가 주어지면 지나가는 경로를 반환한다.") + void find_paths_success(String targetPosition) { + //given + Position source = Position.of("d4"); + Position target = Position.of(targetPosition); + Piece piece = new King(Color.WHITE); + Path expected = new Path(Collections.emptyList()); + + //when + Path path = piece.findMovePath(source, target); + + //then + assertThat(path).isEqualTo(expected); + } + + @ParameterizedTest + @ValueSource(strings = {"c2", "d2", "e2", "f2", "b3", "b4", "b5"}) + @DisplayName("도착 위치가 이동할 수 없는 경로일 경우 예외가 발생한다.") + void find_paths_invalid_target(String invalidTarget) { + //given + Position source = Position.of("d4"); + Position target = Position.of(invalidTarget); + Piece piece = new King(Color.WHITE); + + //when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> piece.findMovePath(source, target)); + } + + @Test + @DisplayName("입력받은 위치에서 공격 가능한 위치들을 반환해준다.") + void find_available_attack_positions() { + //given + Position source = Position.of("d4"); + Piece king = new King(Color.WHITE); + Collection expected = Arrays.asList( + new Path(source, Position.of("d3")), new Path(source, Position.of("d5")), + new Path(source, Position.of("c3")), new Path(source, Position.of("c4")), new Path(source, Position.of("c5")), + new Path(source, Position.of("e3")), new Path(source, Position.of("e4")), new Path(source, Position.of("e5")) + ); + + //when + Collection availableAttackPaths = king.findAttackPaths(source); + + + //then + assertThat(availableAttackPaths) + .hasSize(expected.size()) + .containsAll(expected); + } +} diff --git a/src/test/java/chess/domain/piece/type/KnightTest.java b/src/test/java/chess/domain/piece/type/KnightTest.java new file mode 100644 index 0000000..5a5b6dc --- /dev/null +++ b/src/test/java/chess/domain/piece/type/KnightTest.java @@ -0,0 +1,70 @@ +package chess.domain.piece.type; + +import chess.domain.board.Position; +import chess.domain.piece.move.Path; +import chess.domain.team.Color; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class KnightTest { + + @ParameterizedTest + @ValueSource(strings = {"c6", "e6", "c2", "e2", "f5", "f3", "b5", "b3"}) + @DisplayName("출발과 도착 위치가 주어지면 지나가는 경로를 반환한다.") + void find_paths_success(String targetPosition) { + //given + Position source = Position.of("d4"); + Position target = Position.of(targetPosition); + Piece piece = new Knight(Color.WHITE); + Path expected = new Path(Collections.emptyList()); + + //when + Path path = piece.findMovePath(source, target); + + //then + assertThat(path).isEqualTo(expected); + } + + @ParameterizedTest + @ValueSource(strings = {"c3", "c4", "c5", "e3", "e4", "e5", "d3", "d5"}) + @DisplayName("도착 위치가 이동할 수 없는 경로일 경우 예외가 발생한다.") + void find_paths_invalid_target(String invalidTarget) { + //given + Position source = Position.of("d4"); + Position target = Position.of(invalidTarget); + Piece piece = new Knight(Color.WHITE); + + //when //then + assertThatIllegalArgumentException() + .isThrownBy(() -> piece.findMovePath(source, target)); + } + + @Test + @DisplayName("입력받은 위치에서 공격 가능한 위치들을 반환해준다.") + void find_available_attack_positions() { + //given + Position source = Position.of("d4"); + Piece knight = new Knight(Color.WHITE); + Collection expected = Arrays.asList( + new Path(source, Position.of("c6")), new Path(source, Position.of("c2")), new Path(source, Position.of("e6")), new Path(source, Position.of("e2")), + new Path(source, Position.of("b5")), new Path(source, Position.of("b3")), new Path(source, Position.of("f5")), new Path(source, Position.of("f3")) + ); + + //when + Collection availableAttackPositions = knight.findAttackPaths(source); + + //then + assertThat(availableAttackPositions) + .hasSize(expected.size()) + .containsAll(expected); + } +} diff --git a/src/test/java/chess/domain/piece/type/PawnTest.java b/src/test/java/chess/domain/piece/type/PawnTest.java new file mode 100644 index 0000000..46f8246 --- /dev/null +++ b/src/test/java/chess/domain/piece/type/PawnTest.java @@ -0,0 +1,132 @@ +package chess.domain.piece.type; + +import chess.domain.board.Position; +import chess.domain.piece.move.Path; +import chess.domain.team.Color; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class PawnTest { + + @ParameterizedTest + @CsvSource({"b2, b3, WHITE", "b7, b6, BLACK"}) + @DisplayName("최초 이동 시 1칸 전진한다.") + void find_paths_success_move_count_one_on_initial_move(String sourcePosition, String targetPosition, Color color) { + //given + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + Piece piece = new Pawn(color); + Path expected = new Path(Collections.emptyList()); + + //when + Path path = piece.findMovePath(source, target); + + //then + assertThat(path).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({"b2, b4, WHITE, b3", "b7, b5, BLACK, b6"}) + @DisplayName("최초 이동시 2칸 전진하면 지나가는 경로를 반환한다.") + void find_paths_success_move_count_two_on_initial_move(String sourcePosition, String targetPosition, Color color, String expectedPosition) { + //given + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + Piece piece = new Pawn(color); + Path expected = new Path(Position.of(expectedPosition)); + + //when + Path path = piece.findMovePath(source, target); + + //then + assertThat(path).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({"d4, d5, WHITE", "d4, c5, WHITE", "d4, e5, WHITE", + "d4, d3, BLACK", "d4, c3, BLACK", "d4, e3, BLACK"}) + @DisplayName("최초 이동이 아닌 경우 1칸 전진한다.") + void find_paths_success_move_count_one(String sourcePosition, String targetPosition, Color color) { + //given + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + Piece piece = new Pawn(color); + Path expected = new Path(Collections.emptyList()); + + //when + Path path = piece.findMovePath(source, target); + + //then + assertThat(path).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({"d2, d5, WHITE", "d7, d4, BLACK"}) + @DisplayName("최초 이동 시 2칸 초과 전진하면 예외가 발생한다.") + void find_paths_fail_move_invalid_count_on_initial_move(String sourcePosition, String targetPosition, Color color) { + //given + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + Piece piece = new Pawn(color); + + //when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> piece.findMovePath(source, target)); + } + + @ParameterizedTest + @CsvSource({"d4, d6, WHITE", "d4, d2, BLACK"}) + @DisplayName("최초 이동이 아닌 경우 1칸 초과 전진하면 예외가 발생한다.") + void find_paths_fail_move_invalid_count(String sourcePosition, String targetPosition, Color color) { + //given + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + Piece piece = new Pawn(color); + + //when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> piece.findMovePath(source, target)); + } + + @ParameterizedTest + @CsvSource({"d2, d1, WHITE", "d7, d8, BLACK"}) + @DisplayName("후진 시 예외가 발생한다.") + void find_paths_fail_move_backward(String sourcePosition, String targetPosition, Color color) { + //given + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + Piece piece = new Pawn(color); + + //when //then + assertThatIllegalArgumentException() + .isThrownBy(() -> piece.findMovePath(source, target)); + } + + @Test + @DisplayName("입력받은 위치에서 공격 가능한 위치들을 반환해준다.") + void find_available_attack_positions() { + //given + Position source = Position.of("d4"); + Piece pawn = new Pawn(Color.WHITE); + Collection expected = Arrays.asList( + new Path(source, Position.of("c5")), new Path(source, Position.of("e5")) + ); + + //when + Collection availableAttackPaths = pawn.findAttackPaths(source); + + //then + assertThat(availableAttackPaths) + .hasSize(expected.size()) + .containsAll(expected); + } +} diff --git a/src/test/java/chess/domain/piece/type/PieceTest.java b/src/test/java/chess/domain/piece/type/PieceTest.java new file mode 100644 index 0000000..7ec21b2 --- /dev/null +++ b/src/test/java/chess/domain/piece/type/PieceTest.java @@ -0,0 +1,55 @@ +package chess.domain.piece.type; + +import chess.domain.team.Color; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PieceTest { + + @ParameterizedTest + @CsvSource({"WHITE, true", "BLACK, false"}) + @DisplayName("색상을 인자로 받아 객체를 생성한다.") + void create(Color color, boolean expected) { + //given, when + Piece piece = new Pawn(color); + + //then + assertThat(piece.isWhite()).isEqualTo(expected); + } + + @Test + @DisplayName("폰인지 확인한다.") + void is_pawn() { + // given + Piece pawn = new Pawn(Color.WHITE); + Piece rook = new Rook(Color.BLACK); + + // when + boolean isPawn = pawn.isPawn(); + boolean isNotPawn = rook.isNotPawn(); + + // then + assertThat(isPawn).isTrue(); + assertThat(isNotPawn).isTrue(); + } + + @Test + @DisplayName("킹인지 확인한다.") + void is_king() { + // given + Piece pawn = new Pawn(Color.WHITE); + Piece king = new King(Color.BLACK); + + // when + boolean isKing = king.isKing(); + boolean isNotKing = pawn.isKing(); + + // then + assertThat(isKing).isTrue(); + assertThat(isNotKing).isFalse(); + } +} diff --git a/src/test/java/chess/domain/piece/type/PieceTypeTest.java b/src/test/java/chess/domain/piece/type/PieceTypeTest.java new file mode 100644 index 0000000..900df37 --- /dev/null +++ b/src/test/java/chess/domain/piece/type/PieceTypeTest.java @@ -0,0 +1,91 @@ +package chess.domain.piece.type; + +import chess.domain.team.Color; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PieceTypeTest { + + @Test + @DisplayName("피스에 해당하는 피스 타입을 반환한다.") + void of() { + // given + Piece piece = new Pawn(Color.WHITE); + + // when + PieceType pieceType = PieceType.of(piece); + + // then + assertThat(pieceType).isSameAs(PieceType.PAWN); + } + + @ParameterizedTest + @MethodSource("createParamsForName") + @DisplayName("피스가 주어지면 해당 피스와 색상에 맞는 이름을 반환한다.") + void find_name_by(Piece piece, String expected) { + //given, when + String name = PieceType.findNameBy(piece); + + //then + assertThat(name).isEqualTo(expected); + } + + @ParameterizedTest + @MethodSource("createParamsForScore") + @DisplayName("피스가 주어지면 해당 피스의 점수를 반환한다.") + void get_score(Piece piece, double expected) { + //given, when + double score = PieceType.findScoreBy(piece); + + //then + assertThat(score).isEqualTo(expected); + } + + @Test + @DisplayName("킹인지 확인한다.") + void is_king() { + // given + Piece king = new King(Color.WHITE); + Piece queen = new Queen(Color.WHITE); + + // when, then + assertThat(PieceType.isKing(king)).isTrue(); + assertThat(PieceType.isKing(queen)).isFalse(); + } + + @Test + @DisplayName("폰인지 확인한다.") + void is_pawn() { + // given + Piece pawn = new Pawn(Color.WHITE); + Piece queen = new Queen(Color.WHITE); + + // when, then + assertThat(PieceType.isPawn(pawn)).isTrue(); + assertThat(PieceType.isPawn(queen)).isFalse(); + } + + private static Stream createParamsForName() { + return Stream.of( + Arguments.of(new Pawn(Color.WHITE), "p"), + Arguments.of(new Pawn(Color.BLACK), "P") + ); + } + + private static Stream createParamsForScore() { + return Stream.of( + Arguments.of(new Pawn(Color.WHITE), 1), + Arguments.of(new Knight(Color.WHITE), 2.5), + Arguments.of(new Bishop(Color.WHITE), 3), + Arguments.of(new Rook(Color.WHITE), 5), + Arguments.of(new Queen(Color.WHITE), 9) + ); + } +} diff --git a/src/test/java/chess/domain/piece/type/QueenTest.java b/src/test/java/chess/domain/piece/type/QueenTest.java new file mode 100644 index 0000000..8128d23 --- /dev/null +++ b/src/test/java/chess/domain/piece/type/QueenTest.java @@ -0,0 +1,96 @@ +package chess.domain.piece.type; + +import chess.domain.board.Position; +import chess.domain.piece.move.Path; +import chess.domain.team.Color; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class QueenTest { + + @ParameterizedTest + @CsvSource({"b6, c5", "b2, c3", "f2, e3", "f6, e5"}) + @DisplayName("출발과 도착 위치가 주어지면 지나가는 경로를 반환한다.") + void find_paths_success_diagonal(String targetPosition, String expectedPosition) { + //given + Position source = Position.of("d4"); + Position target = Position.of(targetPosition); + Piece piece = new Queen(Color.WHITE); + Path expected = new Path(Position.of(expectedPosition)); + + //when + Path path = piece.findMovePath(source, target); + + //then + assertThat(path).isEqualTo(expected); + } + + @ParameterizedTest + @ValueSource(strings = {"d5", "c4", "e4", "d3"}) + @DisplayName("출발과 도착 위치가 주어지면 지나가는 경로를 반환한다.") + void find_paths_success_cardinal(String targetPosition) { + //given + Position source = Position.of("d4"); + Position target = Position.of(targetPosition); + Piece piece = new Queen(Color.WHITE); + Path expected = new Path(Collections.emptyList()); + + //when + Path paths = piece.findMovePath(source, target); + + //then + assertThat(paths).isEqualTo(expected); + } + + @ParameterizedTest + @ValueSource(strings = {"f5", "e6", "b3", "c2"}) + @DisplayName("도착 위치가 이동할 수 없는 경로일 경우 예외가 발생한다.") + void find_paths_invalid_target(String invalidTarget) { + //given + Position source = Position.of("d4"); + Position target = Position.of(invalidTarget); + Piece piece = new Queen(Color.WHITE); + + //when //then + assertThatIllegalArgumentException() + .isThrownBy(() -> piece.findMovePath(source, target)); + } + + @Test + @DisplayName("입력받은 위치에서 공격 가능한 위치들을 반환해준다.") + void find_available_attack_positions() { + //given + Position source = Position.of("d4"); + Piece queen = new Queen(Color.WHITE); + Collection expected = Arrays.asList( + // diagonal + new Path(Position.of("c3"), Position.of("b2"), Position.of("a1")), + new Path(Position.of("e5"), Position.of("f6"), Position.of("g7"), Position.of("h8")), + new Path(Position.of("c5"), Position.of("b6"), Position.of("a7")), + new Path(Position.of("e3"), Position.of("f2"), Position.of("g1")), + // cardinal + new Path(Position.of("d3"), Position.of("d2"), Position.of("d1")), + new Path(Position.of("d5"), Position.of("d6"), Position.of("d7"), Position.of("d8")), + new Path(Position.of("c4"), Position.of("b4"), Position.of("a4")), + new Path(Position.of("e4"), Position.of("f4"), Position.of("g4"), Position.of("h4")) + ); + + //when + Collection availableAttackPaths = queen.findAttackPaths(source); + + //then + assertThat(availableAttackPaths) + .hasSize(expected.size()) + .containsAll(expected); + } +} diff --git a/src/test/java/chess/domain/piece/type/RookTest.java b/src/test/java/chess/domain/piece/type/RookTest.java new file mode 100644 index 0000000..fc76d9c --- /dev/null +++ b/src/test/java/chess/domain/piece/type/RookTest.java @@ -0,0 +1,69 @@ +package chess.domain.piece.type; + +import chess.domain.board.Position; +import chess.domain.piece.move.Path; +import chess.domain.team.Color; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.Arrays; +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class RookTest { + + @ParameterizedTest + @CsvSource({"d2, d3", "d6, d5", "b4, c4", "f4, e4"}) + @DisplayName("출발과 도착 위치가 주어지면 지나가는 경로를 반환한다.") + void find_paths_success(String targetPosition, String expectedPosition) { + //given + Position source = Position.of("d4"); + Position target = Position.of(targetPosition); + Piece piece = new Rook(Color.WHITE); + Path expected = new Path(Position.of(expectedPosition)); + + //when + Path path = piece.findMovePath(source, target); + + //then + assertThat(path).isEqualTo(expected); + } + + @Test + @DisplayName("도착 위치가 이동할 수 없는 경로일 경우 예외가 발생한다.") + void find_paths_invalid_target() { + //given + Position source = Position.of("c1"); + Position target = Position.of("f5"); + Piece piece = new Rook(Color.WHITE); + + //when //then + assertThatIllegalArgumentException().isThrownBy(() -> piece.findMovePath(source, target)); + } + + @Test + @DisplayName("입력받은 위치에서 공격 가능한 위치들을 반환해준다.") + void find_available_attack_positions() { + //given + Position source = Position.of("d4"); + Piece rook = new Rook(Color.WHITE); + Collection expected = Arrays.asList( + new Path(Position.of("d3"), Position.of("d2"), Position.of("d1")), + new Path(Position.of("d5"), Position.of("d6"), Position.of("d7"), Position.of("d8")), + new Path(Position.of("c4"), Position.of("b4"), Position.of("a4")), + new Path(Position.of("e4"), Position.of("f4"), Position.of("g4"), Position.of("h4")) + ); + + //when + Collection availableAttackPaths = rook.findAttackPaths(source); + + //then + assertThat(availableAttackPaths) + .hasSize(expected.size()) + .containsAll(expected); + } +} diff --git a/src/test/java/chess/domain/team/ColorTest.java b/src/test/java/chess/domain/team/ColorTest.java new file mode 100644 index 0000000..a83662e --- /dev/null +++ b/src/test/java/chess/domain/team/ColorTest.java @@ -0,0 +1,35 @@ +package chess.domain.team; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ColorTest { + + @Test + @DisplayName("백에서 흑으로 색을 반전한다.") + void next_turn_white() { + // given + Color color = Color.WHITE; + + // when + Color next = color.flip(); + + // then + assertThat(next.isBlack()).isTrue(); + } + + @Test + @DisplayName("흑에서 백으로 색을 반전한다.") + void next_turn_black() { + // given + Color color = Color.BLACK; + + // when + Color next = color.flip(); + + // then + assertThat(next.isWhite()).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/chess/domain/team/TeamTest.java b/src/test/java/chess/domain/team/TeamTest.java new file mode 100644 index 0000000..8564c9f --- /dev/null +++ b/src/test/java/chess/domain/team/TeamTest.java @@ -0,0 +1,168 @@ +package chess.domain.team; + +import chess.domain.board.Position; +import chess.domain.piece.move.Path; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class TeamTest { + + @Test + @DisplayName("WHITE 팀 객체를 생성한다.") + void create_with_color() { + //given + Position present = Position.of("a1"); + Position notPresent = Position.of("a8"); + + // when + Team team = Team.white(); + + //then + assertThat(team.hasPieceOn(present)).isTrue(); + assertThat(team.hasNoPieceOn(notPresent)).isTrue(); + } + + @Test + @DisplayName("BLACK 팀 객체를 생성한다.") + void create_black_team() { + //given + Position present = Position.of("a8"); + Position notPresent = Position.of("a1"); + + // when + Team team = Team.black(); + + //then + assertThat(team.hasPieceOn(present)).isTrue(); + assertThat(team.hasNoPieceOn(notPresent)).isTrue(); + } + + @Test + @DisplayName("기물을 움직인다.") + void update_board() { + //given + Team team = Team.white(); + Position source = Position.of("b2"); + Position target = Position.of("b3"); + + //when + team.movePiece(source, target); + + //then + assertThat(team.hasNoPieceOn(source)).isTrue(); + assertThat(team.hasPieceOn(target)).isTrue(); + } + + @Test + @DisplayName("이동시킬 기물이 존재하지 않을 경우 예외가 발생한다.") + void update_source_position_empty() { + //given + Team team = Team.white(); + Position source = Position.of("b3"); + Position target = Position.of("b4"); + + //when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> team.movePiece(source, target)) + .withMessage("해당 위치에 기물이 존재하지 않습니다."); + } + + @Test + @DisplayName("기물 이동 경로를 반환한다.") + void find_paths() { + //given + Team team = Team.white(); + Position source = Position.of("b2"); + Position target = Position.of("b4"); + Path expected = new Path(Position.of("b3")); + + //when + Path path = team.findMovePath(source, target); + + //then + assertThat(path).isEqualTo(expected); + } + + @Test + @DisplayName("공격 가능한 모든 경로를 반환한다.") + void find_attack_paths() { + // given + Team team = Team.white(); + Position target = Position.of("c3"); + Collection expected = Arrays.asList( + new Path(Position.of("b2")), + new Path(Position.of("d2")) + ); + team.removePiece(Position.of("b1")); + + // when + Collection attackPaths = team.findAttackPaths(target); + + // then + assertThat(attackPaths) + .hasSize(expected.size()) + .containsAll(expected); + } + + @Test + @DisplayName("적에게 공격받은 경우 기물을 제거한다.") + void was_attacked_by() { + // given + Team team = Team.white(); + Position target = Position.of("e2"); + + // when + team.removePiece(target); + + // then + assertThat(team.hasNoPieceOn(target)).isTrue(); + } + + @Test + @DisplayName("주어진 위치에 킹이 있는지 확인한다.") + void has_king_on() { + // given + Team team = Team.white(); + + // when + boolean isKing = team.hasKingOn(Position.of("e1")); + boolean isNotKing = team.hasKingOn(Position.of("e2")); + + // then + assertThat(isKing).isTrue(); + assertThat(isNotKing).isFalse(); + } + + @Test + @DisplayName("킹이 존재하는지 반환한다.") + void is_king_dead() { + //given + Team team = Team.white(); + team.removePiece(Position.of("e1")); + + //when + boolean kingDead = team.isKingDead(); + + //then + assertThat(kingDead).isTrue(); + } + + @Test + @DisplayName("현재 남아있는 피스의 점수 합을 구한다.") + void sum_scores() { + //given + Team team = Team.white(); + + //when + double sum = team.calculateScores(); + + //then + assertThat(sum).isEqualTo(38); + } +} diff --git a/src/test/java/empty.txt b/src/test/java/empty.txt deleted file mode 100644 index e69de29..0000000