diff --git a/README.md b/README.md index 4a9cf0f..3cb26bf 100644 --- a/README.md +++ b/README.md @@ -1 +1,101 @@ # java-chess 게임 + +## 구현해야할 목록 + +- [x] 입력 + - [x] 명령어 입력 +- [x] 출력 + - [x] 시작 안내 문구 출력 + - [x] 체스판 전체 출력 + - [x] 점수와 결과 출력 + +- [x] 체스게임(ChessGame) + - [x] 체스판 + - [x] 현재 플레이어 + - [x] 게임 시작&종료 상태 제어 + + - 기능 + - [x] 명령어 검증 및 처리 + - [x] start : 게임 실행 + - [x] end : 게임 종료 + - [x] move : 인자로 전달받은 source 위치에서 target 위치로 기물 이동 + +- [x] 체스판(Board) + - [x] 체스판 + - [x] 생성 시 32개의 기물 초기화 + + - 기능 + - [x] 인자로 전달받은 위치에 기물이 있는지 확인 + - [x] ERROR : 존재하지 않을 경우 + - [x] 같은 색상 기물인지 확인 + - [x] ERROR : 다른 색상일 경우 + - [x] 시작과 도착 위치의 기물이 다른 색상인지 확인 + - [x] ERROR : 같은 색상일 경우 + - [x] source 위치에서 target 위치로 기물 이동 + - [x] ERROR : source 위치에 기물이 없는 경우 + - [x] ERROR : 자신의 기물이 아닌 경우 + - [x] ERROR : source, target 위치의 기물 색상이 같을 경우 + - [x] ERROR : source, target 위치가 같을 경우 + - [x] ERROR : 이동 경로에 기물이 존재할 경우 + +- [x] 기물(Piece) + - [x] 색상 + + - [x] 기물 위치(Position) + - [x] 파일 + - [x] 랭크 + - [x] 64개의 캐싱된 위치 + + - 기능 + - [x] 전달받은 인자에 해당하는 위치 객체 반환 + + - [x] 기물 색상(Color) + - [x] 색상 + + - [x] 기물 이름 매퍼 + - [x] 기물 종류에 따라 이름을 매핑 + +- [x] 플레이어(Player) + - [x] 자신의 기물 + - [x] 자신이 공격 가능한 범위 + + - 기능 + - [x] 기물을 이동시킨다. + - [x] 기물의 이동 경로를 반환한다. + - [x] 입력받은 위치에 기물이 있는지 확인한다. + +## 이동 규칙 + +- [x] 폰 (1 or 0.5) + - [x] 적 방향 직선 1칸 이동 + - [x] ERROR : 직선 방향 2칸 이상일 경우 + - [x] ERROR : 좌, 우 이동이 포함될 경우 + - [x] 처음 이동 시에는 2칸 이동 가능 + - [x] ERROR : 직선 방향 3칸 이상일 경우 + - [x] ERROR : 좌, 우 이동이 포함될 경우 + + - [x] 공격 : 적 방향 좌, 우 대각선 1칸 + - [x] ERROR : 직선 1칸 && 좌 또는 우 1칸이 아닐 경우 + +- [x] 룩 (5) + - [x] 모든 직선 방향으로 원하는 만큼 이동 가능 + - [x] ERROR : 룩 이동 패턴으로 이동할 수 없는 위치일 경우 + +- [x] 나이트 (2.5) + - [x] 모든 직선 방향 1칸 + 이동한 직선 방향의 좌, 우 대각선 1칸으로 이동 가능 + - [x] ERROR : 나이트 이동 패턴으로 이동할 수 없는 위치일 경우 + - [x] 진행 방향이 가로막혀도 적, 아군 상관없이 뛰어넘을 수 있다. + +- [x] 비숍 (3) + - [x] 모든 대각선 방향으로 원하는 만큼 이동 가능 + - [x] ERROR : 비숍 이동 패턴으로 이동할 수 없는 위치일 경우 + +- [x] 퀸 (9) + - [x] 모든 방향 1칸 + α 이동 (모든 대각선 방향으로는 원하는 만큼 이동 가능) + - [x] ERROR : 퀸 이동 패턴으로 이동할 수 없는 위치일 경우 + +- [x] 킹 + - [x] 모든 방향 1칸 이동 + - [x] ERROR : 킹 이동 패턴으로 이동할 수 없는 위치일 경우 + - [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/ConsoleApplication.java b/src/main/java/chess/ConsoleApplication.java new file mode 100644 index 0000000..d5f97f2 --- /dev/null +++ b/src/main/java/chess/ConsoleApplication.java @@ -0,0 +1,17 @@ +package chess; + +import chess.controller.ChessController; +import chess.view.ConsoleInputView; +import chess.view.ConsoleOutputView; +import chess.view.InputView; +import chess.view.OutputView; + +public class ConsoleApplication { + + public static void main(String[] args) { + OutputView outputView = new ConsoleOutputView(); + InputView inputView = new ConsoleInputView(); + ChessController chessController = new ChessController(inputView, outputView); + chessController.run(); + } +} diff --git a/src/main/java/chess/controller/ChessController.java b/src/main/java/chess/controller/ChessController.java new file mode 100644 index 0000000..aa70151 --- /dev/null +++ b/src/main/java/chess/controller/ChessController.java @@ -0,0 +1,88 @@ +package chess.controller; + +import chess.controller.dto.BoardDto; +import chess.domain.ChessGame; +import chess.domain.board.Status; +import chess.domain.command.CommandOptions; +import chess.view.InputView; +import chess.view.OutputView; + +public class ChessController { + + private final InputView inputView; + private final OutputView outputView; + + public ChessController(final InputView inputView, final OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public void run() { + ChessGame chessGame = initialize(); + + while (chessGame.isRunning()) { + play(chessGame); + } + + printResult(chessGame); + } + + private ChessGame initialize() { + outputView.printGuide(); + ChessGame chessGame = new ChessGame(); + CommandOptions initialCommandOptions = CommandOptions.of(inputView.getCommand()); + executeInitialCommand(initialCommandOptions, chessGame); + + return chessGame; + } + + private void executeInitialCommand(final CommandOptions commandOptions, final ChessGame chessGame) { + commandOptions.validateInitialCommand(); + + if (commandOptions.isEnd()) { + chessGame.end(); + } + } + + private void play(final ChessGame chessGame) { + try { + outputView.printTurn(chessGame.isWhiteTurn()); + CommandOptions commandOptions = CommandOptions.of(inputView.getCommand()); + executeCommand(chessGame, commandOptions); + printBoard(chessGame); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + } + } + + private void executeCommand(final ChessGame chessGame, final CommandOptions commandOptions) { + if (commandOptions.isEnd()) { + chessGame.end(); + return; + } + + if (commandOptions.isMove()) { + chessGame.move(commandOptions.getMoveOptions()); + return; + } + + if (commandOptions.isStatus()) { + printStatus(chessGame); + } + } + + private void printBoard(final ChessGame chessGame) { + BoardDto boardDto = new BoardDto(chessGame.getBoard()); + outputView.printBoard(boardDto); + } + + private void printStatus(final ChessGame chessGame) { + Status status = chessGame.getStatus(); + outputView.printStatus(status); + } + + private void printResult(final ChessGame chessGame) { + printBoard(chessGame); + printStatus(chessGame); + } +} diff --git a/src/main/java/chess/controller/dto/BoardDto.java b/src/main/java/chess/controller/dto/BoardDto.java new file mode 100644 index 0000000..78a66db --- /dev/null +++ b/src/main/java/chess/controller/dto/BoardDto.java @@ -0,0 +1,41 @@ +package chess.controller.dto; + +import chess.domain.board.Board; +import chess.domain.board.File; +import chess.domain.board.Rank; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceResolver; +import chess.domain.player.Position; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class BoardDto { + private List positionDtos = new ArrayList<>(); + + public BoardDto(final Board board) { + Arrays.stream(Rank.values()).forEach(rank -> Arrays.stream( + File.values()).forEach(file -> addPositionDto(file, rank, board))); + } + + private void addPositionDto(final File file, final Rank rank, final Board board) { + Position position = Position.from(file, rank); + + if (board.isEmpty(position)) { + PositionDto positionDto = new PositionDto(position.getFile()); + positionDtos.add(positionDto); + return; + } + + Piece piece = board.findBy(position); + String name = PieceResolver.findNameBy(piece); + PositionDto positionDto = new PositionDto(position.getFile(), name); + positionDtos.add(positionDto); + } + + public List getPositionDtos() { + return Collections.unmodifiableList(positionDtos); + } +} diff --git a/src/main/java/chess/controller/dto/PositionDto.java b/src/main/java/chess/controller/dto/PositionDto.java new file mode 100644 index 0000000..32493a9 --- /dev/null +++ b/src/main/java/chess/controller/dto/PositionDto.java @@ -0,0 +1,28 @@ +package chess.controller.dto; + +import chess.domain.board.File; + +public class PositionDto { + + private static final String DEFAULT_NAME = "."; + + private final boolean isLastFile; + private final String name; + + public PositionDto(final File file, final String name) { + this.isLastFile = (file == File.H); + this.name = name; + } + + public PositionDto(final File file) { + this(file, DEFAULT_NAME); + } + + public boolean isLastFile() { + return isLastFile; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/chess/domain/ChessGame.java b/src/main/java/chess/domain/ChessGame.java new file mode 100644 index 0000000..4ba805d --- /dev/null +++ b/src/main/java/chess/domain/ChessGame.java @@ -0,0 +1,44 @@ +package chess.domain; + +import chess.domain.board.Board; +import chess.domain.board.Status; +import chess.domain.command.MoveOptions; + +public class ChessGame { + + private final Board board = new Board(); + private boolean isRunning = true; + private boolean isWhiteTurn = true; + + public ChessGame() { + } + + public void move(final MoveOptions moveOptions) { + board.move(moveOptions, isWhiteTurn); + isWhiteTurn = !isWhiteTurn; + + if (board.isEnd()) { + isRunning = false; + } + } + + public void end() { + isRunning = false; + } + + public Status getStatus() { + return board.getStatus(); + } + + public boolean isRunning() { + return isRunning; + } + + public Board getBoard() { + return board; + } + + public boolean isWhiteTurn() { + return isWhiteTurn; + } +} diff --git a/src/main/java/chess/domain/board/Board.java b/src/main/java/chess/domain/board/Board.java new file mode 100644 index 0000000..32f2a3b --- /dev/null +++ b/src/main/java/chess/domain/board/Board.java @@ -0,0 +1,109 @@ +package chess.domain.board; + +import chess.domain.command.MoveOptions; +import chess.domain.piece.Color; +import chess.domain.piece.Piece; +import chess.domain.player.Player; +import chess.domain.player.Position; + +import java.util.Collection; + +public class Board { + private final Player white; + private final Player black; + + public Board() { + this.white = new Player(Color.WHITE); + this.black = new Player(Color.BLACK); + } + + public void move(final MoveOptions moveOptions, final boolean isWhiteTurn) { + Player player = currentPlayer(isWhiteTurn); + Player enemy = currentPlayer(!isWhiteTurn); + Position source = moveOptions.getSource(); + Position target = moveOptions.getTarget(); + + validate(player, enemy, source, target); + + enemy.removePieceOn(target); + movePiece(player, source, target); + } + + private Player currentPlayer(final boolean isWhiteTurn) { + if (isWhiteTurn) { + return white; + } + return black; + } + + private void validate(final Player player, final Player enemy, final Position source, final Position target) { + validateSourceOwner(enemy, source); + validateSamePosition(source, target); + validateTarget(player, target); + validateKingMovable(player, enemy, source, target); + } + + private void validateSourceOwner(final Player enemy, final Position source) { + if (enemy.hasPieceOn(source)) { + throw new IllegalArgumentException("자신의 기물만 움직일 수 있습니다."); + } + } + + private void validateSamePosition(final Position source, final Position target) { + if (source.equals(target)) { + throw new IllegalArgumentException("출발 위치와 도착 위치가 같을 수 없습니다."); + } + } + + private void validateTarget(final Player player, final Position target) { + if (player.hasPieceOn(target)) { + throw new IllegalArgumentException("같은 색상의 기물은 공격할 수 없습니다."); + } + } + + private void validateKingMovable(final Player player, final Player enemy, final Position source, final Position target) { + if (player.hasKingOn(source) && enemy.canAttack(target)) { + throw new IllegalArgumentException("킹은 상대방이 공격 가능한 위치로 이동할 수 없습니다."); + } + } + + private void movePiece(final Player player, final Position source, final Position target) { + Collection paths = player.findPaths(source, target); + validatePathsEmpty(paths); + player.update(source, target); + } + + private void validatePathsEmpty(final Collection paths) { + boolean isWhiteBlocked = paths.stream() + .anyMatch(white::hasPieceOn); + boolean isBlackBlocked = paths.stream() + .anyMatch(black::hasPieceOn); + + if (isWhiteBlocked || isBlackBlocked) { + throw new IllegalArgumentException("기물을 통과하여 이동할 수 없습니다."); + } + } + + public Piece findBy(final Position position) { + if (white.hasPieceOn(position)) { + return white.findPieceBy(position); + } + + return black.findPieceBy(position); + } + + public boolean isEmpty(final Position position) { + return !white.hasPieceOn(position) && !black.hasPieceOn(position); + } + + public Status getStatus() { + double whiteScore = white.sumScores(); + double blackScore = black.sumScores(); + + return new Status(whiteScore, blackScore, white.isKingDead(), black.isKingDead()); + } + + public boolean isEnd() { + return white.isKingDead() || black.isKingDead(); + } +} 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..b55f17e --- /dev/null +++ b/src/main/java/chess/domain/board/File.java @@ -0,0 +1,62 @@ +package chess.domain.board; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public enum File { + A(1), + B(2), + C(3), + D(4), + E(5), + F(6), + G(7), + H(8); + + private static final Map FILES = createFiles(); + + private final int index; + + File(final int index) { + this.index = index; + } + + private static Map createFiles() { + HashMap files = new HashMap<>(); + + Arrays.stream(values()) + .forEach(file -> files.put(file.index, file)); + + return Collections.unmodifiableMap(files); + } + + public static File of(final int fileIndex) { + File file = FILES.get(fileIndex); + + if (file == null) { + throw new IllegalArgumentException("일치하는 파일이 존재하지 않습니다."); + } + + return file; + } + + public int calculateGap(final File file) { + return this.index - file.index; + } + + public File move(final int amount) { + return File.of(this.index + amount); + } + + public boolean canMove(final int amount) { + int fileIndex = index + amount; + + return isInRange(fileIndex); + } + + private boolean isInRange(final int fileIndex) { + return fileIndex >= A.index && fileIndex <= H.index; + } +} 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..b1511c1 --- /dev/null +++ b/src/main/java/chess/domain/board/Rank.java @@ -0,0 +1,67 @@ +package chess.domain.board; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public enum Rank { + R8(8), + R7(7), + R6(6), + R5(5), + R4(4), + R3(3), + R2(2), + R1(1); + + private static final Map RANKS = createRanks(); + + private final int index; + + Rank(final int index) { + this.index = index; + } + + private static Map createRanks() { + Map ranks = new HashMap<>(); + + Arrays.stream(values()) + .forEach(rank -> ranks.put(rank.index, rank)); + + return Collections.unmodifiableMap(ranks); + } + + public static Rank of(final int rankIndex) { + Rank rank = RANKS.get(rankIndex); + + if (rank == null) { + throw new IllegalArgumentException("일치하는 랭크가 존재하지 않습니다."); + } + + return rank; + } + + public int getIndex() { + return index; + } + + public int calculateGap(final Rank rank) { + return this.index - rank.index; + } + + public Rank move(final int amount) { + return Rank.of(this.index + amount); + } + + public boolean canMove(final int amount) { + int rankIndex = index + amount; + + return isInRange(rankIndex); + } + + private boolean isInRange(final int rankIndex) { + return rankIndex >= R1.index && rankIndex <= R8.index; + } +} + diff --git a/src/main/java/chess/domain/board/Status.java b/src/main/java/chess/domain/board/Status.java new file mode 100644 index 0000000..b48f253 --- /dev/null +++ b/src/main/java/chess/domain/board/Status.java @@ -0,0 +1,31 @@ +package chess.domain.board; + +public class Status { + private final double whiteScore; + private final double blackScore; + private final boolean isWhiteKingDead; + private final boolean isBlackKingDead; + + public Status(final double whiteScore, final double blackScore, boolean isWhiteKingDead, boolean isBlackKingDead) { + this.whiteScore = whiteScore; + this.blackScore = blackScore; + this.isWhiteKingDead = isWhiteKingDead; + this.isBlackKingDead = isBlackKingDead; + } + + public double getWhiteScore() { + return whiteScore; + } + + public double getBlackScore() { + return blackScore; + } + + public boolean isWhiteKingDead() { + return isWhiteKingDead; + } + + public boolean isBlackKingDead() { + return isBlackKingDead; + } +} 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..66662cf --- /dev/null +++ b/src/main/java/chess/domain/command/Command.java @@ -0,0 +1,58 @@ +package chess.domain.command; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public enum Command { + + START("start"), + END("end"), + MOVE("move"), + STATUS("status"); + + private static final Map COMMANDS = createCommands(); + + private static Map createCommands() { + Map commands = new HashMap<>(); + + Arrays.stream(values()) + .forEach(command -> commands.put(command.command, command)); + + return commands; + } + + private final String command; + + Command(final String command) { + this.command = command; + } + + public static Command of(final String command) { + Command foundCommand = COMMANDS.get(command); + + if (foundCommand == null) { + throw new IllegalArgumentException("유효하지 않은 명령어입니다."); + } + + return foundCommand; + } + + public void validateInitialCommand() { + if (isStatus() || isMove()) { + throw new IllegalArgumentException(String.format("%s 또는 %s를 입력해주세요.", START.command, END.command)); + } + } + + public boolean isEnd() { + return this.equals(END); + } + + public boolean isMove() { + return this.equals(MOVE); + } + + public boolean isStatus() { + return this.equals(STATUS); + } +} diff --git a/src/main/java/chess/domain/command/CommandOptions.java b/src/main/java/chess/domain/command/CommandOptions.java new file mode 100644 index 0000000..a46abce --- /dev/null +++ b/src/main/java/chess/domain/command/CommandOptions.java @@ -0,0 +1,76 @@ +package chess.domain.command; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class CommandOptions { + + private static final String DELIMITER = " "; + private static final int COMMAND_INDEX = 0; + private static final int CHECK_VALUE_IF_COMMAND_HAS_OPTIONS = 1; + private static final int FIRST_OPTION_INDEX = 1; + + private final Command command; + private final List options; + + private CommandOptions(final Command command, final List options) { + this.command = command; + this.options = options; + } + + public static CommandOptions of(final String text) { + validateEmpty(text); + List commandAndOptions = split(text); + Command command = Command.of(commandAndOptions.get(COMMAND_INDEX)); + List options = extractOptions(commandAndOptions); + + return new CommandOptions(command, options); + } + + private static void validateEmpty(final String text) { + if (text == null) { + throw new IllegalArgumentException("명령어가 존재하지 않습니다."); + } + } + + private static List split(final String text) { + return Arrays.stream(text.split(DELIMITER)) + .map(String::trim) + .map(String::toLowerCase) + .collect(Collectors.toList()); + } + + private static List extractOptions(final List commandAndOptions) { + if (hasOptions(commandAndOptions)) { + return commandAndOptions.subList(FIRST_OPTION_INDEX, commandAndOptions.size()); + } + + return Collections.emptyList(); + } + + private static boolean hasOptions(final List splitText) { + return splitText.size() > CHECK_VALUE_IF_COMMAND_HAS_OPTIONS; + } + + public void validateInitialCommand() { + command.validateInitialCommand(); + } + + public boolean isEnd() { + return command.isEnd(); + } + + public boolean isMove() { + return command.isMove(); + } + + public boolean isStatus() { + return command.isStatus(); + } + + public MoveOptions getMoveOptions() { + return new MoveOptions(options); + } +} diff --git a/src/main/java/chess/domain/command/MoveOptions.java b/src/main/java/chess/domain/command/MoveOptions.java new file mode 100644 index 0000000..c06b15b --- /dev/null +++ b/src/main/java/chess/domain/command/MoveOptions.java @@ -0,0 +1,31 @@ +package chess.domain.command; + +import chess.domain.player.Position; + +import java.util.List; + +public class MoveOptions { + + private static final int SOURCE_INDEX = 0; + private static final int TARGET_INDEX = 1; + + private final Position source; + private final Position target; + + public MoveOptions(final List options) { + this(Position.of(options.get(SOURCE_INDEX)), Position.of(options.get(TARGET_INDEX))); + } + + public MoveOptions(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/piece/Bishop.java b/src/main/java/chess/domain/piece/Bishop.java new file mode 100644 index 0000000..520f8f4 --- /dev/null +++ b/src/main/java/chess/domain/piece/Bishop.java @@ -0,0 +1,8 @@ +package chess.domain.piece; + +public class Bishop extends Piece { + + public Bishop(final Color color) { + super(MovePattern.DIAGONAL_DIRECTIONS, color, true); + } +} diff --git a/src/main/java/chess/domain/piece/Color.java b/src/main/java/chess/domain/piece/Color.java new file mode 100644 index 0000000..c83450c --- /dev/null +++ b/src/main/java/chess/domain/piece/Color.java @@ -0,0 +1,10 @@ +package chess.domain.piece; + +public enum Color { + WHITE, + BLACK; + + public boolean isWhite() { + return this == WHITE; + } +} diff --git a/src/main/java/chess/domain/piece/King.java b/src/main/java/chess/domain/piece/King.java new file mode 100644 index 0000000..fe07987 --- /dev/null +++ b/src/main/java/chess/domain/piece/King.java @@ -0,0 +1,45 @@ +package chess.domain.piece; + +import chess.domain.player.Direction; +import chess.domain.player.Position; + +import java.util.Collection; + +import static java.lang.Math.abs; + +public class King extends Piece { + + private static final int MAX_THRESHOLD = 2; + + public King(final Color color) { + super(MovePattern.ALL_DIRECTIONS, color, false); + } + + @Override + public Collection findPath(final Position source, final Position target) { + int fileGap = target.calculateFileGap(source); + int rankGap = target.calculateRankGap(source); + int absoluteFileGap = abs(fileGap); + int absoluteRankGap = abs(rankGap); + + validateThreshold(absoluteFileGap, absoluteRankGap); + + Direction direction = movePattern.findDirection(fileGap, rankGap); + return findPassingPositions(source, target, direction); + } + + private void validateThreshold(final int absoluteFileGap, final int absoluteRankGap) { + if (isGreaterOrEqualThan(absoluteFileGap) || isGreaterOrEqualThan(absoluteRankGap)) { + throw new IllegalArgumentException(); + } + } + + private boolean isGreaterOrEqualThan(final int fileGap) { + return fileGap >= MAX_THRESHOLD; + } + + @Override + public boolean isKing() { + return true; + } +} diff --git a/src/main/java/chess/domain/piece/Knight.java b/src/main/java/chess/domain/piece/Knight.java new file mode 100644 index 0000000..c7f1f3e --- /dev/null +++ b/src/main/java/chess/domain/piece/Knight.java @@ -0,0 +1,8 @@ +package chess.domain.piece; + +public class Knight extends Piece { + + public Knight(final Color color) { + super(MovePattern.KNIGHT_DIRECTIONS, color, false); + } +} diff --git a/src/main/java/chess/domain/piece/MovePattern.java b/src/main/java/chess/domain/piece/MovePattern.java new file mode 100644 index 0000000..e2526bd --- /dev/null +++ b/src/main/java/chess/domain/piece/MovePattern.java @@ -0,0 +1,63 @@ +package chess.domain.piece; + +import chess.domain.player.Direction; + +import java.util.Arrays; +import java.util.List; + +import static chess.domain.player.Direction.EAST; +import static chess.domain.player.Direction.NORTH; +import static chess.domain.player.Direction.NORTH_EAST; +import static chess.domain.player.Direction.NORTH_EAST_EAST; +import static chess.domain.player.Direction.NORTH_EAST_NORTH; +import static chess.domain.player.Direction.NORTH_WEST; +import static chess.domain.player.Direction.NORTH_WEST_NORTH; +import static chess.domain.player.Direction.NORTH_WEST_WEST; +import static chess.domain.player.Direction.SOUTH; +import static chess.domain.player.Direction.SOUTH_EAST; +import static chess.domain.player.Direction.SOUTH_EAST_EAST; +import static chess.domain.player.Direction.SOUTH_EAST_SOUTH; +import static chess.domain.player.Direction.SOUTH_WEST; +import static chess.domain.player.Direction.SOUTH_WEST_SOUTH; +import static chess.domain.player.Direction.SOUTH_WEST_WEST; +import static chess.domain.player.Direction.WEST; + +public enum MovePattern { + CARDINAL_DIRECTIONS(Arrays.asList(NORTH, SOUTH, WEST, EAST)), + + DIAGONAL_DIRECTIONS(Arrays.asList(NORTH_EAST, NORTH_WEST, SOUTH_EAST, SOUTH_WEST)), + + ALL_DIRECTIONS(Arrays.asList(NORTH, SOUTH, WEST, EAST, NORTH_EAST, NORTH_WEST, SOUTH_EAST, SOUTH_WEST)), + + WHITE_PAWN_DIRECTIONS(Arrays.asList(NORTH_EAST, NORTH_WEST, NORTH)), + + BLACK_PAWN_DIRECTIONS(Arrays.asList(SOUTH_EAST, SOUTH_WEST, SOUTH)), + + KNIGHT_DIRECTIONS(Arrays.asList(NORTH_EAST_NORTH, NORTH_EAST_EAST, NORTH_WEST_WEST, NORTH_WEST_NORTH, + SOUTH_EAST_EAST, SOUTH_EAST_SOUTH, SOUTH_WEST_SOUTH, SOUTH_WEST_WEST)); + + private final List directions; + + MovePattern(final List directions) { + this.directions = directions; + } + + public static MovePattern findPawnMovePattern(final Color color) { + if (color.isWhite()) { + return WHITE_PAWN_DIRECTIONS; + } + + return BLACK_PAWN_DIRECTIONS; + } + + public Direction findDirection(int fileGap, int rankGap) { + return this.directions.stream() + .filter(direction -> direction.matches(fileGap, rankGap)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("매칭되는 방향이 없습니다.")); + } + + public List getDirections() { + return directions; + } +} diff --git a/src/main/java/chess/domain/piece/Pawn.java b/src/main/java/chess/domain/piece/Pawn.java new file mode 100644 index 0000000..73262d3 --- /dev/null +++ b/src/main/java/chess/domain/piece/Pawn.java @@ -0,0 +1,79 @@ +package chess.domain.piece; + +import chess.domain.board.Rank; +import chess.domain.player.Direction; +import chess.domain.player.Position; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.lang.Math.abs; + +public class Pawn extends Piece { + + private static final Rank WHITE_INITIAL_RANK = Rank.R2; + private static final Rank BLACK_INITIAL_RANK = Rank.R7; + private static final int MAX_RANK_THRESHOLD = 2; + + public Pawn(final Color color) { + super(MovePattern.findPawnMovePattern(color), color, false); + } + + @Override + public Collection findPath(final Position source, final Position target) { + int fileGap = target.calculateFileGap(source); + int rankGap = target.calculateRankGap(source); + boolean initialMove = isInitialMove(source); + int absoluteRankGap = abs(rankGap); + + validateInitialMoveThreshold(absoluteRankGap, initialMove); + validateFollowingMoveThreshold(absoluteRankGap, initialMove); + + Direction direction = movePattern.findDirection(fileGap, rankGap); + return findPassingPositions(source, target, direction); + } + + private boolean isInitialMove(final Position source) { + return source.hasSameRank(WHITE_INITIAL_RANK) || source.hasSameRank(BLACK_INITIAL_RANK); + } + + private void validateInitialMoveThreshold(final int absoluteRankGap, final boolean isInitialMove) { + if (isGreaterThanMaxRankThreshold(absoluteRankGap) && isInitialMove) { + throw new IllegalArgumentException("최초 이동 시 최대 2칸까지 이동할 수 있습니다."); + } + } + + private void validateFollowingMoveThreshold(final int absoluteRankGap, final boolean isInitialMove) { + if ((isGreaterOrEqualToMaxThreshold(absoluteRankGap) && !isInitialMove)) { + throw new IllegalArgumentException("최초 이동이 아닐 시 1칸만 이동할 수 있습니다."); + } + } + + private boolean isGreaterThanMaxRankThreshold(final int absoluteRankGap) { + return absoluteRankGap > MAX_RANK_THRESHOLD; + } + + private boolean isGreaterOrEqualToMaxThreshold(final int absoluteRankGap) { + return absoluteRankGap >= MAX_RANK_THRESHOLD; + } + + @Override + public Set findAvailableAttackPositions(final Position source) { + return movePattern.getDirections().stream() + .filter(Direction::isDiagonal) + .map(direction -> findAvailablePositions(source, direction, false)) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + @Override + public boolean isPawn() { + return true; + } + + @Override + public boolean isNotPawn() { + return false; + } +} diff --git a/src/main/java/chess/domain/piece/Piece.java b/src/main/java/chess/domain/piece/Piece.java new file mode 100644 index 0000000..6e33fc2 --- /dev/null +++ b/src/main/java/chess/domain/piece/Piece.java @@ -0,0 +1,98 @@ +package chess.domain.piece; + +import chess.domain.player.Direction; +import chess.domain.player.Position; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +public abstract class Piece { + + protected final MovePattern movePattern; + private final Color color; + private final boolean canMoveInfinitely; + + Piece(final MovePattern movePattern, final Color color, final boolean canMoveInfinitely) { + this.movePattern = movePattern; + this.color = color; + this.canMoveInfinitely = canMoveInfinitely; + } + + public boolean isWhite() { + return color.isWhite(); + } + + public Collection findPath(final Position source, final Position target) { + int fileGap = target.calculateFileGap(source); + int rankGap = target.calculateRankGap(source); + + Direction direction = movePattern.findDirection(fileGap, rankGap); + + return findPassingPositions(source, target, direction); + } + + protected Collection findPassingPositions(final Position source, final Position target, final Direction direction) { + Set positions = new HashSet<>(); + Position current = source; + + while (current.isDifferent(target)) { + current = current.move(direction); + positions.add(current); + } + + positions.remove(target); + return positions; + } + + public Collection findAvailableAttackPositions(final Position source) { + return movePattern.getDirections().stream() + .map(direction -> findAvailablePositions(source, direction, canMoveInfinitely)) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + protected Collection findAvailablePositions(final Position source, final Direction direction, final boolean canMoveInfinitely) { + if (canMoveInfinitely) { + return findInfinitePositions(source, direction); + } + + return findFinitePositions(source, direction); + } + + private Collection findInfinitePositions(final Position source, final Direction direction) { + Collection positions = new HashSet<>(); + Position current = source; + + while (current.isMovable(direction)) { + current = current.move(direction); + positions.add(current); + } + + return positions; + } + + private Collection findFinitePositions(final Position source, final Direction direction) { + if (source.isMovable(direction)) { + Position movedPosition = source.move(direction); + return Collections.singleton(movedPosition); + } + + return Collections.emptySet(); + } + + public boolean isKing() { + return false; + } + + public boolean isPawn() { + return false; + } + + public boolean isNotPawn() { + return true; + } +} + diff --git a/src/main/java/chess/domain/piece/PieceFactory.java b/src/main/java/chess/domain/piece/PieceFactory.java new file mode 100644 index 0000000..ec051cc --- /dev/null +++ b/src/main/java/chess/domain/piece/PieceFactory.java @@ -0,0 +1,49 @@ +package chess.domain.piece; + +import chess.domain.board.File; +import chess.domain.board.Rank; +import chess.domain.player.Position; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static chess.domain.piece.Color.BLACK; +import static chess.domain.piece.Color.WHITE; + +public class PieceFactory { + + private PieceFactory() { + } + + public static Map createPieces(final Color color) { + if (color.isWhite()) { + return initializePieces(Rank.R1, Rank.R2, WHITE); + } + + return initializePieces(Rank.R8, Rank.R7, BLACK); + } + + private static Map initializePieces(final Rank rank, final Rank pawnRank, final Color color) { + Map pieces = new HashMap<>(); + initializePiecesExceptForPawns(rank, color, pieces); + initializePawns(pawnRank, color, pieces); + return pieces; + } + + private static void initializePiecesExceptForPawns(final Rank rank, final Color color, final Map pieces) { + pieces.put(Position.from(File.A, rank), new Rook(color)); + pieces.put(Position.from(File.B, rank), new Knight(color)); + pieces.put(Position.from(File.C, rank), new Bishop(color)); + pieces.put(Position.from(File.D, rank), new Queen(color)); + pieces.put(Position.from(File.E, rank), new King(color)); + pieces.put(Position.from(File.F, rank), new Bishop(color)); + pieces.put(Position.from(File.G, rank), new Knight(color)); + pieces.put(Position.from(File.H, rank), new Rook(color)); + } + + private static void initializePawns(final Rank pawnRank, final Color color, final Map pieces) { + Arrays.stream(File.values()) + .forEach(file -> pieces.put(Position.from(file, pawnRank), new Pawn(color))); + } +} diff --git a/src/main/java/chess/domain/piece/PieceResolver.java b/src/main/java/chess/domain/piece/PieceResolver.java new file mode 100644 index 0000000..d673fc1 --- /dev/null +++ b/src/main/java/chess/domain/piece/PieceResolver.java @@ -0,0 +1,72 @@ +package chess.domain.piece; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public enum PieceResolver { + + KING(King.class, "k", 0), + QUEEN(Queen.class, "q", 9), + ROOK(Rook.class, "r", 5), + BISHOP(Bishop.class, "b", 3), + KNIGHT(Knight.class, "n", 2.5), + PAWN(Pawn.class, "p", 1); + + private static final Map, PieceResolver> PIECE_RESOLVERS = createPieceResolvers(); + private static final int DUPLICATION_THRESHOLD = 1; + private static final double PAWN_SCORE_ON_DUPLICATION = 0.5; + + private final Class piece; + private final String name; + private final double score; + + PieceResolver(final Class piece, final String name, final double score) { + this.piece = piece; + this.name = name; + this.score = score; + } + + private static Map, PieceResolver> createPieceResolvers() { + Map, PieceResolver> pieceResolvers = new HashMap<>(); + + Arrays.stream(values()) + .forEach(pieceResolver -> pieceResolvers.put(pieceResolver.piece, pieceResolver)); + + return Collections.unmodifiableMap(pieceResolvers); + } + + public static String findNameBy(final Piece piece) { + PieceResolver pieceResolver = PieceResolver.of(piece); + + if (piece.isWhite()) { + return pieceResolver.name; + } + + return pieceResolver.name.toUpperCase(); + } + + public static PieceResolver of(final Piece piece) { + PieceResolver pieceResolver = PIECE_RESOLVERS.get(piece.getClass()); + + if (pieceResolver == null) { + throw new IllegalArgumentException("기물에 대한 일치하는 리졸버가 존재하지 않습니다."); + } + + return pieceResolver; + } + + public static double findScoreBy(final Piece piece) { + PieceResolver pieceResolver = PieceResolver.of(piece); + return pieceResolver.score; + } + + public static double sumPawnScores(final int pawnCount) { + if (pawnCount > DUPLICATION_THRESHOLD) { + return pawnCount * PAWN_SCORE_ON_DUPLICATION; + } + + return PAWN.score; + } +} diff --git a/src/main/java/chess/domain/piece/Queen.java b/src/main/java/chess/domain/piece/Queen.java new file mode 100644 index 0000000..813935f --- /dev/null +++ b/src/main/java/chess/domain/piece/Queen.java @@ -0,0 +1,8 @@ +package chess.domain.piece; + +public class Queen extends Piece { + + public Queen(final Color color) { + super(MovePattern.ALL_DIRECTIONS, color, true); + } +} diff --git a/src/main/java/chess/domain/piece/Rook.java b/src/main/java/chess/domain/piece/Rook.java new file mode 100644 index 0000000..7c22d3b --- /dev/null +++ b/src/main/java/chess/domain/piece/Rook.java @@ -0,0 +1,8 @@ +package chess.domain.piece; + +public class Rook extends Piece { + + public Rook(final Color color) { + super(MovePattern.CARDINAL_DIRECTIONS, color, true); + } +} diff --git a/src/main/java/chess/domain/player/AttackRange.java b/src/main/java/chess/domain/player/AttackRange.java new file mode 100644 index 0000000..60855c5 --- /dev/null +++ b/src/main/java/chess/domain/player/AttackRange.java @@ -0,0 +1,49 @@ +package chess.domain.player; + +import chess.domain.piece.Piece; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class AttackRange { + + private static final int EMPTY = 0; + private static final int COUNT_UNIT = 1; + + private final Map counts = new HashMap<>(); + + public AttackRange(final Map pieces) { + pieces.forEach((position, piece) -> { + Collection positions = piece.findAvailableAttackPositions(position); + positions.forEach(this::increase); + }); + } + + private void increase(final Position position) { + counts.put(position, counts.getOrDefault(position, EMPTY) + COUNT_UNIT); + } + + public boolean contains(final Position position) { + return counts.containsKey(position) && (counts.get(position) > EMPTY); + } + + public void update(final Position source, final Position target, final Piece piece) { + remove(source, piece); + add(target, piece); + } + + public void remove(final Position position, final Piece piece) { + Collection previousAttackPositions = piece.findAvailableAttackPositions(position); + previousAttackPositions.forEach(this::decrease); + } + + private void decrease(final Position position) { + counts.put(position, counts.get(position) - COUNT_UNIT); + } + + private void add(final Position position, final Piece piece) { + Collection currentAttackPositions = piece.findAvailableAttackPositions(position); + currentAttackPositions.forEach(this::increase); + } +} diff --git a/src/main/java/chess/domain/player/Direction.java b/src/main/java/chess/domain/player/Direction.java new file mode 100644 index 0000000..79bf503 --- /dev/null +++ b/src/main/java/chess/domain/player/Direction.java @@ -0,0 +1,89 @@ +package chess.domain.player; + +public enum Direction { + NORTH(0, 1), + SOUTH(0, -1), + EAST(1, 0), + WEST(-1, 0), + NORTH_EAST(1, 1), + SOUTH_EAST(1, -1), + NORTH_WEST(-1, 1), + SOUTH_WEST(-1, -1), + + NORTH_EAST_NORTH(1, 2), + NORTH_EAST_EAST(2, 1), + NORTH_WEST_NORTH(-1, 2), + NORTH_WEST_WEST(-2, 1), + SOUTH_EAST_SOUTH(1, -2), + SOUTH_EAST_EAST(2, -1), + SOUTH_WEST_SOUTH(-1, -2), + SOUTH_WEST_WEST(-2, -1); + + private static final int DIVISIBLE_STANDARD = 0; + private static final int CARDINAL_STANDARD = 0; + private static final int SIGN_STANDARD = 0; + + private final int x; + private final int y; + + Direction(final int x, final int y) { + this.x = x; + this.y = y; + } + + public boolean isDiagonal() { + return this.x != CARDINAL_STANDARD && this.y != CARDINAL_STANDARD; + } + + public boolean matches(final int x, final int y) { + if (isNorthOrSouth()) { + return this.x == x && isYDivisible(y) && hasSameSign(x, y); + } + + if (isEastOrWest()) { + return this.y == y && isXDivisible(x) && hasSameSign(x, y); + } + + return isMultiple(x, y) && hasSameSign(x, y); + } + + private boolean isNorthOrSouth() { + return this.x == CARDINAL_STANDARD; + } + + private boolean isEastOrWest() { + return this.y == CARDINAL_STANDARD; + } + + private boolean isYDivisible(final int y) { + return (y % this.y) == DIVISIBLE_STANDARD; + } + + private boolean isXDivisible(final int x) { + return (x % this.x) == DIVISIBLE_STANDARD; + } + + private boolean isMultiple(final int x, final int y) { + return hasSameRatio(x, y) && isDivisible(x, y); + } + + private boolean hasSameRatio(final int x, final int y) { + return (x / this.x) == (y / this.y); + } + + private boolean isDivisible(final int x, final int y) { + return isXDivisible(x) && isYDivisible(y); + } + + private boolean hasSameSign(final int x, final int y) { + return (this.x ^ x) >= SIGN_STANDARD && (this.y ^ y) >= SIGN_STANDARD; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } +} diff --git a/src/main/java/chess/domain/player/Player.java b/src/main/java/chess/domain/player/Player.java new file mode 100644 index 0000000..8d63521 --- /dev/null +++ b/src/main/java/chess/domain/player/Player.java @@ -0,0 +1,127 @@ +package chess.domain.player; + +import chess.domain.board.File; +import chess.domain.piece.Color; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceFactory; +import chess.domain.piece.PieceResolver; + +import java.util.Collection; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class Player { + + private final Map pieces; + private final AttackRange attackRange; + + public Player(final Color color) { + pieces = new HashMap<>(PieceFactory.createPieces(color)); + attackRange = new AttackRange(pieces); + } + + public double sumScores() { + double pawnScores = calculatePawnScores(); + double scoresExceptPawn = calculateScoresExceptPawn(); + + return pawnScores + scoresExceptPawn; + } + + private double calculatePawnScores() { + List pawnPositions = findPawnPositions(); + Map pawnCountsOnSameFile = extractPawnCountsOnSameFile(pawnPositions); + + return pawnCountsOnSameFile.values() + .stream() + .mapToDouble(PieceResolver::sumPawnScores) + .sum(); + } + + private List findPawnPositions() { + return pieces.keySet() + .stream() + .filter(position -> { + Piece piece = pieces.get(position); + return piece.isPawn(); + }) + .collect(Collectors.toList()); + } + + private Map extractPawnCountsOnSameFile(final List pawnPositions) { + Map pawnCountOnSameFile = new EnumMap<>(File.class); + + pawnPositions.stream() + .map(Position::getFile) + .forEach(file -> pawnCountOnSameFile.put(file, pawnCountOnSameFile.getOrDefault(file, 0) + 1)); + + return pawnCountOnSameFile; + } + + private double calculateScoresExceptPawn() { + return pieces.values() + .stream() + .filter(Piece::isNotPawn) + .mapToDouble(PieceResolver::findScoreBy) + .sum(); + } + + public Collection findPaths(final Position source, final Position target) { + Piece sourcePiece = findPieceBy(source); + + return sourcePiece.findPath(source, target); + } + + public Piece findPieceBy(final Position position) { + if (isEmptyOn(position)) { + throw new IllegalArgumentException("해당 위치에 기물이 존재하지 않습니다."); + } + + return pieces.get(position); + } + + private boolean isEmptyOn(final Position position) { + return !pieces.containsKey(position); + } + + public void update(final Position source, final Position target) { + Piece sourcePiece = findPieceBy(source); + movePiece(source, target, sourcePiece); + attackRange.update(source, target, sourcePiece); + } + + private void movePiece(final Position source, final Position target, final Piece sourcePiece) { + pieces.put(target, sourcePiece); + pieces.remove(source); + } + + public boolean hasKingOn(final Position position) { + Piece piece = findPieceBy(position); + + return piece.isKing(); + } + + public boolean isKingDead() { + return pieces.values().stream() + .noneMatch(Piece::isKing); + } + + public boolean canAttack(final Position position) { + return attackRange.contains(position); + } + + public void removePieceOn(final Position position) { + if (isEmptyOn(position)) { + return; + } + + attackRange.remove(position, pieces.get(position)); + pieces.remove(position); + } + + public boolean hasPieceOn(final Position position) { + return pieces.containsKey(position); + } +} diff --git a/src/main/java/chess/domain/player/Position.java b/src/main/java/chess/domain/player/Position.java new file mode 100644 index 0000000..c621f51 --- /dev/null +++ b/src/main/java/chess/domain/player/Position.java @@ -0,0 +1,92 @@ +package chess.domain.player; + +import chess.domain.board.File; +import chess.domain.board.Rank; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class Position { + private static final Map POSITIONS = createPositions(); + + private static Map createPositions() { + Map positions = new HashMap<>(); + + Arrays.stream(File.values()) + .forEach(put(positions)); + + return positions; + } + + private static Consumer put(final Map positions) { + return file -> Arrays.stream(Rank.values()) + .map(rank -> new Position(file, rank)) + .forEach(position -> positions.put(createKey(position), position)); + } + + private static String createKey(final Position position) { + return createKey(position.file, position.rank); + } + + private static String createKey(final File file, final Rank rank) { + return file.name() + rank.getIndex(); + } + + public static Position of(final String key) { + Position position = POSITIONS.get(key.toUpperCase()); + + if (position == null) { + throw new IllegalArgumentException("해당 파일과 랭크에 대한 위치가 존재하지 않습니다."); + } + + return position; + } + + public static Position from(final File file, final Rank rank) { + return of(createKey(file, rank)); + } + + private final File file; + private final Rank rank; + + private Position(final File file, final Rank rank) { + this.file = file; + this.rank = rank; + } + + public int calculateFileGap(final Position position) { + return file.calculateGap(position.getFile()); + } + + public int calculateRankGap(final Position position) { + return rank.calculateGap(position.getRank()); + } + + public boolean isDifferent(final Position current) { + return !this.equals(current); + } + + public boolean isMovable(final Direction direction) { + return rank.canMove(direction.getY()) && file.canMove(direction.getX()); + } + + public Position move(final Direction direction) { + File movedFile = this.file.move(direction.getX()); + Rank movedRank = this.rank.move(direction.getY()); + return Position.from(movedFile, movedRank); + } + + public boolean hasSameRank(final Rank rank) { + return this.rank == rank; + } + + public File getFile() { + return file; + } + + public Rank getRank() { + return rank; + } +} 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..4720828 --- /dev/null +++ b/src/main/java/chess/view/ConsoleOutputView.java @@ -0,0 +1,57 @@ +package chess.view; + +import chess.controller.dto.BoardDto; +import chess.domain.board.Status; + +public class ConsoleOutputView implements OutputView { + + private static final String HEADER = "> "; + private static final String TURN_FORMAT = HEADER + "%s의 차례입니다.%n"; + private static final String WINNER_FORMAT = HEADER + "%s의 승리입니다. 축하합니다.%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 BoardDto boardDto) { + boardDto.getPositionDtos() + .forEach(positionDto -> { + System.out.print(positionDto.getName()); + if (positionDto.isLastFile()) { + System.out.println(); + } + }); + System.out.println(); + } + + @Override + public void printStatus(final Status status) { + if (status.isWhiteKingDead()) { + System.out.printf(WINNER_FORMAT, "BLACK"); + return; + } + + if (status.isBlackKingDead()) { + System.out.printf(WINNER_FORMAT, "WHITE"); + return; + } + + System.out.println(HEADER + "WHITE 점수: " + status.getWhiteScore()); + System.out.println(HEADER + "BLACK 점수: " + status.getBlackScore()); + } + + @Override + public void printTurn(boolean isWhiteTurn) { + if (isWhiteTurn) { + System.out.printf(TURN_FORMAT, "WHITE"); + return; + } + + System.out.printf(TURN_FORMAT, "BLACK"); + } +} 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..50c37bf --- /dev/null +++ b/src/main/java/chess/view/OutputView.java @@ -0,0 +1,14 @@ +package chess.view; + +import chess.controller.dto.BoardDto; +import chess.domain.board.Status; + +public interface OutputView { + void printGuide(); + + void printBoard(BoardDto boardDto); + + void printStatus(Status status); + + void printTurn(boolean isWhiteTurn); +} diff --git a/src/test/java/chess/domain/board/BoardTest.java b/src/test/java/chess/domain/board/BoardTest.java new file mode 100644 index 0000000..8a5eb44 --- /dev/null +++ b/src/test/java/chess/domain/board/BoardTest.java @@ -0,0 +1,155 @@ +package chess.domain.board; + +import chess.domain.command.MoveOptions; +import chess.domain.player.Position; +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; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class BoardTest { + + @Test + @DisplayName("객체를 생성한다.") + void create() { + //given + Position pawnPosition = Position.of("b2"); + Position emptyPosition = Position.of("b3"); + + //when + Board board = new Board(); + + //then + assertThat(board.isEmpty(pawnPosition)).isFalse(); + assertThat(board.isEmpty(emptyPosition)).isTrue(); + } + + @Test + @DisplayName("시작 위치에 기물이 존재하지 않을 경우 예외가 발생한다.") + void move_source_position_empty() { + //given + Board board = new Board(); + Position source = Position.of("b3"); + Position target = Position.of("b4"); + MoveOptions moveOptions = new MoveOptions(source, target); + + //when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> board.move(moveOptions, true)) + .withMessage("해당 위치에 기물이 존재하지 않습니다."); + } + + @ParameterizedTest + @CsvSource({"b2, b3, false", "a7, a6, true"}) + @DisplayName("자신의 기물이 아닌 기물을 선택할 경우 예외가 발생한다.") + void move_source_not_owner(String sourcePosition, String targetPosition, boolean isWhiteTurn) { + //given + Board board = new Board(); + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + MoveOptions moveOptions = new MoveOptions(source, target); + + //when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> board.move(moveOptions, isWhiteTurn)) + .withMessage("자신의 기물만 움직일 수 있습니다."); + } + + @ParameterizedTest + @CsvSource({"a1, a2, true", "a8, a7, false"}) + @DisplayName("시작과 도착 위치의 기물이 같은 색상일 경우 예외가 발생한다.") + void move_source_and_target_same_color(String sourcePosition, String targetPosition, boolean isWhiteTurn) { + //given + Board board = new Board(); + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + MoveOptions moveOptions = new MoveOptions(source, target); + + //when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> board.move(moveOptions, isWhiteTurn)) + .withMessage("같은 색상의 기물은 공격할 수 없습니다."); + } + + @ParameterizedTest + @CsvSource({"a1, a1, true", "a8, a8, false"}) + @DisplayName("시작과 도착 위치가 같을 경우 예외가 발생한다.") + void move_source_and_target_same(String sourcePosition, String targetPosition, boolean isWhiteTurn) { + //given + Board board = new Board(); + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + MoveOptions moveOptions = new MoveOptions(source, target); + + //when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> board.move(moveOptions, isWhiteTurn)) + .withMessage("출발 위치와 도착 위치가 같을 수 없습니다."); + } + + @ParameterizedTest + @CsvSource({"a1, a3, true", "a8, a6, false"}) + @DisplayName("경로에 다른 기물이 존재하는 경우 예외가 발생한다.") + void move_invalid_paths(String sourcePosition, String targetPosition, boolean isWhiteTurn) { + //given + Board board = new Board(); + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + MoveOptions moveOptions = new MoveOptions(source, target); + + //when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> board.move(moveOptions, isWhiteTurn)) + .withMessage("기물을 통과하여 이동할 수 없습니다."); + } + + @ParameterizedTest + @CsvSource({"e2, d2", "e2, e1"}) + @DisplayName("상대방이 킹의 목적지를 공격 가능한 경우 예외가 발생한다.") + void move_king_invalid_target(String source, String target) { + //given + Board board = setBoardToAttackKing(); + MoveOptions moveOptions = new MoveOptions(Position.of(source), Position.of(target)); + + //when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> board.move(moveOptions, true)) + .withMessage("킹은 상대방이 공격 가능한 위치로 이동할 수 없습니다."); + } + + @Test + @DisplayName("모든 플레이어의 점수를 반환한다.") + void get_status() { + //given + Board board = setBoardToGetStatus(); + + //when + Status status = board.getStatus(); + + //then + assertThat(status.getWhiteScore()).isEqualTo(37); + assertThat(status.getBlackScore()).isEqualTo(37); + } + + private Board setBoardToGetStatus() { + Board board = new Board(); + board.move(new MoveOptions(Position.of("e7"), Position.of("e5")), false); + board.move(new MoveOptions(Position.of("e5"), Position.of("e4")), false); + board.move(new MoveOptions(Position.of("e4"), Position.of("e3")), false); + board.move(new MoveOptions(Position.of("d2"), Position.of("e3")), true); + return board; + } + + private Board setBoardToAttackKing() { + Board board = new Board(); + board.move(new MoveOptions(Position.of("e2"), Position.of("e4")), true); + board.move(new MoveOptions(Position.of("d2"), Position.of("d4")), true); + board.move(new MoveOptions(Position.of("e1"), Position.of("e2")), true); + board.move(new MoveOptions(Position.of("c7"), Position.of("c5")), false); + board.move(new MoveOptions(Position.of("d8"), Position.of("a5")), false); + return board; + } +} diff --git a/src/test/java/chess/domain/board/FileTest.java b/src/test/java/chess/domain/board/FileTest.java new file mode 100644 index 0000000..0da1d2b --- /dev/null +++ b/src/test/java/chess/domain/board/FileTest.java @@ -0,0 +1,79 @@ +package chess.domain.board; + +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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class FileTest { + + @ParameterizedTest + @CsvSource(value = {"1, A", "8, H"}) + @DisplayName("일치하는 파일 객체를 반환한다.") + void of(int fileIndex, File expected) { + //when + File file = File.of(fileIndex); + + //then + assertThat(file).isEqualTo(expected); + } + + @ParameterizedTest + @ValueSource(ints = {0, 9}) + @DisplayName("일치하는 파일을 찾지 못할 경우 예외가 발생한다.") + void of(int fileIndex) { + //when //then + assertThatIllegalArgumentException() + .isThrownBy(() -> File.of(fileIndex)) + .withMessage("일치하는 파일이 존재하지 않습니다."); + } + + @Test + @DisplayName("다른 파일의 위치 값을 뺀 값을 반환한다.") + void calculateGap() { + //given + int index1 = 8; + int index2 = 1; + File file1 = File.of(index1); + File file2 = File.of(index2); + + //when + int result = file1.calculateGap(file2); + + //then + assertThat(result).isEqualTo(index1 - index2); + } + + @Test + @DisplayName("이동한 위치의 파일을 반환한다.") + void move() { + //given + int index1 = 1; + int index2 = 7; + File file = File.of(index1); + + //when + File movedFile = file.move(index2); + + //then + assertThat(movedFile).isEqualTo(File.of(index1 + index2)); + } + + @ParameterizedTest + @CsvSource(value = {"1, 7, true", "1, 8, false"}) + @DisplayName("이동하려는 위치가 이동 범위에 있는 지 확인한다.") + void canMove(int fileIndex, int moveAmount, boolean expected) { + //given + File file = File.of(fileIndex); + + //when + boolean actual = file.canMove(moveAmount); + + //then + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/chess/domain/board/RankTest.java b/src/test/java/chess/domain/board/RankTest.java new file mode 100644 index 0000000..012406e --- /dev/null +++ b/src/test/java/chess/domain/board/RankTest.java @@ -0,0 +1,79 @@ +package chess.domain.board; + +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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class RankTest { + + @ParameterizedTest + @CsvSource(value = {"1, R1", "8, R8"}) + @DisplayName("일치하는 랭크 객체를 반환한다.") + void of(int rankIndex, Rank expected) { + //when + Rank rank = Rank.of(rankIndex); + + //then + assertThat(rank).isEqualTo(expected); + } + + @ParameterizedTest + @ValueSource(ints = {0, 9}) + @DisplayName("일치하는 랭크를 찾지 못할 경우 예외가 발생한다.") + void of(int rankIndex) { + //when //then + assertThatIllegalArgumentException() + .isThrownBy(() -> Rank.of(rankIndex)) + .withMessage("일치하는 랭크가 존재하지 않습니다."); + } + + @Test + @DisplayName("다른 랭크의 위치 값을 뺀 값을 반환한다.") + void calculateGap() { + //given + int index1 = 8; + int index2 = 1; + Rank rank1 = Rank.of(index1); + Rank rank2 = Rank.of(index2); + + //when + int result = rank1.calculateGap(rank2); + + //then + assertThat(result).isEqualTo(index1 - index2); + } + + @Test + @DisplayName("이동한 위치의 랭크를 반환한다.") + void move() { + //given + int index1 = 1; + int index2 = 7; + Rank rank = Rank.of(index1); + + //when + Rank movedRank = rank.move(index2); + + //then + assertThat(movedRank).isEqualTo(Rank.of(index1 + index2)); + } + + @ParameterizedTest + @CsvSource(value = {"1, 7, true", "1, 8, false"}) + @DisplayName("이동하려는 위치가 이동 범위에 있는 지 확인한다.") + void canMove(int rankIndex, int moveAmount, boolean expected) { + //given + Rank rank = Rank.of(rankIndex); + + //when + boolean actual = rank.canMove(moveAmount); + + //then + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/chess/domain/command/CommandOptionsTest.java b/src/test/java/chess/domain/command/CommandOptionsTest.java new file mode 100644 index 0000000..22532de --- /dev/null +++ b/src/test/java/chess/domain/command/CommandOptionsTest.java @@ -0,0 +1,115 @@ +package chess.domain.command; + +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.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CommandOptionsTest { + + @ParameterizedTest + @CsvSource(value = {"start", "end", "move b2 b3", "status"}) + @DisplayName("옵션이 담긴 명령어를 반환한다.") + void of(String text) { + //when + CommandOptions commandOptions = CommandOptions.of(text); + + //then + assertThat(commandOptions).isNotNull(); + } + + @ParameterizedTest + @NullSource + @DisplayName("명령어가 null일 경우 예외가 발생한다.") + void of_fail(String text) { + //when //then + assertThatThrownBy(() -> CommandOptions.of(text)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("명령어가 존재하지 않습니다."); + } + + @ParameterizedTest + @CsvSource(value = {"status, move"}) + @DisplayName("시작 또는 종료 명령어가 아닐 경우 예외가 발생한다.") + void validateInitialCommand(String text) { + //given + CommandOptions commandOptions = CommandOptions.of(text); + + //when // then + assertThatThrownBy(commandOptions::validateInitialCommand) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("start 또는 end를 입력해주세요."); + } + + @ParameterizedTest + @CsvSource(value = {"start, false", "end, true", "move, false", "status, false"}) + @DisplayName("종료 명령어인지 확인한다.") + void isEnd(String text, boolean expected) { + //given + CommandOptions commandOptions = CommandOptions.of(text); + + //when + boolean actual = commandOptions.isEnd(); + + //then + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"start, false", "end, false", "move, true", "status, false"}) + @DisplayName("종료 명령어인지 확인한다.") + void isMove(String text, boolean expected) { + //given + CommandOptions commandOptions = CommandOptions.of(text); + + //when + boolean actual = commandOptions.isMove(); + + //then + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"start, false", "end, false", "move, false", "status, true"}) + @DisplayName("종료 명령어인지 확인한다.") + void isStatus(String text, boolean expected) { + //given + CommandOptions commandOptions = CommandOptions.of(text); + + //when + boolean actual = commandOptions.isStatus(); + + //then + assertThat(actual).isEqualTo(expected); + } + + @Test + @DisplayName("이동 명령 관련 옵션을 반환한다.") + void getMoveOptions() { + //given + CommandOptions commandOptions = CommandOptions.of("move b2 b3"); + + //when + MoveOptions moveOptions = commandOptions.getMoveOptions(); + + //then + assertThat(moveOptions).isNotNull(); + } + + @ParameterizedTest + @ValueSource(strings = {"start", "end", "status"}) + @DisplayName("이동 명령이 아닌 다른 명령이 이동 관련 옵션을 반환하려고 할 경우 예외가 발생한다.") + void getMoveOptions_fail(String text) { + //given + CommandOptions commandOptions = CommandOptions.of(text); + + //when //then + assertThatThrownBy(commandOptions::getMoveOptions) + .isInstanceOf(IndexOutOfBoundsException.class); + } +} diff --git a/src/test/java/chess/domain/command/CommandTest.java b/src/test/java/chess/domain/command/CommandTest.java new file mode 100644 index 0000000..657d774 --- /dev/null +++ b/src/test/java/chess/domain/command/CommandTest.java @@ -0,0 +1,78 @@ +package chess.domain.command; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CommandTest { + + @ParameterizedTest + @CsvSource(value = {"start, START", "end, END", "move, MOVE", "status, STATUS"}) + @DisplayName("명령어를 반환한다.") + void of(String text, Command expectedCommand) { + //when + Command command = Command.of(text); + + //then + assertThat(command).isEqualTo(expectedCommand); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"star", "reset", "undo", "redo", "show"}) + @DisplayName("명령어를 찾을 수 없을 경우 예외가 발생한다.") + void of_fail(String text) { + //when //then + assertThatThrownBy(() -> Command.of(text)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("유효하지 않은 명령어입니다."); + } + + @ParameterizedTest + @CsvSource(value = {"STATUS, MOVE"}) + @DisplayName("시작 또는 종료 명령어가 아닐 경우 예외가 발생한다.") + void validateInitialCommand(Command command) { + //when // then + assertThatThrownBy(command::validateInitialCommand) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("start 또는 end를 입력해주세요."); + } + + @ParameterizedTest + @CsvSource(value = {"START, false", "END, true", "MOVE, false", "STATUS, false"}) + @DisplayName("종료 명령어인지 확인한다.") + void isEnd(Command command, boolean expected) { + //when + boolean actual = command.isEnd(); + + //then + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"START, false", "END, false", "MOVE, true", "STATUS, false"}) + @DisplayName("종료 명령어인지 확인한다.") + void isMove(Command command, boolean expected) { + //when + boolean actual = command.isMove(); + + //then + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"START, false", "END, false", "MOVE, false", "STATUS, true"}) + @DisplayName("종료 명령어인지 확인한다.") + void isStatus(Command command, boolean expected) { + //when + boolean actual = command.isStatus(); + + //then + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/chess/domain/command/MoveOptionsTest.java b/src/test/java/chess/domain/command/MoveOptionsTest.java new file mode 100644 index 0000000..28163e4 --- /dev/null +++ b/src/test/java/chess/domain/command/MoveOptionsTest.java @@ -0,0 +1,45 @@ +package chess.domain.command; + +import chess.domain.board.File; +import chess.domain.board.Rank; +import chess.domain.player.Position; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class MoveOptionsTest { + + @Test + @DisplayName("시작 위치를 반환한다.") + void getSource() { + //given + List options = Arrays.asList("a1", "b2"); + MoveOptions moveOptions = new MoveOptions(options); + + //when + Position source = moveOptions.getSource(); + + //then + assertThat(source.getFile()).isEqualTo(File.A); + assertThat(source.getRank()).isEqualTo(Rank.R1); + } + + @Test + @DisplayName("도착 위치를 반환한다.") + void getTarget() { + //given + List options = Arrays.asList("a1", "b2"); + MoveOptions moveOptions = new MoveOptions(options); + + //when + Position target = moveOptions.getTarget(); + + //then + assertThat(target.getFile()).isEqualTo(File.B); + assertThat(target.getRank()).isEqualTo(Rank.R2); + } +} diff --git a/src/test/java/chess/domain/piece/BishopTest.java b/src/test/java/chess/domain/piece/BishopTest.java new file mode 100644 index 0000000..4b265ae --- /dev/null +++ b/src/test/java/chess/domain/piece/BishopTest.java @@ -0,0 +1,82 @@ +package chess.domain.piece; + +import chess.domain.board.File; +import chess.domain.board.Rank; +import chess.domain.player.Position; +import org.assertj.core.groups.Tuple; +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.Arrays; +import java.util.Collection; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.groups.Tuple.tuple; + +class BishopTest { + + @ParameterizedTest + @MethodSource("createParameters") + @DisplayName("출발과 도착 위치가 주어지면 지나가는 경로를 반환한다.") + void find_paths_success(String targetPosition, Tuple expected) { + //given + Position source = Position.of("d4"); + Position target = Position.of(targetPosition); + Piece piece = new Bishop(Color.WHITE); + + //when + Collection paths = piece.findPath(source, target); + + //then + assertThat(paths).extracting("file", "rank") + .containsOnly(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.findPath(source, target)); + } + + @Test + @DisplayName("입력받은 위치에서 공격 가능한 위치들을 반환해준다.") + void find_available_attack_positions() { + //given + Position position = Position.of("d4"); + Piece bishop = new Bishop(Color.WHITE); + Collection expected = Arrays.asList( + Position.of("a1"), Position.of("b2"), Position.of("c3"), Position.of("e5"), + Position.of("a7"), Position.of("b6"), Position.of("c5"), Position.of("e3"), + Position.of("f6"), Position.of("g7"), Position.of("h8"), + Position.of("f2"), Position.of("g1") + ); + + //when + Collection availableAttackPositions = bishop.findAvailableAttackPositions(position); + + //then + assertThat(availableAttackPositions) + .hasSize(expected.size()) + .containsAll(expected); + } + + private static Stream createParameters() { + return Stream.of( + Arguments.of("b6", tuple(File.C, Rank.R5)), + Arguments.of("b2", tuple(File.C, Rank.R3)), + Arguments.of("f2", tuple(File.E, Rank.R3)), + Arguments.of("f6", tuple(File.E, Rank.R5)) + ); + } +} diff --git a/src/test/java/chess/domain/piece/KingTest.java b/src/test/java/chess/domain/piece/KingTest.java new file mode 100644 index 0000000..87799c5 --- /dev/null +++ b/src/test/java/chess/domain/piece/KingTest.java @@ -0,0 +1,80 @@ +package chess.domain.piece; + +import chess.domain.player.Position; +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 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); + + //when + Collection paths = piece.findPath(source, target); + + //then + assertThat(paths).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = {"b2", "b3", "b4", "b5", "b6", "c6", "d6", "e6", + "f6", "f5", "f4", "f3", "f2", "c2", "d2", "e2"}) + @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.findPath(source, target)); + } + + @Test + @DisplayName("입력받은 위치에서 공격 가능한 위치들을 반환해준다.") + void find_available_attack_positions() { + //given + Position position = Position.of("d4"); + Piece king = new King(Color.WHITE); + Collection expected = Arrays.asList( + Position.of("d3"), Position.of("d5"), + Position.of("c3"), Position.of("c4"), Position.of("c5"), + Position.of("e3"), Position.of("e4"), Position.of("e5") + ); + + //when + Collection availableAttackPositions = king.findAvailableAttackPositions(position); + + //then + assertThat(availableAttackPositions) + .hasSize(expected.size()) + .containsAll(expected); + } + + @Test + @DisplayName("킹인지 확인한다.") + void is_king() { + // given + Piece king = new King(Color.WHITE); + Piece queen = new Queen(Color.WHITE); + + // when, then + assertThat(king.isKing()).isTrue(); + assertThat(queen.isKing()).isFalse(); + } +} diff --git a/src/test/java/chess/domain/piece/KnightTest.java b/src/test/java/chess/domain/piece/KnightTest.java new file mode 100644 index 0000000..e856edd --- /dev/null +++ b/src/test/java/chess/domain/piece/KnightTest.java @@ -0,0 +1,66 @@ +package chess.domain.piece; + +import chess.domain.player.Position; +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 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); + + //when + Collection paths = piece.findPath(source, target); + + //then + assertThat(paths).isEmpty(); + } + + @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.findPath(source, target)); + } + + @Test + @DisplayName("입력받은 위치에서 공격 가능한 위치들을 반환해준다.") + void find_available_attack_positions() { + //given + Position position = Position.of("d4"); + Piece knight = new Knight(Color.WHITE); + Collection expected = Arrays.asList( + Position.of("c6"), Position.of("c2"), Position.of("e6"), Position.of("e2"), + Position.of("b5"), Position.of("b3"), Position.of("f5"), Position.of("f3") + ); + + //when + Collection availableAttackPositions = knight.findAvailableAttackPositions(position); + + //then + assertThat(availableAttackPositions) + .hasSize(expected.size()) + .containsAll(expected); + } +} diff --git a/src/test/java/chess/domain/piece/MovePatternTest.java b/src/test/java/chess/domain/piece/MovePatternTest.java new file mode 100644 index 0000000..900c980 --- /dev/null +++ b/src/test/java/chess/domain/piece/MovePatternTest.java @@ -0,0 +1,48 @@ +package chess.domain.piece; + +import chess.domain.player.Direction; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Collection; + +import static chess.domain.player.Direction.NORTH; +import static chess.domain.player.Direction.NORTH_EAST; +import static chess.domain.player.Direction.NORTH_WEST; +import static chess.domain.player.Direction.SOUTH; +import static chess.domain.player.Direction.SOUTH_EAST; +import static chess.domain.player.Direction.SOUTH_WEST; +import static org.assertj.core.api.Assertions.assertThat; + +class MovePatternTest { + + @Test + @DisplayName("색상에 따라 폰 패턴을 반환한다.") + void pawn_pattern_black() { + // given + Color color = Color.BLACK; + MovePattern movePattern = MovePattern.findPawnMovePattern(color); + + // when + Collection directions = movePattern.getDirections(); + + // then + assertThat(directions) + .containsExactlyInAnyOrder(SOUTH_EAST, SOUTH_WEST, SOUTH); + } + + @Test + @DisplayName("색상에 따라 폰 패턴을 반환한다.") + void pawn_pattern_white() { + // given + Color color = Color.WHITE; + MovePattern movePattern = MovePattern.findPawnMovePattern(color); + + // when + Collection directions = movePattern.getDirections(); + + // then + assertThat(directions) + .containsExactlyInAnyOrder(NORTH_EAST, NORTH_WEST, NORTH); + } +} diff --git a/src/test/java/chess/domain/piece/PawnTest.java b/src/test/java/chess/domain/piece/PawnTest.java new file mode 100644 index 0000000..7932993 --- /dev/null +++ b/src/test/java/chess/domain/piece/PawnTest.java @@ -0,0 +1,138 @@ +package chess.domain.piece; + +import chess.domain.player.Position; +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 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); + + //when + Collection paths = piece.findPath(source, target); + + //then + assertThat(paths).isEmpty(); + } + + @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 expected) { + //given + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + Piece piece = new Pawn(color); + + //when + Collection paths = piece.findPath(source, target); + + //then + assertThat(paths).containsOnly(Position.of(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); + + //when + Collection paths = piece.findPath(source, target); + + //then + assertThat(paths).isEmpty(); + } + + @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.findPath(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.findPath(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.findPath(source, target)); + } + + @Test + @DisplayName("입력받은 위치에서 공격 가능한 위치들을 반환해준다.") + void find_available_attack_positions() { + //given + Position position = Position.of("d4"); + Piece pawn = new Pawn(Color.WHITE); + Collection expected = Arrays.asList( + Position.of("c5"), Position.of("e5") + ); + + //when + Collection availableAttackPositions = pawn.findAvailableAttackPositions(position); + + //then + assertThat(availableAttackPositions) + .hasSize(expected.size()) + .containsAll(expected); + } + + @Test + @DisplayName("폰인지 확인한다.") + void is_pawn() { + // given + Piece pawn = new Pawn(Color.WHITE); + Piece queen = new Queen(Color.WHITE); + + // when, then + assertThat(pawn.isPawn()).isTrue(); + assertThat(queen.isPawn()).isFalse(); + } +} diff --git a/src/test/java/chess/domain/piece/PieceFactoryTest.java b/src/test/java/chess/domain/piece/PieceFactoryTest.java new file mode 100644 index 0000000..0dff954 --- /dev/null +++ b/src/test/java/chess/domain/piece/PieceFactoryTest.java @@ -0,0 +1,24 @@ +package chess.domain.piece; + +import chess.domain.player.Position; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class PieceFactoryTest { + + @ParameterizedTest + @CsvSource(value = {"WHITE", "BLACK"}) + @DisplayName("색상에 맞는 기물을 생성한다.") + void createPieces(Color color) { + //when + Map pieces = PieceFactory.createPieces(color); + + //then + assertThat(pieces).hasSize(16); + } +} diff --git a/src/test/java/chess/domain/piece/PieceResolverTest.java b/src/test/java/chess/domain/piece/PieceResolverTest.java new file mode 100644 index 0000000..1513fdf --- /dev/null +++ b/src/test/java/chess/domain/piece/PieceResolverTest.java @@ -0,0 +1,66 @@ +package chess.domain.piece; + +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 PieceResolverTest { + + @Test + @DisplayName("피스에 해당하는 피스 타입을 반환한다.") + void of() { + // given + Piece piece = new Pawn(Color.WHITE); + + // when + PieceResolver pieceResolver = PieceResolver.of(piece); + + // then + assertThat(pieceResolver).isSameAs(PieceResolver.PAWN); + } + + @ParameterizedTest + @MethodSource("createParamsForName") + @DisplayName("피스가 주어지면 해당 피스와 색상에 맞는 이름을 반환한다.") + void find_name_by(Piece piece, String expected) { + //given, when + String name = PieceResolver.findNameBy(piece); + + //then + assertThat(name).isEqualTo(expected); + } + + @ParameterizedTest + @MethodSource("createParamsForScore") + @DisplayName("피스가 주어지면 해당 피스의 점수를 반환한다.") + void find_score_by(Piece piece, double expected) { + //given, when + double score = PieceResolver.findScoreBy(piece); + + //then + assertThat(score).isEqualTo(expected); + } + + 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/PieceTest.java b/src/test/java/chess/domain/piece/PieceTest.java new file mode 100644 index 0000000..e4659fe --- /dev/null +++ b/src/test/java/chess/domain/piece/PieceTest.java @@ -0,0 +1,21 @@ +package chess.domain.piece; + +import org.junit.jupiter.api.DisplayName; +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); + } +} diff --git a/src/test/java/chess/domain/piece/QueenTest.java b/src/test/java/chess/domain/piece/QueenTest.java new file mode 100644 index 0000000..2205118 --- /dev/null +++ b/src/test/java/chess/domain/piece/QueenTest.java @@ -0,0 +1,113 @@ +package chess.domain.piece; + +import chess.domain.board.File; +import chess.domain.board.Rank; +import chess.domain.player.Position; +import org.assertj.core.groups.Tuple; +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 org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.groups.Tuple.tuple; + +class QueenTest { + + @ParameterizedTest + @MethodSource("createParametersForDiagonal") + @DisplayName("출발과 도착 위치가 주어지면 지나가는 경로를 반환한다.") + void find_paths_success_diagonal(String targetPosition, Tuple expected) { + //given + Position source = Position.of("d4"); + Position target = Position.of(targetPosition); + Piece piece = new Queen(Color.WHITE); + + //when + Collection paths = piece.findPath(source, target); + + //then + assertThat(paths).extracting("file", "rank") + .containsOnly(expected); + } + + @ParameterizedTest + @MethodSource("createParametersForCardinal") + @DisplayName("출발과 도착 위치가 주어지면 지나가는 경로를 반환한다.") + void find_paths_success_cardinal(String targetPosition, Tuple expected) { + //given + Position source = Position.of("d4"); + Position target = Position.of(targetPosition); + Piece piece = new Queen(Color.WHITE); + + //when + Collection paths = piece.findPath(source, target); + + //then + //then + assertThat(paths).extracting("file", "rank") + .containsOnly(expected); + } + + @ParameterizedTest + @ValueSource(strings = {"c2", "e2", "c6", "e6", + "b3", "b5", "f3", "f5"}) + @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.findPath(source, target)); + } + + @Test + @DisplayName("입력받은 위치에서 공격 가능한 위치들을 반환해준다.") + void find_available_attack_positions() { + //given + Position position = Position.of("d4"); + Piece queen = new Queen(Color.WHITE); + Collection expected = Arrays.asList( + Position.of("a4"), Position.of("b4"), Position.of("c4"), Position.of("e4"), Position.of("f4"), Position.of("g4"), Position.of("h4"), + Position.of("d1"), Position.of("d2"), Position.of("d3"), Position.of("d5"), Position.of("d6"), Position.of("d7"), Position.of("d8"), + Position.of("a1"), Position.of("b2"), Position.of("c3"), Position.of("e5"), Position.of("f6"), Position.of("g7"), Position.of("h8"), + Position.of("a7"), Position.of("b6"), Position.of("c5"), Position.of("e3"), Position.of("f2"), Position.of("g1") + ); + + //when + Collection availableAttackPositions = queen.findAvailableAttackPositions(position); + + //then + assertThat(availableAttackPositions) + .hasSize(expected.size()) + .containsAll(expected); + } + + private static Stream createParametersForDiagonal() { + return Stream.of( + Arguments.of("b6", tuple(File.C, Rank.R5)), + Arguments.of("b2", tuple(File.C, Rank.R3)), + Arguments.of("f2", tuple(File.E, Rank.R3)), + Arguments.of("f6", tuple(File.E, Rank.R5)) + ); + } + + private static Stream createParametersForCardinal() { + return Stream.of( + Arguments.of("d6", tuple(File.D, Rank.R5)), + Arguments.of("d2", tuple(File.D, Rank.R3)), + Arguments.of("b4", tuple(File.C, Rank.R4)), + Arguments.of("f4", tuple(File.E, Rank.R4)) + ); + } +} diff --git a/src/test/java/chess/domain/piece/RookTest.java b/src/test/java/chess/domain/piece/RookTest.java new file mode 100644 index 0000000..30bda79 --- /dev/null +++ b/src/test/java/chess/domain/piece/RookTest.java @@ -0,0 +1,81 @@ +package chess.domain.piece; + +import chess.domain.board.File; +import chess.domain.board.Rank; +import chess.domain.player.Position; +import org.assertj.core.groups.Tuple; +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.Arrays; +import java.util.Collection; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.groups.Tuple.tuple; + +public class RookTest { + + @ParameterizedTest + @MethodSource("createParameters") + @DisplayName("출발과 도착 위치가 주어지면 지나가는 경로를 반환한다.") + void find_paths_success(String targetPosition, Tuple expected) { + //given + Position source = Position.of("d4"); + Position target = Position.of(targetPosition); + Piece piece = new Rook(Color.WHITE); + + //when + Collection paths = piece.findPath(source, target); + + //then + assertThat(paths).extracting("file", "rank") + .containsOnly(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.findPath(source, target)); + } + + @Test + @DisplayName("입력받은 위치에서 공격 가능한 위치들을 반환해준다.") + void find_available_attack_positions() { + //given + Position position = Position.of("d4"); + Piece rook = new Rook(Color.WHITE); + Collection expected = Arrays.asList( + Position.of("d1"), Position.of("d2"), Position.of("d3"), Position.of("d5"), + Position.of("d6"), Position.of("d7"), Position.of("d8"), + Position.of("a4"), Position.of("b4"), Position.of("c4"), Position.of("e4"), + Position.of("f4"), Position.of("g4"), Position.of("h4") + ); + + //when + Collection availableAttackPositions = rook.findAvailableAttackPositions(position); + + //then + assertThat(availableAttackPositions) + .containsAll(expected); + } + + private static Stream createParameters() { + return Stream.of( + Arguments.of("d2", tuple(File.D, Rank.R3)), + Arguments.of("d6", tuple(File.D, Rank.R5)), + Arguments.of("b4", tuple(File.C, Rank.R4)), + Arguments.of("f4", tuple(File.E, Rank.R4)) + ); + } +} diff --git a/src/test/java/chess/domain/player/AttackRangeTest.java b/src/test/java/chess/domain/player/AttackRangeTest.java new file mode 100644 index 0000000..2c9319f --- /dev/null +++ b/src/test/java/chess/domain/player/AttackRangeTest.java @@ -0,0 +1,53 @@ +package chess.domain.player; + +import chess.domain.piece.Color; +import chess.domain.piece.Knight; +import chess.domain.piece.Piece; +import chess.domain.piece.PieceFactory; +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.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class AttackRangeTest { + + @ParameterizedTest + @ValueSource(strings = {"a3", "b3", "c3", "d3", "e3", "f3", "g3", "h3"}) + @DisplayName("공격 가능한 위치들을 표시한다.") + void create(String key) { + //given + Map pieces = PieceFactory.createPieces(Color.WHITE); + AttackRange attackRange = new AttackRange(pieces); + Position position = Position.of(key); + + //when + boolean contains = attackRange.contains(position); + + //then + assertThat(contains).isTrue(); + } + + @Test + @DisplayName("공격 가능한 위치들을 갱신한다.") + void update() { + //given + Map pieces = PieceFactory.createPieces(Color.WHITE); + AttackRange attackRange = new AttackRange(pieces); + Position before = Position.of("b1"); + Position current = Position.of("c3"); + + //when + attackRange.update(before, current, new Knight(Color.WHITE)); + + //then + assertThat(attackRange.contains(Position.of("a3"))).isTrue(); + assertThat(attackRange.contains(Position.of("b1"))).isTrue(); + assertThat(attackRange.contains(Position.of("c3"))).isTrue(); + assertThat(attackRange.contains(Position.of("b5"))).isTrue(); + assertThat(attackRange.contains(Position.of("d5"))).isTrue(); + } +} diff --git a/src/test/java/chess/domain/player/DirectionTest.java b/src/test/java/chess/domain/player/DirectionTest.java new file mode 100644 index 0000000..0e3a81e --- /dev/null +++ b/src/test/java/chess/domain/player/DirectionTest.java @@ -0,0 +1,24 @@ +package chess.domain.player; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class DirectionTest { + + @ParameterizedTest + @CsvSource(value = {"0, 2, NORTH", "0, -2, SOUTH", "2, 0, EAST", "-2, 0, WEST", + "2, 2, NORTH_EAST", "2, -2, SOUTH_EAST", "-2, 2, NORTH_WEST", "-2, -2, SOUTH_WEST", + "2, 4, NORTH_EAST_NORTH", "4, 2, NORTH_EAST_EAST", "-2, 4, NORTH_WEST_NORTH", "-4, 2, NORTH_WEST_WEST", + "2, -4, SOUTH_EAST_SOUTH", "4, -2, SOUTH_EAST_EAST", "-2, -4, SOUTH_WEST_SOUTH", "-4, -2, SOUTH_WEST_WEST"}) + @DisplayName("주어진 좌표로 이동할 수 있는 방향이 존재하는지 확인한다.") + void matches(int x, int y, Direction direction) { + //when + boolean isMatched = direction.matches(x, y); + + //then + assertThat(isMatched).isTrue(); + } +} diff --git a/src/test/java/chess/domain/player/PlayerTest.java b/src/test/java/chess/domain/player/PlayerTest.java new file mode 100644 index 0000000..eaaba9c --- /dev/null +++ b/src/test/java/chess/domain/player/PlayerTest.java @@ -0,0 +1,138 @@ +package chess.domain.player; + +import chess.domain.piece.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.Collection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class PlayerTest { + + @ParameterizedTest + @CsvSource({"b2, true", "b3, false"}) + @DisplayName("피스 색상을 넣어서 플레이어 객체를 생성한다.") + void create_with_color(String key, boolean expected) { + //given + Position position = Position.of(key); + Color color = Color.WHITE; + + // when + Player player = new Player(color); + + //then + assertThat(player.hasPieceOn(position)).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({"WHITE, b3, b4", "BLACK, b6, b5"}) + @DisplayName("시작 위치에 기물이 존재하지 않을 경우 예외가 발생한다.") + void update_source_position_empty(Color color, String sourcePosition, String targetPosition) { + //given + Player player = new Player(color); + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + + //when, then + assertThatIllegalArgumentException() + .isThrownBy(() -> player.update(source, target)) + .withMessage("해당 위치에 기물이 존재하지 않습니다."); + } + + @ParameterizedTest + @CsvSource({"WHITE, b2, b3", "BLACK, d7, d6"}) + @DisplayName("기물을 움직인다.") + void update_board(Color color, String sourcePosition, String targetPosition) { + //given + Player player = new Player(color); + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + + //when + player.update(source, target); + + //then + assertThat(player.hasPieceOn(source)).isFalse(); + assertThat(player.hasPieceOn(target)).isTrue(); + } + + @ParameterizedTest + @CsvSource({"WHITE, b2, b4, b3", "BLACK, d7, d5, d6"}) + @DisplayName("이동 경로를 반환한다.") + void find_paths(Color color, String sourcePosition, String targetPosition, String expected) { + //given + Player player = new Player(color); + Position source = Position.of(sourcePosition); + Position target = Position.of(targetPosition); + Position path = Position.of(expected); + + //when + Collection paths = player.findPaths(source, target); + + //then + assertThat(paths).containsOnly(path); + } + + @ParameterizedTest + @CsvSource({"WHITE, e1, e2", "BLACK, e8, e7"}) + @DisplayName("주어진 위치에 킹이 있는지 확인한다.") + void has_king_on(Color color, String kingPosition, String notKingPosition) { + // given + Player player = new Player(color); + + // when + boolean isKing = player.hasKingOn(Position.of(kingPosition)); + boolean isNotKing = player.hasKingOn(Position.of(notKingPosition)); + + // then + assertThat(isKing).isTrue(); + assertThat(isNotKing).isFalse(); + } + + @ParameterizedTest + @CsvSource({"WHITE, b3, e6", "BLACK, e6, b3"}) + @DisplayName("주어진 위치를 공격할 수 있는지 확인한다.") + void can_attack(Color color, String attackPosition, String notAttackPosition) { + // given + Player player = new Player(color); + + // when + boolean can = player.canAttack(Position.of(attackPosition)); + boolean cannot = player.canAttack(Position.of(notAttackPosition)); + + // then + assertThat(can).isTrue(); + assertThat(cannot).isFalse(); + } + + @Test + @DisplayName("현재 남아있는 피스의 점수 합을 구한다.") + void sum_scores() { + //given + Player player = new Player(Color.WHITE); + + //when + double sum = player.sumScores(); + + //then + assertThat(sum).isEqualTo(38); + } + + @Test + @DisplayName("킹이 존재하는지 반환한다.") + void is_king_dead() { + //given + Player player = new Player(Color.WHITE); + player.removePieceOn(Position.of("e1")); + + //when + boolean kingDead = player.isKingDead(); + + //then + assertThat(kingDead).isTrue(); + } +} diff --git a/src/test/java/chess/domain/player/PositionTest.java b/src/test/java/chess/domain/player/PositionTest.java new file mode 100644 index 0000000..3cbbacc --- /dev/null +++ b/src/test/java/chess/domain/player/PositionTest.java @@ -0,0 +1,56 @@ +package chess.domain.player; + +import chess.domain.board.File; +import chess.domain.board.Rank; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +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 from_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); + } + + @Test + @DisplayName("랭크가 동일한지 확인한다.") + void has_same_rank() { + // given + Rank rank = Rank.R1; + Position position = Position.of("a1"); + + // when + boolean hasSameRank = position.hasSameRank(rank); + + // then + assertThat(hasSameRank).isTrue(); + } +} diff --git a/src/test/java/empty.txt b/src/test/java/empty.txt deleted file mode 100644 index e69de29..0000000