diff --git a/README.md b/README.md new file mode 100644 index 0000000000..2917028f93 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# 체스 1단계 - 체스구현하기 + +## **기능 요구사항** + +- 기능 요구사항 +- 체스판을 초기화 할 수 있다. +- 말 이동을 할 수 있다. +- 승패 및 점수를 판단할 수 있다. +- 웹으로 체스 게임이 가능해야 한다.(스파크 적용) +- 웹 서버를 재시작하더라도 이전에 하던 체스 게임을 다시 시작할 수 있어야 한다.(DB 적용) \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5ba642c94c..a234ac906d 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,14 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.1' + implementation 'com.google.code.gson:gson:2.8.6' + implementation 'com.google.code.gson:gson:2.8.6' + compile('com.sparkjava:spark-core:2.9.0') + compile('com.sparkjava:spark-template-handlebars:2.7.1') + compile('ch.qos.logback:logback-classic:1.2.3') + compile("mysql:mysql-connector-java:8.0.16") + testCompile('org.junit.jupiter:junit-jupiter:5.6.0') + testCompile('org.assertj:assertj-core:3.15.0') testImplementation 'io.rest-assured:rest-assured:3.3.0' testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' diff --git a/src/main/java/wooteco/chess/SparkChessApplication.java b/src/main/java/wooteco/chess/SparkChessApplication.java index 86a8e29510..0ab1145e8e 100644 --- a/src/main/java/wooteco/chess/SparkChessApplication.java +++ b/src/main/java/wooteco/chess/SparkChessApplication.java @@ -1,22 +1,15 @@ package wooteco.chess; -import spark.ModelAndView; -import spark.template.handlebars.HandlebarsTemplateEngine; +import wooteco.chess.controller.SparkChessController; -import java.util.HashMap; -import java.util.Map; - -import static spark.Spark.get; +import static spark.Spark.staticFiles; public class SparkChessApplication { public static void main(String[] args) { - get("/", (req, res) -> { - Map model = new HashMap<>(); - return render(model, "index.hbs"); - }); - } + staticFiles.location("/templates"); + + SparkChessController sparkChessController = new SparkChessController(); - private static String render(Map model, String templatePath) { - return new HandlebarsTemplateEngine().render(new ModelAndView(model, templatePath)); + sparkChessController.route(); } } diff --git a/src/main/java/wooteco/chess/SpringChessApplication.java b/src/main/java/wooteco/chess/SpringChessApplication.java index b23c24d0bd..5e3f2c807b 100644 --- a/src/main/java/wooteco/chess/SpringChessApplication.java +++ b/src/main/java/wooteco/chess/SpringChessApplication.java @@ -6,8 +6,8 @@ @SpringBootApplication public class SpringChessApplication { - public static void main(String[] args) { - SpringApplication.run(SpringChessApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(SpringChessApplication.class, args); + } } diff --git a/src/main/java/wooteco/chess/controller/SparkChessController.java b/src/main/java/wooteco/chess/controller/SparkChessController.java new file mode 100644 index 0000000000..5097833b1d --- /dev/null +++ b/src/main/java/wooteco/chess/controller/SparkChessController.java @@ -0,0 +1,122 @@ +package wooteco.chess.controller; + +import spark.ModelAndView; +import spark.Request; +import spark.Response; +import spark.template.handlebars.HandlebarsTemplateEngine; +import wooteco.chess.domain.game.NormalStatus; +import wooteco.chess.domain.piece.Color; +import wooteco.chess.domain.position.MovingPosition; +import wooteco.chess.dto.DestinationPositionDto; +import wooteco.chess.dto.MovablePositionsDto; +import wooteco.chess.dto.MoveStatusDto; +import wooteco.chess.service.ChessWebService; + +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +import static spark.Spark.get; +import static spark.Spark.post; +import static wooteco.chess.web.JsonTransformer.json; + +public class SparkChessController { + private ChessWebService chessWebService; + + public SparkChessController() { + this.chessWebService = new ChessWebService(); + } + + public void route() { + get("/", this::index); + + get("/new", this::startNewGame); + + get("/loading", this::loadGame); + + get("/board", (req, res) -> chessWebService.setBoard(), json()); + + post("/board", this::postBoard); + + post("/source", this::getMovablePositions, json()); + + post("/destination", this::move, json()); + } + + private String index(Request req, Response res) { + Map model = new HashMap<>(); + model.put("normalStatus", NormalStatus.YES.isNormalStatus()); + + return render(model, "index.html"); + } + + private String startNewGame(Request req, Response res) throws SQLException { + Map model = new HashMap<>(); + model.put("normalStatus", NormalStatus.YES.isNormalStatus()); + + chessWebService.clearHistory(); + + return render(model, "chess.html"); + } + + private String loadGame(Request req, Response res) { + Map model = new HashMap<>(); + model.put("normalStatus", NormalStatus.YES.isNormalStatus()); + + return render(model, "chess.html"); + } + + private String postBoard(Request req, Response res) { + Map model = new HashMap<>(); + + try { + MoveStatusDto moveStatusDto = chessWebService.move(new MovingPosition(req.queryParams("source"), req.queryParams("destination"))); + + model.put("normalStatus", moveStatusDto.getNormalStatus()); + model.put("winner", moveStatusDto.getWinner()); + + if (moveStatusDto.getWinner().isNone()) { + return render(model, "chess.html"); + } + return render(model, "result.html"); + } catch (IllegalArgumentException | UnsupportedOperationException | NullPointerException | SQLException e) { + model.put("normalStatus", NormalStatus.NO.isNormalStatus()); + model.put("exception", e.getMessage()); + model.put("winner", Color.NONE); + return render(model, "chess.html"); + } + } + + private Map getMovablePositions(Request req, Response res) throws SQLException { + Map model = new HashMap<>(); + try { + MovablePositionsDto movablePositionsDto = chessWebService.findMovablePositions(req.queryParams("source")); + + model.put("movable", movablePositionsDto.getMovablePositionNames()); + model.put("position", movablePositionsDto.getPosition()); + model.put("normalStatus", NormalStatus.YES.isNormalStatus()); + + return model; + } catch (IllegalArgumentException | UnsupportedOperationException | NullPointerException e) { + model.put("normalStatus", NormalStatus.NO.isNormalStatus()); + model.put("exception", e.getMessage()); + + return model; + } + } + + private Map move(Request req, Response res) { + Map model = new HashMap<>(); + + DestinationPositionDto destinationPositionDto = chessWebService.chooseDestinationPosition(req.queryParams("destination")); + + model.put("normalStatus", destinationPositionDto.getNormalStatus().isNormalStatus()); + model.put("position", destinationPositionDto.getPosition()); + + return model; + } + + private static String render(Map model, String templatePath) { + return new HandlebarsTemplateEngine().render(new ModelAndView(model, templatePath)); + } +} diff --git a/src/main/java/wooteco/chess/controller/ChessController.java b/src/main/java/wooteco/chess/controller/SpringChessController.java similarity index 86% rename from src/main/java/wooteco/chess/controller/ChessController.java rename to src/main/java/wooteco/chess/controller/SpringChessController.java index 1a18c64438..ebf346323f 100644 --- a/src/main/java/wooteco/chess/controller/ChessController.java +++ b/src/main/java/wooteco/chess/controller/SpringChessController.java @@ -4,7 +4,7 @@ import org.springframework.web.bind.annotation.GetMapping; @Controller -public class ChessController { +public class SpringChessController { @GetMapping("/") public String index() { return "index"; diff --git a/src/main/java/wooteco/chess/dao/FakeHistoryDao.java b/src/main/java/wooteco/chess/dao/FakeHistoryDao.java new file mode 100644 index 0000000000..7650ffe939 --- /dev/null +++ b/src/main/java/wooteco/chess/dao/FakeHistoryDao.java @@ -0,0 +1,35 @@ +package wooteco.chess.dao; + +import wooteco.chess.domain.position.MovingPosition; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class FakeHistoryDao { + private final Map fakeHistoryDao; + + public FakeHistoryDao() { + fakeHistoryDao = new LinkedHashMap<>(); + + fakeHistoryDao.put(1, new MovingPosition("a2", "a4")); + fakeHistoryDao.put(2, new MovingPosition("a7", "a6")); + fakeHistoryDao.put(3, new MovingPosition("a4", "a5")); + fakeHistoryDao.put(4, new MovingPosition("b7", "b5")); + } + + public Map selectAll() { + return fakeHistoryDao; + } + + public void clear() { + fakeHistoryDao.clear(); + } + + public void insert(String start, String end) { + insert(new MovingPosition(start, end)); + } + + public void insert(MovingPosition movingPosition) { + fakeHistoryDao.put(fakeHistoryDao.size() + 1, movingPosition); + } +} diff --git a/src/main/java/wooteco/chess/dao/HistoryDao.java b/src/main/java/wooteco/chess/dao/HistoryDao.java new file mode 100644 index 0000000000..23cca0102b --- /dev/null +++ b/src/main/java/wooteco/chess/dao/HistoryDao.java @@ -0,0 +1,43 @@ +package wooteco.chess.dao; + +import wooteco.chess.domain.position.MovingPosition; +import wooteco.chess.web.ConnectionManager; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class HistoryDao { + private final ConnectionManager connectionManager = new ConnectionManager(); + + public void insert(MovingPosition movingPosition) throws SQLException { + String query = "INSERT INTO history (start, end) VALUES (?, ?)"; + try (PreparedStatement pstmt = connectionManager.getConnection().prepareStatement(query)) { + pstmt.setString(1, movingPosition.getStart()); + pstmt.setString(2, movingPosition.getEnd()); + pstmt.executeUpdate(); + } + } + + public List selectAll() throws SQLException { + String query = "SELECT * FROM history"; + try (PreparedStatement pstmt = connectionManager.getConnection().prepareStatement(query); ResultSet rs = pstmt.executeQuery()) { + + List result = new ArrayList<>(); + while (rs.next()) { + result.add(new MovingPosition(rs.getString("start"), rs.getString("end"))); + } + return Collections.unmodifiableList(result); + } + } + + public void clear() throws SQLException { + String query = "DELETE FROM history"; + try (PreparedStatement pstmt = connectionManager.getConnection().prepareStatement(query)) { + pstmt.executeUpdate(); + } + } +} \ No newline at end of file diff --git a/src/main/java/wooteco/chess/domain/game/ChessGame.java b/src/main/java/wooteco/chess/domain/game/ChessGame.java new file mode 100644 index 0000000000..447e4987c5 --- /dev/null +++ b/src/main/java/wooteco/chess/domain/game/ChessGame.java @@ -0,0 +1,61 @@ +package wooteco.chess.domain.game; + +import wooteco.chess.domain.piece.Color; +import wooteco.chess.domain.piece.Piece; +import wooteco.chess.domain.piece.pieces.Pieces; +import wooteco.chess.domain.piece.pieces.PiecesInitializer; +import wooteco.chess.domain.position.MovingPosition; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.PositionFactory; +import wooteco.chess.domain.position.positions.Positions; + +import java.util.List; +import java.util.stream.Collectors; + +public class ChessGame { + private Pieces pieces; + private Turn turn; + + public ChessGame() { + pieces = PiecesInitializer.operate(); + turn = new Turn(Color.WHITE); + } + + public void move(MovingPosition movingPosition) { + pieces.move(movingPosition.getStartPosition(), movingPosition.getEndPosition(), turn.getColor()); + turn = turn.change(); + } + + public ScoreResult calculateScore() { + return new ScoreResult(pieces); + } + + public boolean isKingDead() { + return pieces.isKingDead(); + } + + public Color getAliveKingColor() { + return pieces.getAliveKingColor(); + } + + public Positions findMovablePositions(Position position) { + Piece piece = pieces.findBy(position, turn.getColor()); + return piece.createMovablePositions(pieces.getPieces()); + } + + public List findMovablePositionNames(String position) { + return this.findMovablePositions(PositionFactory.of(position)) + .getPositions() + .stream() + .map(Position::toString) + .collect(Collectors.toList()); + } + + public Turn getTurn() { + return turn; + } + + public Pieces getPieces() { + return pieces; + } +} diff --git a/src/main/java/wooteco/chess/domain/game/NormalStatus.java b/src/main/java/wooteco/chess/domain/game/NormalStatus.java new file mode 100644 index 0000000000..ba51928fc7 --- /dev/null +++ b/src/main/java/wooteco/chess/domain/game/NormalStatus.java @@ -0,0 +1,16 @@ +package wooteco.chess.domain.game; + +public enum NormalStatus { + YES(true), + NO(false); + + private boolean normalStatus; + + NormalStatus(boolean normalStatus) { + this.normalStatus = normalStatus; + } + + public boolean isNormalStatus() { + return normalStatus; + } +} diff --git a/src/main/java/wooteco/chess/domain/game/ScoreResult.java b/src/main/java/wooteco/chess/domain/game/ScoreResult.java new file mode 100644 index 0000000000..ec35d57525 --- /dev/null +++ b/src/main/java/wooteco/chess/domain/game/ScoreResult.java @@ -0,0 +1,77 @@ +package wooteco.chess.domain.game; + +import wooteco.chess.domain.piece.Color; +import wooteco.chess.domain.piece.Piece; +import wooteco.chess.domain.piece.pieces.Pieces; +import wooteco.chess.domain.position.Row; + +import java.util.*; +import java.util.stream.Collectors; + +public class ScoreResult { + private static final double PAWN_SPECIAL_SCORE = 0.5; + private static final int DEFAULT_COUNT = 0; + private static final int PAWN_SPECIAL_SCORE_COUNT_BOUNDARY = 1; + + private final Map scores; + + public ScoreResult(Pieces pieces) { + scores = new EnumMap<>(Color.class); + calculateScore(pieces.getPieces()); + } + + private void calculateScore(List pieces) { + scores.put(Color.WHITE, calculateScoreBy(Color.WHITE, pieces)); + scores.put(Color.BLACK, calculateScoreBy(Color.BLACK, pieces)); + } + + private double calculateScoreBy(Color color, List pieces) { + double scoreByColor = calculateTotalScoreBy(color, pieces); + List pawns = getPawnsBy(color, pieces); + Map pawnCountByRows = new EnumMap<>(Row.class); + + for (Row row : Row.values()) { + pawnCountByRows.put(row, getPawnCountBy(row, pawns)); + } + + return scoreByColor - (PAWN_SPECIAL_SCORE * getTotalDuplicatedPawns(pawnCountByRows)); + } + + private Integer getTotalDuplicatedPawns(Map pawnCountByRows) { + return pawnCountByRows.values() + .stream() + .filter(x -> x > PAWN_SPECIAL_SCORE_COUNT_BOUNDARY) + .reduce(DEFAULT_COUNT, Integer::sum); + } + + private int getPawnCountBy(Row row, List pawns) { + return (int) pawns.stream() + .filter(pawn -> row.isSame(pawn.getPosition().getRow())) + .count(); + } + + private List getPawnsBy(Color color, List pieces) { + return pieces.stream() + .filter(piece -> piece.isSameColor(color)) + .filter(Piece::isPawn) + .collect(Collectors.toList()); + } + + private double calculateTotalScoreBy(Color color, List pieces) { + return pieces.stream() + .filter(piece -> piece.isSameColor(color)) + .mapToDouble(Piece::getScore) + .sum(); + } + + private void validate(Color color) { + if (Objects.isNull(color) || color.isNone()) { + throw new IllegalArgumentException("잘못된 입력입니다."); + } + } + + public double getScoreBy(Color color) { + validate(color); + return scores.get(color); + } +} diff --git a/src/main/java/wooteco/chess/domain/game/Turn.java b/src/main/java/wooteco/chess/domain/game/Turn.java new file mode 100644 index 0000000000..1c208bf31e --- /dev/null +++ b/src/main/java/wooteco/chess/domain/game/Turn.java @@ -0,0 +1,46 @@ +package wooteco.chess.domain.game; + +import wooteco.chess.domain.piece.Color; + +import java.util.Arrays; +import java.util.List; + +public class Turn { + private static final List turns; + private final Color color; + + static { + turns = Arrays.asList( + new Turn(Color.WHITE), + new Turn(Color.BLACK) + ); + } + + public Turn(Color color) { + validate(color); + this.color = color; + } + + private void validate(Color color) { + if (color.isNone()) { + throw new IllegalArgumentException("턴은 BLACK이나 WHITE로만 시작할 수 있습니다."); + } + } + + public Turn change() { + if (color.isWhite()) { + return turns.stream() + .filter(turn -> turn.getColor().isBlack()) + .findFirst() + .orElseThrow(UnsupportedOperationException::new); + } + return turns.stream() + .filter(turn -> turn.getColor().isWhite()) + .findFirst() + .orElseThrow(UnsupportedOperationException::new); + } + + public Color getColor() { + return color; + } +} diff --git a/src/main/java/wooteco/chess/domain/piece/Blank.java b/src/main/java/wooteco/chess/domain/piece/Blank.java new file mode 100644 index 0000000000..a2bc712898 --- /dev/null +++ b/src/main/java/wooteco/chess/domain/piece/Blank.java @@ -0,0 +1,11 @@ +package wooteco.chess.domain.piece; + +import wooteco.chess.domain.position.PositionFactory; + +public class Blank extends Piece { + public static final String BLANK_DEFAULT_POSITION = "a1"; + + public Blank() { + super(PositionFactory.of(BLANK_DEFAULT_POSITION), PieceType.BLANK, Color.NONE); + } +} diff --git a/src/main/java/wooteco/chess/domain/piece/Color.java b/src/main/java/wooteco/chess/domain/piece/Color.java new file mode 100644 index 0000000000..517bb888a1 --- /dev/null +++ b/src/main/java/wooteco/chess/domain/piece/Color.java @@ -0,0 +1,23 @@ +package wooteco.chess.domain.piece; + +public enum Color { + WHITE, + BLACK, + NONE; + + public boolean isWhite() { + return WHITE.equals(this); + } + + public boolean isBlack() { + return BLACK.equals(this); + } + + public boolean isNone() { + return NONE.equals(this); + } + + public boolean isSame(Color color) { + return this.equals(color); + } +} diff --git a/src/main/java/wooteco/chess/domain/piece/Piece.java b/src/main/java/wooteco/chess/domain/piece/Piece.java new file mode 100644 index 0000000000..67bb755ae0 --- /dev/null +++ b/src/main/java/wooteco/chess/domain/piece/Piece.java @@ -0,0 +1,74 @@ +package wooteco.chess.domain.piece; + +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.positions.Positions; + +import java.util.List; +import java.util.Objects; + +public class Piece { + private static final String INVALID_INPUT_EXCEPTION_MESSAGE = "말을 생성할 수 없습니다."; + + private Position position; + private final PieceType pieceType; + private final Color color; + + public Piece(Position position, PieceType pieceType, Color color) { + this.position = position; + this.pieceType = pieceType; + this.color = color; + } + + public Positions createMovablePositions(List pieces) { + return pieceType.findMovablePositions(position, pieces, color); + } + + public void move(Position position) { + if (isSamePosition(position)) { + throw new UnsupportedOperationException("같은 위치로 이동할 수 없습니다."); + } + this.position = position; + } + + public boolean isSamePosition(Position position) { + Objects.requireNonNull(position, INVALID_INPUT_EXCEPTION_MESSAGE); + return this.position.equals(position); + } + + public boolean isSameColor(Color color) { + Objects.requireNonNull(color, INVALID_INPUT_EXCEPTION_MESSAGE); + return this.color.isSame(color); + } + + public boolean isNotSameColor(Color color) { + return !isSameColor(color); + } + + public boolean isWhite() { + return color.isWhite(); + } + + public boolean isKing() { + return pieceType.isKing(); + } + + public boolean isPawn() { + return pieceType.isPawn(); + } + + public Position getPosition() { + return position; + } + + public Color getColor() { + return color; + } + + public double getScore() { + return pieceType.getScore(); + } + + public PieceType getPieceType() { + return pieceType; + } +} diff --git a/src/main/java/wooteco/chess/domain/piece/PieceType.java b/src/main/java/wooteco/chess/domain/piece/PieceType.java new file mode 100644 index 0000000000..a3fd8aa47d --- /dev/null +++ b/src/main/java/wooteco/chess/domain/piece/PieceType.java @@ -0,0 +1,42 @@ +package wooteco.chess.domain.piece; + +import wooteco.chess.domain.piece.movable.*; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.positions.Positions; + +import java.util.List; + +public enum PieceType { + KING(0.0, new UnblockedMovable(MovableDirections.EVERY)), + QUEEN(9.0, new BlockedMovable(MovableDirections.EVERY)), + KNIGHT(2.5, new UnblockedMovable(MovableDirections.KNIGHT)), + ROOK(5.0, new BlockedMovable(MovableDirections.LINEAR)), + BISHOP(3.0, new BlockedMovable(MovableDirections.DIAGONAL)), + WHITE_PAWN(1.0, new PawnMovable(MovableDirections.WHITE_PAWN)), + BLACK_PAWN(1.0, new PawnMovable(MovableDirections.BLACK_PAWN)), + BLANK(0.0, new UnblockedMovable(MovableDirections.NONE)); + + private final double score; + private final Movable movable; + + PieceType(double score, Movable movable) { + this.score = score; + this.movable = movable; + } + + public Positions findMovablePositions(Position position, List pieces, Color color) { + return movable.findMovablePositions(position, pieces, color); + } + + public double getScore() { + return score; + } + + public boolean isKing() { + return this.equals(KING); + } + + public boolean isPawn() { + return this.equals(WHITE_PAWN) || this.equals(BLACK_PAWN); + } +} diff --git a/src/main/java/wooteco/chess/domain/piece/movable/BlockedMovable.java b/src/main/java/wooteco/chess/domain/piece/movable/BlockedMovable.java new file mode 100644 index 0000000000..cddd86c513 --- /dev/null +++ b/src/main/java/wooteco/chess/domain/piece/movable/BlockedMovable.java @@ -0,0 +1,146 @@ +package wooteco.chess.domain.piece.movable; + +import wooteco.chess.domain.piece.Color; +import wooteco.chess.domain.piece.Piece; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.positions.Positions; + +import java.util.List; + +/** + * BlockedMovable은 정해진 방향으로 더 이상 진행할 수 없을 때까지 이동하는, + * Queen,Bishop,Rook의 움직이는 방법을 구현한 클래스이다. + * BlockedMovable이란 이름은 막히는(즉 범위를 벗어나거나 다른 말이 있어 진행할 수 없는) 형태 움직임을 가졌다는 의미를 가진다. + */ +public class BlockedMovable implements Movable { + /** + * moveDirections는 Pawn이 움직일 수 있는 방향들을 저장한다. + */ + private final MovableDirections movableDirections; + + /** + * Constructor는 방향을 주입받아 이를 저장한다. + * + * @param movableDirections 움직일 수 있는 방향의 목록이다.일반적으로 전진 및 대각선 전진(왼쪽방향, 오른쪽 방향)의 세 가지 방향을 포함한다. + */ + public BlockedMovable(MovableDirections movableDirections) { + this.movableDirections = movableDirections; + } + + /** + * findMovablePositions는 이 인터페이스를 포함하는 상위 클래스(즉, Queen이나 Rook,Bishop을 구현한 클래스)에서 호출된다. + * 현재 말의 위치, 모든 말들의 목록, 이 말의 색상(색상이 같으면 같은 팀이다) 을 전달받는다. + * 이후 말이 움직일 수 있는 방향들을 확인하여, 이를 순회한다. + * 순서는 아래와 같다. + * - 각 방향에 대해 먼저 순회한다. + * - 현재 위치에 갈 수 있는 방향을 더하여 새로운 위치를 만든다. + * - 생성한 위치에 대해, 갈 수 있는지 여부를 검사한다. + * - 만약 갈 수 없다면, 루프를 종료하고 다음 방향을 찾는다. + * - 갈 수 있다면, 그 위치를 목록에 추가하고 다음 위치를 생성한다. + * - 종합적으로 모인, 한 방향으로 갈 수 있는 모든 위치의 목록을 반환하여 기존 목록과 합친다. + * - 최종적으로 만들어진 목록을 Positions로 반환한다. + * + * @param position 말의 현재 위치값이다. 이 위치에 필드변수 moveDiretions를 더하여 말이 움직일 수 있는 위치들을 구한다. + * @param pieces 모든 말들을 저장한 리스트이다. 이를 통해 내가 움직이려는 자리에 아군 말이 있는지 확인한다. + * @param color 말의 색상값이다. 다른 말과 위치가 겹칠 때, 색이 다른지(적이라는 뜻이므로, 말을 잡을 수 있다) + * 혹은 같은지(아군이 있는 곳으로는 이동할 수 없다) 여부를 판단한다. + * @return 최종적으로 말이 position에서 출발해 갈 수 있는 모든 위치를 Positions 객체에 저장해 반환한다. + */ + @Override + public Positions findMovablePositions(Position position, List pieces, Color color) { + Positions movablePositions = Positions.create(); + for (Direction direction : movableDirections.getDirections()) { + Positions positions = createMovablePositionsByDirection(position, direction, pieces, color); + movablePositions.addAll(positions); + } + return movablePositions; + } + + /** + * createMovablePositionsByDirection은, 방향값과 위치를 받은 뒤 그 방향으로 더 이상 진행이 불가할 때까지 탐색하는 메서드이다. + * 탐색 과정에서 모든 말의 목록인 pieces와 내 말의 색상인 color를 참조한다. + * 순서는 다음과 같다. + * - 다른 말이 경로를 막거나, 판의 범위를 벗어나는 등 이동이 불가할 때까지 순회를 진행한다. + * - 위치를 방향값을 바탕으로 한 칸 이동시킨다. + * - 이동한 방향값을 목록에 추가한다. + * - 순회하면서 구한 값들의 목록을 Positions로 만들어 반환한다. + * + * @param position 내 말의 초기 위치이다. 이 값이 바뀌면 상위 메서드에서도 바뀔 수 있으므로, 별도의 movablePosition을 지역변수로 만든다. + * @param direction 내 말이 이동할 방향값이다. 이를 바탕으로 movablePosition을 한 칸씩 이동시킨다. + * @param pieces 모든 말의 목록이다. 이를 통해 다른 말이 경로를 막는지 확인할 수 있다. + * @param color 내 말의 색상(팀) 이다. + * 이를 통해 특정 칸을 적 말이 막을 경우(적 말을 잡을 수 있으므로 그 칸까지는 전진 가능)와 + * 아군 말이 막는 경우(그 칸부터 바로 전진 불가, 순회 종료)를 구분할 수 있다. + * @return 입력된 방향으로 전진하여 갈 수 있는 모든 Position을 Positions로 묶어 반환한다. + */ + private Positions createMovablePositionsByDirection(Position position, Direction direction, List pieces, Color color) { + Positions movablePositions = Positions.create(); + Position movablePosition = position; + while (isOpen(movablePosition, direction, pieces, color)) { + movablePosition = movablePosition.getMovedPositionBy(direction); + movablePositions.add(movablePosition); + } + return movablePositions; + } + + /** + * isOpen은 방향값과 위치값을 바탕으로, 한 칸 더 이동할 수 있는지 여부를 검사해주는 메서드이다. + * 만약 아래 조건에 해당되면, 다음 칸으로 갈 수 없다고 판단한다. + * - 다음 칸이 범위 바깥일 경우 + * - 현재 칸에 적 말이 있는 경우 + * - 다음 칸에 아군 말이 있는 경우 + * 조건 중 "현재 칸에 적 말이 있는 경우"의 의미는 아래와 같다. + * - 만약 Rook이 전진한다고 가정해 보자. + * 적 말이 경로를 막고 있다면, 그 칸까지는 갈 수 있다.(적 말을 잡을 수 있다.) + * 그러나, 그 다음 칸부터는 갈 수 없다. + * 이를 다르게 말하면, 바로 이전 칸에서 적 말을 만났다면, 그때부터는 전진할 수 없다는 뜻이다. + * 그렇기 때문에, 아직 움직이지 않은 position값을 바탕으로 검사하는 것이다. + * (세 번째 조건 검사시에는 position.getMovedPositionBy를 사용하여, 한 칸 이동했을 때 아군이 있는지 검사한다) + * + * @param position 말의 현재 위치값이다. + * @param direction 말이 이동할 수 있는지 검사할 방향값이다. + * @param pieces 모든 말의 목록이다. 이를 바탕으로 검사를 수행한다. + * @param color 말의 색상(팀) 값이다. 이를 통해 아군/적군 말을 구분한다. + * @return 만약 다음 칸으로 진행할 수 있다면 true를, 아니라면 false를 반환한다. + */ + private boolean isOpen(Position position, Direction direction, List pieces, Color color) { + if (!position.checkBound(direction)) { + return false; + } + if (isPossessedByDifferentColor(position, pieces, color)) { + return false; + } + return !isPossessedBySameColor(position.getMovedPositionBy(direction), pieces, color); + } + + /** + * isPossessedByDifferentColor는 위치값을 전달받아 그 위치에 적이 있는지 검사한다. + * 만약 그 칸에 적이 있다면, true를 반환한다. + * 그렇지 않다면 false를 반환한다. + * + * @param position 말의 현재 위치값이다. + * @param pieces 모든 말의 목록이다. 이 값을 바탕으로 탐색을 진행한다. + * @param color 말의 색상(팀) 값이다. 이를 통해 아군/적군을 구분한다. + * @return 만약 주어진 위치에 적이 있다면 true를, 아니라면 false를 반환한다. + */ + private boolean isPossessedByDifferentColor(Position position, List pieces, Color color) { + return pieces.stream() + .anyMatch(piece -> piece.isSamePosition(position) && piece.isNotSameColor(color)); + } + + + /** + * isPossessedBySameColor는 위치값을 전달받아 그 위치에 아군이 있는지 검사한다. + * 만약 그 칸에 아군이 있다면, true를 반환한다. + * 그렇지 않다면 false를 반환한다. + * + * @param position 말의 현재 위치값이다. + * @param pieces 모든 말의 목록이다. 이 값을 바탕으로 탐색을 진행한다. + * @param color 말의 색상(팀) 값이다. 이를 통해 아군/적군을 구분한다. + * @return 만약 주어진 위치에 아군이 있다면 true를, 아니라면 false를 반환한다. + */ + private boolean isPossessedBySameColor(Position position, List pieces, Color color) { + return pieces.stream() + .anyMatch(piece -> piece.isSamePosition(position) && piece.isSameColor(color)); + } +} \ No newline at end of file diff --git a/src/main/java/wooteco/chess/domain/piece/movable/Direction.java b/src/main/java/wooteco/chess/domain/piece/movable/Direction.java new file mode 100644 index 0000000000..d798ca9a0f --- /dev/null +++ b/src/main/java/wooteco/chess/domain/piece/movable/Direction.java @@ -0,0 +1,44 @@ +package wooteco.chess.domain.piece.movable; + +public enum Direction { + NORTH(0, 1), + NORTHEAST(1, 1), + EAST(1, 0), + SOUTHEAST(1, -1), + SOUTH(0, -1), + SOUTHWEST(-1, -1), + WEST(-1, 0), + NORTHWEST(-1, 1), + + // Knight가 이동 가능한 방향 + NNE(1, 2), //북북동 + NNW(-1, 2), //북북서 + SSE(1, -2), //남남동 + SSW(-1, -2), //남남서 + EEN(2, 1), //동동북 + EES(2, -1), //동동남 + WWN(-2, 1), //서서북 + WWS(-2, -1), //서서남 + + NONE(0, 0); //방향이 없음 + + private int xDegree; + private int yDegree; + + Direction(int xDegree, int yDegree) { + this.xDegree = xDegree; + this.yDegree = yDegree; + } + + public boolean canOnlyMoveVertical() { + return xDegree == 0; + } + + public int getXDegree() { + return xDegree; + } + + public int getYDegree() { + return yDegree; + } +} diff --git a/src/main/java/wooteco/chess/domain/piece/movable/Movable.java b/src/main/java/wooteco/chess/domain/piece/movable/Movable.java new file mode 100644 index 0000000000..328a8f1420 --- /dev/null +++ b/src/main/java/wooteco/chess/domain/piece/movable/Movable.java @@ -0,0 +1,12 @@ +package wooteco.chess.domain.piece.movable; + +import wooteco.chess.domain.piece.Color; +import wooteco.chess.domain.piece.Piece; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.positions.Positions; + +import java.util.List; + +public interface Movable { + Positions findMovablePositions(Position position, List pieces, Color color); +} diff --git a/src/main/java/wooteco/chess/domain/piece/movable/MovableDirections.java b/src/main/java/wooteco/chess/domain/piece/movable/MovableDirections.java new file mode 100644 index 0000000000..4fe0a7647c --- /dev/null +++ b/src/main/java/wooteco/chess/domain/piece/movable/MovableDirections.java @@ -0,0 +1,27 @@ +package wooteco.chess.domain.piece.movable; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static wooteco.chess.domain.piece.movable.Direction.*; + +public enum MovableDirections { + LINEAR(Arrays.asList(NORTH, EAST, SOUTH, WEST)), + DIAGONAL(Arrays.asList(NORTHEAST, SOUTHEAST, SOUTHWEST, NORTHWEST)), + EVERY(Arrays.asList(NORTH, EAST, SOUTH, WEST, NORTHEAST, SOUTHEAST, SOUTHWEST, NORTHWEST)), + KNIGHT(Arrays.asList(NNE, NNW, SSE, SSW, EEN, EES, WWN, WWS)), + WHITE_PAWN(Arrays.asList(NORTH, NORTHEAST, NORTHWEST)), + BLACK_PAWN(Arrays.asList(SOUTH, SOUTHEAST, SOUTHWEST)), + NONE(Collections.emptyList()); + + private List directions; + + MovableDirections(List directions) { + this.directions = directions; + } + + public List getDirections() { + return directions; + } +} diff --git a/src/main/java/wooteco/chess/domain/piece/movable/PawnMovable.java b/src/main/java/wooteco/chess/domain/piece/movable/PawnMovable.java new file mode 100644 index 0000000000..6dfc08ffe1 --- /dev/null +++ b/src/main/java/wooteco/chess/domain/piece/movable/PawnMovable.java @@ -0,0 +1,164 @@ +package wooteco.chess.domain.piece.movable; + +import wooteco.chess.domain.piece.Color; +import wooteco.chess.domain.piece.Piece; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.positions.Positions; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * PawnMovable은 체스의 Pawn이 움직이는 방법을 구현한 클래스이다. + * Pawn의 움직임 규칙은 아래와 같다. + * - Pawn은 앞에 아무도 없을 경우 한 칸 전진할 수 있다. + * - 만약 대각선으로 앞쪽에 적 말이 있을 경우, 그 칸으로 이동해 적 말을 잡을 수 있다. + * - 만약 Pawn이 게임동안 한 번도 움직이지 않았다면(Initial 상태라면) 앞으로 2칸 움직일 수 있다. + * 본 클래스는 세 가지 룰을 메서드로 분리하고, 추가적인 검사 메서드를 구현하였다. + */ +public class PawnMovable implements Movable { + /** + * moveDirections는 Pawn이 움직일 수 있는 방향들을 저장한다. + */ + private final MovableDirections movableDirections; + + /** + * Constructor는 방향을 주입받아 이를 저장한다. + * + * @param movableDirections 움직일 수 있는 방향의 목록이다. 일반적으로 전진 및 대각선 전진(왼쪽방향, 오른쪽 방향)의 세 가지 방향을 포함한다. + */ + public PawnMovable(MovableDirections movableDirections) { + this.movableDirections = movableDirections; + } + + /** + * findMovablePositions는 이 인터페이스를 포함하는 상위 클래스(즉, Pawn을 구현한 클래스)에서 호출된다. + * 현재 말의 위치, 모든 말들의 목록, 이 말의 색상(색상이 같으면 같은 팀이다) 을 전달받는다. + * 이후 말이 움직일 수 있는 위치들을 확인하여, 이를 Positions 객체에 저장한다. + * 순서는 아래와 같다. + * - 먼저, 대각선으로 갈 수 있는지 확인하여 movablePositions를 만든다. + * - 전진하는 방향값을 구한 뒤, 전진이 가능한지(다른 말이 방해하지 않는지) 확인한다. 불가하다면 아래 두 줄은 생략한다. + * - 전진이 가능하다면 한 칸 전진하는 값을 movablePositions에 추가한다. + * - 만약 Pawn이 이번에 처음 움직이는 것이라면, 추가로 한 칸 더 전진할 수 있는지 검사하여 movablePositions에 추가한다. + * - 최종적으로 구한 movablePositions를 반환한다. + * + * @param position 말의 현재 위치값이다. 이 위치에 필드변수 moveDirections를 더하여 폰이 움직일 수 있는 위치들을 구한다. + * @param pieces 모든 말들을 저장한 리스트이다. 이를 통해 내가 움직이려는 자리에 아군/적군 말이 있는지 확인한다. + * @param color 말의 색상값이다. 다른 말과 위치가 겹칠 때, 색이 다른지(적이라는 뜻이므로, 말을 잡을 수 있다) + * 혹은 같은지(아군이 있는 곳으로는 이동할 수 없다) 여부를 판단한다. + * @return 최종적으로 Pawn이 position에서 출발해 갈 수 있는 모든 위치를 Positions 객체에 저장해 반환한다. + */ + @Override + public Positions findMovablePositions(Position position, List pieces, Color color) { + Positions movablePositions = getMovableDiagonalPositions(position, pieces, color); + Position forwardPosition = position.getMovedPositionBy(getForwardDirection()); + + if (isPossessed(forwardPosition, pieces)) { + return movablePositions; + } + movablePositions.add(getMovableForwardPosition(position, pieces)); + if (position.isPawnInitial(color)) { + movablePositions.add(getMovableForwardPosition(forwardPosition, pieces)); + } + return movablePositions; + } + + /** + * getMovableDiagonalPositions는 대각선으로 이동할 수 있는지 여부를 판단하고 이동 가능한 위치들을 반환한다. + * 먼저 클래스 필드변수로 저장되어 있는 Directions를 매핑하여, 이를 현재 좌표값에 더해 좌표값의 목록을 만든다. + * 그리고 이 중 같은 가로선상에 있는 경우(이는 getMovableForwardPositions에서 따로 처리한다)를 제외하고, + * 서로 다른 색상인 경우만 모아서 다시 목록을 만든다.(같은 색상인 경우 이동할 수 없다. 폰은 대각선에 적이 있는 경우에만 이동이 가능하다) + * 이를 인자로 하여 Positions를 생성하고, 이를 반환한다. + * + * @param position 말의 현재 위치값이다. + * @param pieces 모든 말의 목록값이다. + * @param color 말의 색상(팀) 정보이다. + * @return 대각선으로 이동 가능한 위치들을 모아 Positions 객체를 생성한 후 이를 반환한다. + */ + private Positions getMovableDiagonalPositions(Position position, List pieces, Color color) { + return movableDirections.getDirections() + .stream() + .map(position::getMovedPositionBy) + .filter(movablePosition -> position.isDifferentRow(movablePosition) && isPossessedByDifferentColor(movablePosition, pieces, color)) + .collect(Collectors.collectingAndThen(Collectors.toSet(), Positions::new)); + } + + /** + * isPossessedByDifferentColor는 전달받은 position에 나와 다른 색상(즉, 나와 다른 팀)의 말이 있는지 확인한다. + * 전달받은 모든 말의 목록인 pieces를 순회하며 말의 위치와 색상이 같은 경우를 찾고, + * 찾을 경우 true를, 찾지 못했을 경우 false를 반환한다. + * + * @param position 말의 현재 위치값이다. + * @param pieces 모든 말의 목록이다. + * @param color 말의 색상(팀) 값이다. + * @return 전달된 위치에 다른 색 말이 존재할 경우, true를 반환한다. 그렇지 않을 경우 false를 반환한다. + */ + private boolean isPossessedByDifferentColor(Position position, List pieces, Color color) { + return pieces.stream() + .anyMatch(piece -> piece.isSamePosition(position) && piece.isNotSameColor(color)); + } + + /** + * getForwardDirection은 클래스가 필드변수로 가지는 Directions 값 중 전진하는 값(즉, 내 위치에서 x값이 바뀌지 않고 움직이는 값)을 찾는다. + * Pawn의 경우 전진은 하나의 방향만 가지므로(2칸 이동 룰 역시 이를 재활용하기에, moveDirections에는 1개만 존재한다) Direction을 반환한다. + * 찾지 못했을 경우, Direction.NONE을 반환한다. 이는 움직임이 없는 방향값이다. + * (이 방향값으로 움직임을 실행 시, 모든 말의 목록에 포함되는 자기 자신때문에 isPossessed가 false로 나온다. 즉 의도하지 않은 이동이 불가하다.) + * + * @return 전진하는 Direction 값을 찾아 반환한다. 없을 경우 Direction.NONE을 반환한다. + */ + private Direction getForwardDirection() { + return movableDirections.getDirections() + .stream() + .filter(Direction::canOnlyMoveVertical) + .findFirst() + .orElse(Direction.NONE); + } + + /** + * getMovableForwardPosition은 전진할 수 있는지 여부를 판단하고 가능할 시 그 위치를 반환하는 메서드이다. + * 전달받은 위치값을 바탕으로 movableForwardPosition이라는, 전진시에 도착하게 될 Position을 먼저 구한다. + * 그리고 이 값에 다른 말이 없을 경우(폰은 전진할 때 다른 말을 잡을 수 없으므로, 아무 말도 없어야만 전진이 가능하다) + * 그 위치를 반환한다. + * 만약 이동이 불가할 경우, 원래의 위치를 반환한다. + * 원래의 위치로 이동하려고 할 경우 WrongPositionException이 걸리므로, 로직상의 문제가 없으리라 판단했다. + * + * @param position 말의 현재 위치 값이다. + * @param pieces 모든 말의 목록이다. 말이 움직일 위치에 다른 말이 있는지 여부를 확인한다. + * @return 움직일 수 있을 경우, 그 위치를 반환한다. 움직일 수 없을 경우, 현재의 위치를 반환한다. + */ + private Position getMovableForwardPosition(Position position, List pieces) { + Position movableForwardPosition = position.getMovedPositionBy(getForwardDirection()); + if (isNotPossessed(movableForwardPosition, pieces)) { + return movableForwardPosition; + } + return position; + } + + /** + * isPossessed는 위치값과 말의 목록을 받아 그 위치에 말이 있는지 점검한다. + * 만약 말이 하나라도 있을 경우, true를 반환한다. + * 그렇지 않을 경우 false를 반환한다. + * + * @param position 확인할 Position 값이다. + * @param pieces 모든 말의 목록이다. + * @return Position에 말이 있는지 여부를 boolean으로 반환한다. + */ + private boolean isPossessed(Position position, List pieces) { + return pieces.stream() + .anyMatch(piece -> piece.isSamePosition(position)); + } + + /** + * isNotPossessed는 위치값과 말의 목록을 받아 그 위치에 말이 없는지 점검한다. + * 만약 말이 하나도 없을 경우, true를 반환한다. + * 그렇지 않을 경우 false를 반환한다. + * + * @param position 확인할 Position 값이다. + * @param pieces 모든 말의 목록이다. + * @return Position에 말이 없는지 여부를 boolean으로 반환한다. + */ + private boolean isNotPossessed(Position position, List pieces) { + return pieces.stream() + .noneMatch(piece -> piece.isSamePosition(position)); + } +} \ No newline at end of file diff --git a/src/main/java/wooteco/chess/domain/piece/movable/UnblockedMovable.java b/src/main/java/wooteco/chess/domain/piece/movable/UnblockedMovable.java new file mode 100644 index 0000000000..1ba8d1aeab --- /dev/null +++ b/src/main/java/wooteco/chess/domain/piece/movable/UnblockedMovable.java @@ -0,0 +1,70 @@ +package wooteco.chess.domain.piece.movable; + +import wooteco.chess.domain.piece.Color; +import wooteco.chess.domain.piece.Piece; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.positions.Positions; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * UnblockedMovable은 정해진 칸으로만 움직이는 King, Knight의 움직이는 방법을 구현한 클래스이다. + * UnblockedMovable이란 이름은 막히지 않는(즉 Rook이나 Bishop처럼 누군가에 의해 막힐 때까지 전진하는 로직이 아닌) 움직임을 가졌다는 의미를 가진다. + * UnblockedMovable을 사용하는 말의 움직임 규칙은 아래와 같다. + * - 갈 수 있는 칸에 대해, 다른 말이 있는지 확인한다. 만약 아군이 있으면 이동할 수 없고, 비어있거나 적이 있다면 이동할 수 있다. + */ +public class UnblockedMovable implements Movable { + /** + * moveDirections는 Pawn이 움직일 수 있는 방향들을 저장한다. + */ + private final MovableDirections movableDirections; + + /** + * Constructor는 방향을 주입받아 이를 저장한다. + * + * @param movableDirections 움직일 수 있는 방향의 목록이다. 각 말의 설정에 따라 다른 값이 들어온다. + */ + public UnblockedMovable(MovableDirections movableDirections) { + this.movableDirections = movableDirections; + } + + /** + * findMovablePositions는 이 인터페이스를 포함하는 상위 클래스(즉, King이나 Knight를 구현한 클래스)에서 호출된다. + * 현재 말의 위치, 모든 말들의 목록, 이 말의 색상(색상이 같으면 같은 팀이다) 을 전달받는다. + * 이후 말이 움직일 수 있는 방향들을 확인하여, 이를 순회한다. + * 순서는 아래와 같다. + * - 먼저, 방향값을 통해 구한 위치(현재 위치에서 이동하여 도착하는 위치)를 모은다. + * - 이 값들 중 움직일 수 없는 위치(다른 아군 말이 있는 위치)를 필터링한다. + * - 필터링되지 않은 위치들을 모은다. + * - 최종적으로 구한 Positions 객체를 반환한다. + * + * @param position 말의 현재 위치값이다. 이 위치에 필드변수 moveDirections를 더하여 말이 움직일 수 있는 위치들을 구한다. + * @param pieces 모든 말들을 저장한 리스트이다. 이를 통해 내가 움직이려는 자리에 아군 말이 있는지 확인한다. + * @param color 말의 색상값이다. 다른 말과 위치가 겹칠 때, 색이 다른지(적이라는 뜻이므로, 말을 잡을 수 있다) + * 혹은 같은지(아군이 있는 곳으로는 이동할 수 없다) 여부를 판단한다. + * @return 최종적으로 말이 position에서 출발해 갈 수 있는 모든 위치를 Positions 객체에 저장해 반환한다. + */ + @Override + public Positions findMovablePositions(Position position, List pieces, Color color) { + return movableDirections.getDirections().stream() + .map(position::getMovedPositionBy) + .filter(movablePosition -> checkMovable(movablePosition, pieces, color)) + .collect(Collectors.collectingAndThen(Collectors.toSet(), Positions::new)); + } + + /** + * checkMovable은 위치와 모든 말의 목록 및 말의 색상을 전달받아, 그 위치에 아군 말이 있는지 여부를 검사한다. + * 만약 그 위치에 아군 말이 있을 경우, 그 위치로는 이동할 수 없으므로 false를 반환한다. + * 그러나 적군 말(잡을 수 있다)이 있거나 비어있는 경우 true를 반환한다. + * + * @param position 말의 현재 위치이다. + * @param pieces 모든 말의 목록이다. + * @param color 말의 색상(팀) 이다. 이를 바탕으로 같은 색을 찾는다. + * @return 찾고자 하는 위치에 아군 말이 있을 경우 false를, 그렇지 않은 경우 true를 반환한다. + */ + private boolean checkMovable(Position position, List pieces, Color color) { + return pieces.stream() + .noneMatch(piece -> piece.isSamePosition(position) && piece.isSameColor(color)); + } +} \ No newline at end of file diff --git a/src/main/java/wooteco/chess/domain/piece/pieces/Pieces.java b/src/main/java/wooteco/chess/domain/piece/pieces/Pieces.java new file mode 100644 index 0000000000..b754b162ea --- /dev/null +++ b/src/main/java/wooteco/chess/domain/piece/pieces/Pieces.java @@ -0,0 +1,72 @@ +package wooteco.chess.domain.piece.pieces; + +import wooteco.chess.domain.piece.Blank; +import wooteco.chess.domain.piece.Color; +import wooteco.chess.domain.piece.Piece; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.positions.Positions; + +import java.util.List; + +public class Pieces { + private static final String INVALID_INPUT_EXCEPTION_MESSAGE = "유효한 입력이 아닙니다. 다시 입력해주세요"; + private static final int DEFAULT_KING_COUNT = 2; + + private final List pieces; + + // package accessed + Pieces(List pieces) { + this.pieces = pieces; + } + + public void move(Position start, Position end, Color color) { + Piece piece = findBy(start, color); + Positions movablePositions = piece.createMovablePositions(pieces); + validateEndPositionIsMovable(end, movablePositions); + + Piece removingPiece = findBy(end); + pieces.remove(removingPiece); + + piece.move(end); + } + + public Piece findBy(Position position) { + return pieces.stream() + .filter(piece -> piece.isSamePosition(position)) + .findFirst() + .orElseGet(Blank::new); + } + + public Piece findBy(Position start, Color color) { + Piece piece = findBy(start); + if (piece.isSameColor(color)) { + return piece; + } + throw new IllegalArgumentException(INVALID_INPUT_EXCEPTION_MESSAGE); + } + + private void validateEndPositionIsMovable(Position end, Positions movablePositions) { + if (!movablePositions.contains(end)) { + throw new IllegalArgumentException(INVALID_INPUT_EXCEPTION_MESSAGE); + } + } + + public boolean isKingDead() { + int kingCount = (int) pieces.stream() + .filter(Piece::isKing) + .count(); + return kingCount != DEFAULT_KING_COUNT; + } + + public Color getAliveKingColor() { + return pieces.stream() + .filter(Piece::isKing) + .map(Piece::getColor) + .findFirst() + .orElseThrow(() -> new UnsupportedOperationException("현재상황에서 사용할 수 없는 메서드입니다.")); + } + + public List getPieces() { + return pieces; + } +} \ No newline at end of file diff --git a/src/main/java/wooteco/chess/domain/piece/pieces/PiecesInitializer.java b/src/main/java/wooteco/chess/domain/piece/pieces/PiecesInitializer.java new file mode 100644 index 0000000000..7a6500ab5d --- /dev/null +++ b/src/main/java/wooteco/chess/domain/piece/pieces/PiecesInitializer.java @@ -0,0 +1,44 @@ +package wooteco.chess.domain.piece.pieces; + +import wooteco.chess.domain.piece.Color; +import wooteco.chess.domain.piece.Piece; +import wooteco.chess.domain.position.Column; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.PositionFactory; +import wooteco.chess.domain.position.Row; + +import java.util.ArrayList; +import java.util.List; + +public class PiecesInitializer { + public static Pieces operate() { + return makeInitialPieces(); + } + + private static Pieces makeInitialPieces() { + List pieces = new ArrayList<>(); + + for (Column column : Column.getInitialColumns()) { + addPieceBy(column, pieces); + } + + return new Pieces(pieces); + } + + private static void addPieceBy(Column column, List pieces) { + for (Row row : Row.values()) { + pieces.add(create(row, column)); + } + } + + private static Piece create(Row row, Column column) { + Position position = PositionFactory.of(row, column); + Color color = column.getColor(); + + if (column.isPawnInitial()) { + return new Piece(position, column.getPawnType(), color); + } + + return new Piece(position, row.getPieceType(), color); + } +} diff --git a/src/main/java/wooteco/chess/domain/position/Column.java b/src/main/java/wooteco/chess/domain/position/Column.java new file mode 100644 index 0000000000..bc10abb766 --- /dev/null +++ b/src/main/java/wooteco/chess/domain/position/Column.java @@ -0,0 +1,87 @@ +package wooteco.chess.domain.position; + +import wooteco.chess.domain.piece.Color; +import wooteco.chess.domain.piece.PieceType; + +import java.util.Arrays; +import java.util.List; + +public enum Column { + EIGHTH("8", 8), + SEVENTH("7", 7), + SIXTH("6", 6), + FIFTH("5", 5), + FOURTH("4", 4), + THIRD("3", 3), + SECOND("2", 2), + FIRST("1", 1); + + private static final String INVALID_INPUT_EXCEPTION_MESSAGE = "옳지 않은 좌표 입력입니다."; + private static final int BLACK_PAWN_INITIAL_COLUMN = 7; + private static final int WHITE_PAWN_INITIAL_COLUMN = 2; + private static final double BOTTOM_ZONE_END_POINT = 4; + + private final String name; + private final int value; + + Column(String name, int value) { + this.name = name; + this.value = value; + } + + public static Column of(String name) { + return Arrays.stream(Column.values()) + .filter(column -> column.name.equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(INVALID_INPUT_EXCEPTION_MESSAGE)); + } + + public Column calculate(int value) { + return Arrays.stream(Column.values()) + .filter(column -> column.value == this.value + value) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException((INVALID_INPUT_EXCEPTION_MESSAGE))); + } + + public boolean isPawnInitial() { + return isWhitePawnInitial() || isBlackPawnInitial(); + } + + public boolean isWhitePawnInitial() { + return value == WHITE_PAWN_INITIAL_COLUMN; + } + + public boolean isBlackPawnInitial() { + return value == BLACK_PAWN_INITIAL_COLUMN; + } + + public Color getColor() { + if (isOnHalfBottom()) { + return Color.WHITE; + } + return Color.BLACK; + } + + public boolean isOnHalfBottom() { + return value <= BOTTOM_ZONE_END_POINT; + } + + public static List getInitialColumns() { + return Arrays.asList(EIGHTH, SEVENTH, SECOND, FIRST); + } + + public PieceType getPawnType() { + if (isWhitePawnInitial()) { + return PieceType.WHITE_PAWN; + } + return PieceType.BLACK_PAWN; + } + + public String getName() { + return name; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/wooteco/chess/domain/position/MovingPosition.java b/src/main/java/wooteco/chess/domain/position/MovingPosition.java new file mode 100644 index 0000000000..7cefe94bce --- /dev/null +++ b/src/main/java/wooteco/chess/domain/position/MovingPosition.java @@ -0,0 +1,31 @@ +package wooteco.chess.domain.position; + +public class MovingPosition { + private final String start; + private final String end; + + public MovingPosition(String start, String end) { + this.start = start; + this.end = end; + } + + public boolean isStartAndEndSame() { + return start.equals(end); + } + + public Position getStartPosition() { + return PositionFactory.of(start); + } + + public Position getEndPosition() { + return PositionFactory.of(end); + } + + public String getStart() { + return start; + } + + public String getEnd() { + return end; + } +} diff --git a/src/main/java/wooteco/chess/domain/position/Position.java b/src/main/java/wooteco/chess/domain/position/Position.java new file mode 100644 index 0000000000..af7c22d28d --- /dev/null +++ b/src/main/java/wooteco/chess/domain/position/Position.java @@ -0,0 +1,75 @@ +package wooteco.chess.domain.position; + +import wooteco.chess.domain.piece.Color; +import wooteco.chess.domain.piece.movable.Direction; + +import java.util.Objects; + +public class Position { + private static final String INVALID_INPUT_EXCEPTION_MESSAGE = "옳지 않은 좌표 입력입니다."; + private static final int MIN_BOUND = 1; + private static final int MAX_BOUND = 8; + private static final int ROW_START_INDEX = 0; + private static final int COLUMN_START_INDEX = 1; + + private final Row row; + private final Column column; + + // package accessed + Position(String position) { + validate(position); + this.row = Row.of(position.substring(ROW_START_INDEX, ROW_START_INDEX + 1)); + this.column = Column.of(position.substring(COLUMN_START_INDEX, COLUMN_START_INDEX + 1)); + } + + Position(Row row, Column column) { + this.row = row; + this.column = column; + } + + private void validate(String position) { + Objects.requireNonNull(position, INVALID_INPUT_EXCEPTION_MESSAGE); + if (position.isEmpty()) { + throw new IllegalArgumentException(INVALID_INPUT_EXCEPTION_MESSAGE); + } + } + + public Position getMovedPositionBy(Direction direction) { + if (!checkBound(direction)) { + return this; + } + Row movedRow = row.calculate(direction.getXDegree()); + Column movedColumn = column.calculate(direction.getYDegree()); + return PositionFactory.of(movedRow, movedColumn); + } + + public boolean checkBound(Direction direction) { + int checkingRow = row.getValue() + direction.getXDegree(); + int checkingColumn = column.getValue() + direction.getYDegree(); + return isValidBound(checkingRow) && isValidBound(checkingColumn); + } + + public boolean isPawnInitial(Color color) { + if (column.isWhitePawnInitial() && color.isWhite()) { + return true; + } + return column.isBlackPawnInitial() && color.isBlack(); + } + + public boolean isDifferentRow(Position position) { + return !this.row.equals(position.row); + } + + private static boolean isValidBound(int value) { + return value >= MIN_BOUND && value <= MAX_BOUND; + } + + public Row getRow() { + return row; + } + + @Override + public String toString() { + return row.getName() + column.getValue(); + } +} diff --git a/src/main/java/wooteco/chess/domain/position/PositionFactory.java b/src/main/java/wooteco/chess/domain/position/PositionFactory.java new file mode 100644 index 0000000000..4b9764f491 --- /dev/null +++ b/src/main/java/wooteco/chess/domain/position/PositionFactory.java @@ -0,0 +1,44 @@ +package wooteco.chess.domain.position; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class PositionFactory { + private static final String INVALID_INPUT_EXCEPTION_MESSAGE = "옳지 않은 좌표 입력입니다."; + + private static final Map positionCache; + + static { + positionCache = new HashMap<>(); + for (Row row : Row.values()) { + createPositionBy(row); + } + } + + private static void createPositionBy(Row row) { + for (Column column : Column.values()) { + String name = row.getName() + column.getName(); + positionCache.put(name, new Position(row, column)); + } + } + + public static Position of(String position) { + validate(position); + return positionCache.get(position); + } + + public static Position of(Row row, Column column) { + return of(row.getName() + column.getName()); + } + + private static void validate(String position) { + Objects.requireNonNull(position, INVALID_INPUT_EXCEPTION_MESSAGE); + if (position.isEmpty()) { + throw new IllegalArgumentException(INVALID_INPUT_EXCEPTION_MESSAGE); + } + if (!positionCache.containsKey(position)) { + throw new IllegalArgumentException(INVALID_INPUT_EXCEPTION_MESSAGE); + } + } +} diff --git a/src/main/java/wooteco/chess/domain/position/Row.java b/src/main/java/wooteco/chess/domain/position/Row.java new file mode 100644 index 0000000000..066cfcac7d --- /dev/null +++ b/src/main/java/wooteco/chess/domain/position/Row.java @@ -0,0 +1,96 @@ +package wooteco.chess.domain.position; + +import wooteco.chess.domain.piece.PieceType; + +import java.util.Arrays; + +public enum Row { + FIRST("a", 1), + SECOND("b", 2), + THIRD("c", 3), + FOURTH("d", 4), + FIFTH("e", 5), + SIXTH("f", 6), + SEVENTH("g", 7), + EIGHTH("h", 8); + + private static final String INVALID_INPUT_EXCEPTION_MESSAGE = "옳지 않은 좌표 입력입니다."; + + private final String name; + private final int value; + + Row(String name, int value) { + this.name = name; + this.value = value; + } + + public static Row of(String name) { + return Arrays.stream(Row.values()) + .filter(row -> row.name.equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException((INVALID_INPUT_EXCEPTION_MESSAGE))); + } + + public Row calculate(int value) { + return Arrays.stream(Row.values()) + .filter(row -> row.value == this.value + value) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException((INVALID_INPUT_EXCEPTION_MESSAGE))); + } + + public boolean isSame(Row row) { + return this.equals(row); + } + + public PieceType getPieceType() { + if (isRookInitial()) { + return PieceType.ROOK; + } + + if (isKnightInitial()) { + return PieceType.KNIGHT; + } + + if (isBishopInitial()) { + return PieceType.BISHOP; + } + + if (isQueenInitial()) { + return PieceType.QUEEN; + } + + if (isKingInitial()) { + return PieceType.KING; + } + + return PieceType.BLANK; + } + + private boolean isRookInitial() { + return this.equals(FIRST) || this.equals(EIGHTH); + } + + private boolean isKnightInitial() { + return this.equals(SECOND) || this.equals(SEVENTH); + } + + private boolean isBishopInitial() { + return this.equals(THIRD) || this.equals(SIXTH); + } + + private boolean isQueenInitial() { + return this.equals(FIFTH); + } + + private boolean isKingInitial() { + return this.equals(FOURTH); + } + + public String getName() { + return name; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/wooteco/chess/domain/position/positions/Positions.java b/src/main/java/wooteco/chess/domain/position/positions/Positions.java new file mode 100644 index 0000000000..3aeae563c4 --- /dev/null +++ b/src/main/java/wooteco/chess/domain/position/positions/Positions.java @@ -0,0 +1,38 @@ +package wooteco.chess.domain.position.positions; + +import wooteco.chess.domain.position.Position; + +import java.util.HashSet; +import java.util.Set; + +public class Positions { + private final Set positions; + + public Positions(Set positions) { + this.positions = positions; + } + + private Positions(HashSet positions) { + this.positions = positions; + } + + public static Positions create() { + return new Positions(new HashSet<>()); + } + + public void add(Position position) { + this.positions.add(position); + } + + public void addAll(Positions positions) { + this.positions.addAll(positions.positions); + } + + public boolean contains(Position position) { + return positions.contains(position); + } + + public Set getPositions() { + return positions; + } +} diff --git a/src/main/java/wooteco/chess/dto/BoardDto.java b/src/main/java/wooteco/chess/dto/BoardDto.java new file mode 100644 index 0000000000..6fa8de5de8 --- /dev/null +++ b/src/main/java/wooteco/chess/dto/BoardDto.java @@ -0,0 +1,22 @@ +package wooteco.chess.dto; + +import wooteco.chess.domain.piece.Piece; +import wooteco.chess.domain.piece.pieces.Pieces; +import wooteco.chess.domain.position.Position; + +import java.util.HashMap; +import java.util.Map; + +public class BoardDto { + private final Map board = new HashMap<>(); + + public BoardDto(Pieces pieces) { + for (Piece piece : pieces.getPieces()) { + board.put(piece.getPosition(), new PieceDto(piece)); + } + } + + public Map getBoard() { + return board; + } +} \ No newline at end of file diff --git a/src/main/java/wooteco/chess/dto/ChessGameDto.java b/src/main/java/wooteco/chess/dto/ChessGameDto.java new file mode 100644 index 0000000000..9fb3f033ae --- /dev/null +++ b/src/main/java/wooteco/chess/dto/ChessGameDto.java @@ -0,0 +1,34 @@ +package wooteco.chess.dto; + +import wooteco.chess.domain.game.ScoreResult; +import wooteco.chess.domain.game.Turn; + +public class ChessGameDto { + private BoardDto boardDto; + private Turn turn; + private ScoreResult score; + private boolean normalStatus; + + public ChessGameDto(BoardDto boardDto, Turn turn, ScoreResult score, boolean normalStatus) { + this.boardDto = boardDto; + this.turn = turn; + this.score = score; + this.normalStatus = normalStatus; + } + + public BoardDto getBoardDto() { + return boardDto; + } + + public Turn getTurn() { + return turn; + } + + public ScoreResult getScore() { + return score; + } + + public boolean isNormalStatus() { + return normalStatus; + } +} diff --git a/src/main/java/wooteco/chess/dto/DestinationPositionDto.java b/src/main/java/wooteco/chess/dto/DestinationPositionDto.java new file mode 100644 index 0000000000..c11a65099f --- /dev/null +++ b/src/main/java/wooteco/chess/dto/DestinationPositionDto.java @@ -0,0 +1,21 @@ +package wooteco.chess.dto; + +import wooteco.chess.domain.game.NormalStatus; + +public class DestinationPositionDto { + private String position; + private NormalStatus normalStatus; + + public DestinationPositionDto(String position, NormalStatus normalStatus) { + this.position = position; + this.normalStatus = normalStatus; + } + + public String getPosition() { + return position; + } + + public NormalStatus getNormalStatus() { + return normalStatus; + } +} diff --git a/src/main/java/wooteco/chess/dto/MovablePositionsDto.java b/src/main/java/wooteco/chess/dto/MovablePositionsDto.java new file mode 100644 index 0000000000..81af3719ee --- /dev/null +++ b/src/main/java/wooteco/chess/dto/MovablePositionsDto.java @@ -0,0 +1,21 @@ +package wooteco.chess.dto; + +import java.util.List; + +public class MovablePositionsDto { + private List movablePositionNames; + private String position; + + public MovablePositionsDto(List movablePositionNames, String position) { + this.movablePositionNames = movablePositionNames; + this.position = position; + } + + public List getMovablePositionNames() { + return movablePositionNames; + } + + public String getPosition() { + return position; + } +} diff --git a/src/main/java/wooteco/chess/dto/MoveStatusDto.java b/src/main/java/wooteco/chess/dto/MoveStatusDto.java new file mode 100644 index 0000000000..33b1103396 --- /dev/null +++ b/src/main/java/wooteco/chess/dto/MoveStatusDto.java @@ -0,0 +1,26 @@ +package wooteco.chess.dto; + +import wooteco.chess.domain.piece.Color; + +public class MoveStatusDto { + private boolean normalStatus; + private Color winner; + + public MoveStatusDto(boolean normalStatus, Color winner) { + this.normalStatus = normalStatus; + this.winner = winner; + } + + public MoveStatusDto(boolean normalStatus) { + this.normalStatus = normalStatus; + this.winner = Color.NONE; + } + + public boolean getNormalStatus() { + return normalStatus; + } + + public Color getWinner() { + return winner; + } +} diff --git a/src/main/java/wooteco/chess/dto/PieceDto.java b/src/main/java/wooteco/chess/dto/PieceDto.java new file mode 100644 index 0000000000..4606ff2ceb --- /dev/null +++ b/src/main/java/wooteco/chess/dto/PieceDto.java @@ -0,0 +1,23 @@ +package wooteco.chess.dto; + +import wooteco.chess.domain.piece.Color; +import wooteco.chess.domain.piece.Piece; +import wooteco.chess.domain.piece.PieceType; + +public class PieceDto { + private final PieceType pieceType; + private final Color color; + + public PieceDto(Piece piece) { + this.pieceType = piece.getPieceType(); + this.color = piece.getColor(); + } + + public PieceType getPieceType() { + return pieceType; + } + + public Color getColor() { + return color; + } +} \ No newline at end of file diff --git a/src/main/java/wooteco/chess/service/ChessWebService.java b/src/main/java/wooteco/chess/service/ChessWebService.java new file mode 100644 index 0000000000..aa312a52a1 --- /dev/null +++ b/src/main/java/wooteco/chess/service/ChessWebService.java @@ -0,0 +1,78 @@ +package wooteco.chess.service; + +import wooteco.chess.dao.HistoryDao; +import wooteco.chess.domain.game.ChessGame; +import wooteco.chess.domain.game.NormalStatus; +import wooteco.chess.domain.position.MovingPosition; +import wooteco.chess.dto.*; + +import java.sql.SQLException; +import java.util.List; + +public class ChessWebService { + public void clearHistory() throws SQLException { + HistoryDao historyDao = new HistoryDao(); + + historyDao.clear(); + } + + public ChessGameDto setBoard() throws SQLException { + ChessGame chessGame = new ChessGame(); + + load(chessGame); + + return new ChessGameDto(new BoardDto(chessGame.getPieces()), chessGame.getTurn(), chessGame.calculateScore(), NormalStatus.YES.isNormalStatus()); + } + + private void load(ChessGame chessGame) throws SQLException { + List histories = selectAllHistory(); + + for (MovingPosition movingPosition : histories) { + chessGame.move(movingPosition); + } + } + + private List selectAllHistory() throws SQLException { + HistoryDao historyDao = new HistoryDao(); + return historyDao.selectAll(); + } + + public MoveStatusDto move(MovingPosition movingPosition) throws SQLException { + if (movingPosition.isStartAndEndSame()) { + return new MoveStatusDto(NormalStatus.YES.isNormalStatus()); + } + + ChessGame chessGame = new ChessGame(); + + load(chessGame); + chessGame.move(movingPosition); + + if (chessGame.isKingDead()) { + MoveStatusDto moveStatusDto = new MoveStatusDto(NormalStatus.YES.isNormalStatus(), chessGame.getAliveKingColor()); + clearHistory(); + return moveStatusDto; + } + + insertHistory(movingPosition); + + return new MoveStatusDto(NormalStatus.YES.isNormalStatus()); + } + + private void insertHistory(MovingPosition movingPosition) throws SQLException { + HistoryDao historyDao = new HistoryDao(); + historyDao.insert(movingPosition); + } + + public MovablePositionsDto findMovablePositions(String position) throws SQLException { + ChessGame chessGame = new ChessGame(); + load(chessGame); + + List movablePositionNames = chessGame.findMovablePositionNames(position); + + return new MovablePositionsDto(movablePositionNames, position); + } + + public DestinationPositionDto chooseDestinationPosition(String position) { + return new DestinationPositionDto(position, NormalStatus.YES); + } +} diff --git a/src/main/java/wooteco/chess/web/ConnectionManager.java b/src/main/java/wooteco/chess/web/ConnectionManager.java new file mode 100644 index 0000000000..fa5979ae44 --- /dev/null +++ b/src/main/java/wooteco/chess/web/ConnectionManager.java @@ -0,0 +1,35 @@ +package wooteco.chess.web; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class ConnectionManager { + public Connection getConnection() { + Connection con = null; + String server = "localhost:13306"; // MySQL 서버 주소 + String database = "db_name"; // MySQL DATABASE 이름 + String option = "?useSSL=false&serverTimezone=UTC"; + String userName = "root"; // MySQL 서버 아이디 + String password = "root"; // MySQL 서버 비밀번호 + + // 드라이버 로딩 + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + } catch (ClassNotFoundException e) { + System.err.println(" !! JDBC Driver load 오류: " + e.getMessage()); + e.printStackTrace(); + } + + // 드라이버 연결 + try { + con = DriverManager.getConnection("jdbc:mysql://" + server + "/" + database + option, userName, password); + System.out.println("정상적으로 연결되었습니다."); + } catch (SQLException e) { + System.err.println("연결 오류:" + e.getMessage()); + e.printStackTrace(); + } + + return con; + } +} diff --git a/src/main/java/wooteco/chess/web/JsonTransformer.java b/src/main/java/wooteco/chess/web/JsonTransformer.java new file mode 100644 index 0000000000..3fd1eede18 --- /dev/null +++ b/src/main/java/wooteco/chess/web/JsonTransformer.java @@ -0,0 +1,15 @@ +package wooteco.chess.web; + +import com.google.gson.Gson; +import spark.ResponseTransformer; + +public class JsonTransformer { + + public static String toJson(Object object) { + return new Gson().toJson(object); + } + + public static ResponseTransformer json() { + return JsonTransformer::toJson; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f67c3336f6..fc77ac1d11 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,4 @@ spring.h2.console.enabled=true - spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url= spring.datasource.username= diff --git a/src/main/resources/logback-access.xml b/src/main/resources/logback-access.xml index d6a1c32200..7be2110b11 100644 --- a/src/main/resources/logback-access.xml +++ b/src/main/resources/logback-access.xml @@ -5,5 +5,5 @@ - + \ No newline at end of file diff --git a/src/main/resources/templates/chess.html b/src/main/resources/templates/chess.html new file mode 100644 index 0000000000..17dcd1ff72 --- /dev/null +++ b/src/main/resources/templates/chess.html @@ -0,0 +1,111 @@ + + + + + ♛Play Chess Game♚ + + + +
+

WOOWA CHESS GAME

+
+
+
+
+ {{#normalStatus}} + + {{/normalStatus}} + {{^normalStatus}} + + {{/normalStatus}} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/image/bishop_black.png b/src/main/resources/templates/image/bishop_black.png new file mode 100644 index 0000000000..b12c826bf9 Binary files /dev/null and b/src/main/resources/templates/image/bishop_black.png differ diff --git a/src/main/resources/templates/image/bishop_white.png b/src/main/resources/templates/image/bishop_white.png new file mode 100644 index 0000000000..3b251c1184 Binary files /dev/null and b/src/main/resources/templates/image/bishop_white.png differ diff --git a/src/main/resources/templates/image/king_black.png b/src/main/resources/templates/image/king_black.png new file mode 100644 index 0000000000..41528fd001 Binary files /dev/null and b/src/main/resources/templates/image/king_black.png differ diff --git a/src/main/resources/templates/image/king_white.png b/src/main/resources/templates/image/king_white.png new file mode 100644 index 0000000000..179ca63927 Binary files /dev/null and b/src/main/resources/templates/image/king_white.png differ diff --git a/src/main/resources/templates/image/knight_black.png b/src/main/resources/templates/image/knight_black.png new file mode 100644 index 0000000000..d097828c7c Binary files /dev/null and b/src/main/resources/templates/image/knight_black.png differ diff --git a/src/main/resources/templates/image/knight_white.png b/src/main/resources/templates/image/knight_white.png new file mode 100644 index 0000000000..139647f9e4 Binary files /dev/null and b/src/main/resources/templates/image/knight_white.png differ diff --git a/src/main/resources/templates/image/pawn_black.png b/src/main/resources/templates/image/pawn_black.png new file mode 100644 index 0000000000..7fbac4b214 Binary files /dev/null and b/src/main/resources/templates/image/pawn_black.png differ diff --git a/src/main/resources/templates/image/pawn_white.png b/src/main/resources/templates/image/pawn_white.png new file mode 100644 index 0000000000..62e06728f8 Binary files /dev/null and b/src/main/resources/templates/image/pawn_white.png differ diff --git a/src/main/resources/templates/image/queen_black.png b/src/main/resources/templates/image/queen_black.png new file mode 100644 index 0000000000..9205d409e3 Binary files /dev/null and b/src/main/resources/templates/image/queen_black.png differ diff --git a/src/main/resources/templates/image/queen_white.png b/src/main/resources/templates/image/queen_white.png new file mode 100644 index 0000000000..09aa277868 Binary files /dev/null and b/src/main/resources/templates/image/queen_white.png differ diff --git a/src/main/resources/templates/image/rook_black.png b/src/main/resources/templates/image/rook_black.png new file mode 100644 index 0000000000..9090349eae Binary files /dev/null and b/src/main/resources/templates/image/rook_black.png differ diff --git a/src/main/resources/templates/image/rook_white.png b/src/main/resources/templates/image/rook_white.png new file mode 100644 index 0000000000..86ccddb75f Binary files /dev/null and b/src/main/resources/templates/image/rook_white.png differ diff --git a/src/main/resources/templates/index.hbs b/src/main/resources/templates/index.hbs deleted file mode 100644 index 011c8cb828..0000000000 --- a/src/main/resources/templates/index.hbs +++ /dev/null @@ -1,9 +0,0 @@ - - - - 모두의 체스 - - -

Hello World

- - \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000000..f804b32259 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,18 @@ + + + + + CHESS GAME + + + +

체스게임

+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/public/css/chess.css b/src/main/resources/templates/public/css/chess.css new file mode 100644 index 0000000000..28f35f6d4a --- /dev/null +++ b/src/main/resources/templates/public/css/chess.css @@ -0,0 +1,58 @@ +.title { + justify-content: center; + background-color: gray; + font-size: 48px; + background-color: white; + text-align: center; +} + +.outer-board { + background-color: gray; + width: 800px; + height: 800px; + margin: 0 auto; + text-align: center; + justify-content: center; + position: relative; + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); +} + +.inner-board { + width: 640px; + height: 640px; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + position: absolute; + background-color: darkgray; + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); +} + +.column { + width: 800px; + height: 80px; + display: flex; + position: relative; +} + +.column:nth-of-type(2n - 1) .position:nth-of-type(2n - 1), +.column:nth-of-type(2n) .position:nth-of-type(2n) { + color: #ffffff; + background-color: antiquewhite; +} + +.position { + width: 80px; + height: 80px; + background-color: slategray; +} + +.piece { + width: 100%; + height: 100%; +} + +#turn-box { + height: 20px; + padding: 0; +} diff --git a/src/main/resources/templates/public/css/index.css b/src/main/resources/templates/public/css/index.css new file mode 100644 index 0000000000..76f77421f2 --- /dev/null +++ b/src/main/resources/templates/public/css/index.css @@ -0,0 +1,21 @@ +h1 { + color: blue; + font-size: 48px; + text-align: center; +} + +#new { + text-align: center; +} + +#new a { + font-size: 32px; +} + +#loading { + text-align: center; +} + +#loading a { + font-size: 32px; +} \ No newline at end of file diff --git a/src/main/resources/templates/public/css/result.css b/src/main/resources/templates/public/css/result.css new file mode 100644 index 0000000000..c69384af46 --- /dev/null +++ b/src/main/resources/templates/public/css/result.css @@ -0,0 +1,12 @@ +.result { + margin: 0 auto; + text-align: center; +} + +.result p { + font-size: 32px; +} + +#go-main { + font-size: 28px; +} \ No newline at end of file diff --git a/src/main/resources/templates/public/js/chess.js b/src/main/resources/templates/public/js/chess.js new file mode 100644 index 0000000000..384e3dbe7e --- /dev/null +++ b/src/main/resources/templates/public/js/chess.js @@ -0,0 +1,139 @@ +window.onload = function () { + const PIECES = { + BLACK_KING: ``, + BLACK_QUEEN: ``, + BLACK_ROOK: ``, + BLACK_BISHOP: ``, + BLACK_KNIGHT: ``, + BLACK_BLACK_PAWN: ``, + WHITE_KING: ``, + WHITE_QUEEN: ``, + WHITE_ROOK: ``, + WHITE_BISHOP: ``, + WHITE_KNIGHT: ``, + WHITE_WHITE_PAWN: ``, + }; + let startPosition = null; + + async function getChessGame() { + const response = await fetch("http://localhost:4567/board"); + return await response.json(); + } + + function setBoard(board) { + document.querySelectorAll(".position").forEach(element => { + while (element.lastElementChild) { + element.removeChild(element.lastElementChild); + } + }); + Object.keys(board) + .filter(position => board[position]["pieceType"] !== "BLANK") + .forEach(position => { + const color = board[position]["color"]; + const pieceType = board[position]["pieceType"]; + document.getElementById(position) + .insertAdjacentHTML("beforeend", PIECES[`${color}_${pieceType}`]); + }); + } + + function setTurn(turn) { + document.getElementById("turn_box") + .innerHTML = '

' + turn["color"] + "의 턴입니다." + '

'; + } + + function setScore(score) { + document.getElementById("score") + .innerHTML = '

' + + "WHITE 팀의 점수는 " + score["WHITE"] + "점 입니다.\n" + + "BLACK 팀의 점수는 " + score["BLACK"] + "점 입니다." + + '

'; + } + + (async function () { + const chessGame = await getChessGame(); + const board = chessGame["boardDto"]["board"]; + const turn = chessGame["turn"]; + const score = chessGame["score"]["scores"]; + + if (chessGame["normalStatus"] === false) { + alert("잘못된 명령입니다."); + return; + } + setBoard(board); + setTurn(turn); + setScore(score); + })(); + + function chooseFirstPosition(position) { + fetch(`http://localhost:4567/source?source=${position}`, {method: "POST"}) + .then(res => res.json()) + .then(data => { + startPosition = data.position; + console.log(data.normalStatus); + if (data.normalStatus === false) { + alert(data.exception); + startPosition = null; + return; + } + const positions = data.movable; + positions.forEach(position => { + document.getElementById(position) + .style + .backgroundColor = "rgba(255, 100, 0, 0.2)"; + }); + + document.getElementById(position) + .style + .backgroundColor = "rgba(255, 200, 0, 0.2)"; + }); + } + + function chooseSecondPosition(position) { + fetch(`http://localhost:4567/destination?destination=${position}`, {method: "POST"}) + .then(res => res.json()) + .then(data => { + if (data.normalStatus === false) { + alert(data.exception); + return; + } + const source = startPosition; + const destination = data.position; + startPosition = null; + + if (source === destination) { + alert("이동을 취소합니다."); + } + + post_to_url("/board", {"source": source, "destination": destination}); + }); + } + + document.querySelectorAll(".position").forEach( + element => { + element.addEventListener("click", (e) => { + let position = e.currentTarget.id; + e.preventDefault(); + if (startPosition == null) { + chooseFirstPosition(position); + } else { + chooseSecondPosition(position); + } + }); + } + ); + + function post_to_url(path, params) { + let form = document.createElement("form"); + form.setAttribute("method", "post"); + form.setAttribute("action", path); + for (let key in params) { + let hiddenField = document.createElement("input"); + hiddenField.setAttribute("type", "hidden"); + hiddenField.setAttribute("name", key); + hiddenField.setAttribute("value", params[key]); + form.appendChild(hiddenField); + } + document.body.appendChild(form); + form.submit(); + } +}; \ No newline at end of file diff --git a/src/main/resources/templates/result.html b/src/main/resources/templates/result.html new file mode 100644 index 0000000000..bca140c300 --- /dev/null +++ b/src/main/resources/templates/result.html @@ -0,0 +1,16 @@ + + + + + Chess Game Result + + + +
+

{{winner}}팀 승리!

+
+ + + \ No newline at end of file diff --git a/src/test/java/wooteco/chess/ChessApplicationTests.java b/src/test/java/wooteco/chess/ChessApplicationTests.java deleted file mode 100644 index 27559d30f6..0000000000 --- a/src/test/java/wooteco/chess/ChessApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package wooteco.chess; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ChessApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/wooteco/chess/domain/game/ScoreResultTest.java b/src/test/java/wooteco/chess/domain/game/ScoreResultTest.java new file mode 100644 index 0000000000..757bb81b00 --- /dev/null +++ b/src/test/java/wooteco/chess/domain/game/ScoreResultTest.java @@ -0,0 +1,33 @@ +package wooteco.chess.domain.game; + +import wooteco.chess.domain.piece.*; +import wooteco.chess.domain.piece.pieces.Pieces; +import wooteco.chess.domain.piece.pieces.TestPiecesFactory; +import wooteco.chess.domain.position.PositionFactory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ScoreResultTest { + @DisplayName("getScoreBy 체스판에 따른 점수 반환 테스트") + @Test + void getScoreBy_normal_test() { + Pieces pieces = TestPiecesFactory.createBy(Arrays.asList( + TestPieceFactory.createQueen(PositionFactory.of("a1"), Color.WHITE), + TestPieceFactory.createRook(PositionFactory.of("b1"),Color.WHITE), + TestPieceFactory.createKnight(PositionFactory.of("c1"), Color.WHITE), + TestPieceFactory.createBishop(PositionFactory.of("d1"), Color.WHITE), + TestPieceFactory.createQueen(PositionFactory.of("a8"), Color.BLACK), + TestPieceFactory.createRook(PositionFactory.of("b8"), Color.BLACK), + TestPieceFactory.createKnight(PositionFactory.of("c8"), Color.BLACK), + TestPieceFactory.createKing(PositionFactory.of("d8"), Color.BLACK) + )); + ScoreResult scoreResult = new ScoreResult(pieces); + + assertThat(scoreResult.getScoreBy(Color.WHITE)).isEqualTo(19.5); + assertThat(scoreResult.getScoreBy(Color.BLACK)).isEqualTo(16.5); + } +} diff --git a/src/test/java/wooteco/chess/domain/game/TurnTest.java b/src/test/java/wooteco/chess/domain/game/TurnTest.java new file mode 100644 index 0000000000..5601986903 --- /dev/null +++ b/src/test/java/wooteco/chess/domain/game/TurnTest.java @@ -0,0 +1,35 @@ +package wooteco.chess.domain.game; + +import wooteco.chess.domain.piece.Color; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TurnTest { + + @DisplayName("turn 생성자 정상 동작 확인") + @Test + void create_normal_test() { + assertThat(new Turn(Color.WHITE)).isInstanceOf(Turn.class); + } + + @DisplayName("turn 생성자에 NONE 컬러가 들어올 시 예외처리") + @Test + void create_when_color_none_throw_exception() { + assertThatThrownBy(() -> new Turn(Color.NONE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("턴은 BLACK이나 WHITE로만 시작할 수 있습니다."); + } + + @DisplayName("change WHITE일시 BLACK을 반환") + @Test + void change_when_white_change_to_black() { + Turn turn = new Turn(Color.WHITE); + turn = turn.change(); + assertEquals(turn.getColor(), Color.BLACK); + } + +} \ No newline at end of file diff --git a/src/test/java/wooteco/chess/domain/piece/BishopTest.java b/src/test/java/wooteco/chess/domain/piece/BishopTest.java new file mode 100644 index 0000000000..6e6a909953 --- /dev/null +++ b/src/test/java/wooteco/chess/domain/piece/BishopTest.java @@ -0,0 +1,66 @@ +package wooteco.chess.domain.piece; + +import wooteco.chess.domain.piece.pieces.Pieces; +import wooteco.chess.domain.piece.pieces.TestPiecesFactory; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.PositionFactory; +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.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BishopTest { + @DisplayName("move 유효한 position입력시 정상 동작") + @ParameterizedTest + @ValueSource(strings = {"a1", "b2", "c3", "e5", "f6", "g7", "h8", "g1", "f2", "e3", "c5", "b6", "a7"}) + void move_normal_test(String input) { + Position position = PositionFactory.of("d4"); + Piece bishop = TestPieceFactory.createBishop(position, Color.WHITE); + + assertThat(bishop.createMovablePositions(Collections.emptyList()).getPositions()).contains(PositionFactory.of(input)); + } + + @DisplayName("move 코너 유효한 position입력시 정상 동작") + @ParameterizedTest + @ValueSource(strings = {"b2", "c3", "d4", "e5", "f6", "g7", "h8"}) + void move_normal_corner_test(String input) { + Position position = PositionFactory.of("a1"); + Piece bishop = TestPieceFactory.createBishop(position, Color.WHITE); + + assertThat(bishop.createMovablePositions(Collections.emptyList()).getPositions()).contains(PositionFactory.of(input)); + } + + @DisplayName("createMovablePositions 아군 말이 경로를 막고있는 경우 갈 수 있는 Position의 개수 반환 테스트") + @Test + void createMovablePositions_blocking_count_test() { + Position position = PositionFactory.of("d4"); + Piece bishop = TestPieceFactory.createBishop(position, Color.WHITE); + + Pieces pieces = TestPiecesFactory.of(Arrays.asList( + PositionFactory.of("a1"), + PositionFactory.of("c5") + )); + + assertThat(bishop.createMovablePositions(pieces.getPieces()).getPositions()).size().isEqualTo(9); + } + + @DisplayName("createMovablePositions 아군 말이 경로를 막고있는 경우 갈 수 있는 Position 반환 테스트") + @ParameterizedTest + @ValueSource(strings = {"b2", "c3", "e5", "f6", "g7", "h8", "g1", "f2", "e3"}) + void createMovablePositions_blocking_test(String input) { + Position position = PositionFactory.of("d4"); + Piece bishop = TestPieceFactory.createBishop(position, Color.WHITE); + + Pieces pieces = TestPiecesFactory.of(Arrays.asList( + PositionFactory.of("a1"), + PositionFactory.of("c5") + )); + + assertThat(bishop.createMovablePositions(pieces.getPieces()).getPositions()).contains(PositionFactory.of(input)); + } +} diff --git a/src/test/java/wooteco/chess/domain/piece/KingTest.java b/src/test/java/wooteco/chess/domain/piece/KingTest.java new file mode 100644 index 0000000000..787f6637b1 --- /dev/null +++ b/src/test/java/wooteco/chess/domain/piece/KingTest.java @@ -0,0 +1,69 @@ +package wooteco.chess.domain.piece; + +import wooteco.chess.domain.piece.pieces.Pieces; +import wooteco.chess.domain.piece.pieces.TestPiecesFactory; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.PositionFactory; +import wooteco.chess.domain.position.positions.Positions; +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.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KingTest { + + @DisplayName("createMovablePositions 유효한 position입력시 정상 동작") + @ParameterizedTest + @ValueSource(strings = {"a1", "a2", "a3", "b1", "b3", "c1", "c2", "c3"}) + void createMovablePositions_normal_test(String input) { + Position position = PositionFactory.of("b2"); + Piece king = TestPieceFactory.createKing(position, Color.WHITE); + + Positions positions = king.createMovablePositions(Collections.emptyList()); + assertThat(positions.getPositions()).contains(PositionFactory.of(input)); + } + + @DisplayName("createMovablePositions 코너 유효한 position입력시 정상 동작") + @ParameterizedTest + @ValueSource(strings = {"a2", "b1", "b2"}) + void createMovablePositions_normal_corner_test(String input) { + Position position = PositionFactory.of("a1"); + Piece king = TestPieceFactory.createKing(position, Color.WHITE); + + assertThat(king.createMovablePositions(Collections.emptyList()).getPositions()).contains(PositionFactory.of(input)); + } + + @DisplayName("createMovablePositions 아군 말이 경로를 막고있는 경우 갈 수 있는 Position의 개수 반환 테스트") + @Test + void createMovablePositions_blocking_count_test() { + Position position = PositionFactory.of("b2"); + Piece king = TestPieceFactory.createKing(position, Color.WHITE); + + Pieces pieces = TestPiecesFactory.of(Arrays.asList( + PositionFactory.of("a3"), + PositionFactory.of("c1") + )); + + assertThat(king.createMovablePositions(pieces.getPieces()).getPositions().size()).isEqualTo(6); + } + + @DisplayName("createMovablePositions 아군 말이 경로를 막고있는 경우 갈 수 있는 Position 반환 테스트") + @ParameterizedTest + @ValueSource(strings = {"a1", "a2", "b1", "b3", "c2", "c3"}) + void createMovablePositions_blocking_test(String input) { + Position position = PositionFactory.of("b2"); + Piece king = TestPieceFactory.createKing(position, Color.WHITE); + + Pieces pieces = TestPiecesFactory.of(Arrays.asList( + PositionFactory.of("a3"), + PositionFactory.of("c1") + )); + + assertThat(king.createMovablePositions(pieces.getPieces()).getPositions()).contains(PositionFactory.of(input)); + } +} diff --git a/src/test/java/wooteco/chess/domain/piece/KnightTest.java b/src/test/java/wooteco/chess/domain/piece/KnightTest.java new file mode 100644 index 0000000000..c69e3deb9d --- /dev/null +++ b/src/test/java/wooteco/chess/domain/piece/KnightTest.java @@ -0,0 +1,67 @@ +package wooteco.chess.domain.piece; + +import wooteco.chess.domain.piece.pieces.Pieces; +import wooteco.chess.domain.piece.pieces.TestPiecesFactory; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.PositionFactory; +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.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KnightTest { + + @DisplayName("createMovablePositions 유효한 position입력시 정상 동작") + @ParameterizedTest + @ValueSource(strings = {"a2", "a4", "b1", "b5", "d1", "d5", "e2", "e4"}) + void createMovablePositions_normal_test(String input) { + Position position = PositionFactory.of("c3"); + Piece knight = TestPieceFactory.createKnight(position, Color.WHITE); + + assertThat(knight.createMovablePositions(Collections.emptyList()).getPositions()).contains(PositionFactory.of(input)); + } + + @DisplayName("createMovablePositions 유효한 코너 position입력시 정상 동작") + @ParameterizedTest + @ValueSource(strings = {"b3", "c2"}) + void createMovablePositions_corner_test(String input) { + Position position = PositionFactory.of("a1"); + Piece knight = TestPieceFactory.createKnight(position, Color.WHITE); + + assertThat(knight.createMovablePositions(Collections.emptyList()).getPositions()).contains(PositionFactory.of(input)); + } + + @DisplayName("createMovablePositions 아군 말이 경로를 막고있는 경우 갈 수 있는 Position의 개수 반환 테스트") + @Test + void createMovablePositions_blocking_count_test() { + Position position = PositionFactory.of("c3"); + Piece knight = TestPieceFactory.createKnight(position, Color.WHITE); + + Pieces pieces = TestPiecesFactory.of(Arrays.asList( + PositionFactory.of("a2"), + PositionFactory.of("e4") + )); + + assertThat(knight.createMovablePositions(pieces.getPieces()).getPositions().size()).isEqualTo(6); + } + + @DisplayName("createMovablePositions 아군 말이 경로를 막고있는 경우 갈 수 있는 Position 반환 테스트") + @ParameterizedTest + @ValueSource(strings = {"a4", "b1", "b5", "d1", "d5", "e2"}) + void createMovablePositions_blocking_test(String input) { + Position position = PositionFactory.of("c3"); + Piece knight = TestPieceFactory.createKnight(position, Color.WHITE); + + Pieces pieces = TestPiecesFactory.of(Arrays.asList( + PositionFactory.of("a2"), + PositionFactory.of("e4") + )); + + assertThat(knight.createMovablePositions(pieces.getPieces()).getPositions()).contains(PositionFactory.of(input)); + } +} diff --git a/src/test/java/wooteco/chess/domain/piece/PawnTest.java b/src/test/java/wooteco/chess/domain/piece/PawnTest.java new file mode 100644 index 0000000000..d83ecbb0ff --- /dev/null +++ b/src/test/java/wooteco/chess/domain/piece/PawnTest.java @@ -0,0 +1,78 @@ +package wooteco.chess.domain.piece; + +import wooteco.chess.domain.piece.pieces.Pieces; +import wooteco.chess.domain.piece.pieces.TestPiecesFactory; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.PositionFactory; +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.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PawnTest { + + @DisplayName("createMovablePositions 유효한 position입력시 정상 동작") + @ParameterizedTest + @ValueSource(strings = {"a3", "b3", "c3"}) + void createMovablePositions_normal_test(String input) { + Position position = PositionFactory.of("b2"); + Piece pawn = TestPieceFactory.createPawn(position, Color.WHITE); + + Pieces pieces = TestPiecesFactory.of(Arrays.asList( + PositionFactory.of("a3"), + PositionFactory.of("c3") + ), Color.BLACK); + + assertThat(pawn.createMovablePositions(pieces.getPieces()).getPositions()).contains(PositionFactory.of(input)); + } + + @DisplayName("createMovablePositions 코너 유효한 position입력시 정상 동작") + @ParameterizedTest + @ValueSource(strings = {"a2", "b2"}) + void createMovablePositions_normal_corner_test(String input) { + Position position = PositionFactory.of("a1"); + Piece pawn = TestPieceFactory.createPawn(position, Color.WHITE); + + Pieces pieces = TestPiecesFactory.of(Arrays.asList( + PositionFactory.of("b2"), + PositionFactory.of("c3") + ), Color.BLACK); + + assertThat(pawn.createMovablePositions(pieces.getPieces()).getPositions()).contains(PositionFactory.of(input)); + } + + @DisplayName("createMovablePositions 이동 가능한 경로에 처음이고 아무 말도 없을시 직진 두 칸과 한 칸 가능") + @Test + void createMovablePositions_initial_all_empty_test() { + Position position = PositionFactory.of("b2"); + Piece pawn = TestPieceFactory.createPawn(position, Color.WHITE); + + assertThat(pawn.createMovablePositions(Collections.emptyList()).getPositions().size()).isEqualTo(2); + } + + @DisplayName("createMovablePositions 이동 가능한 경로에 처음이 아니고 아무 말도 없을시 직진 한 칸만 가능") + @Test + void createMovablePositions_not_initial_all_empty_test() { + Position position = PositionFactory.of("b4"); + Piece pawn = TestPieceFactory.createPawn(position, Color.WHITE); + + assertThat(pawn.createMovablePositions(Collections.emptyList()).getPositions().size()).isEqualTo(1); + } + + @DisplayName("createMovablePositions 이동 가능한 경로에 처음이고 바로 앞에 말이 있을시 이동 불가") + @Test + void createMovablePositions_initial_blocked_test() { + Position position = PositionFactory.of("b2"); + Piece pawn = TestPieceFactory.createPawn(position, Color.WHITE); + Pieces pieces = TestPiecesFactory.of(Collections.singletonList( + PositionFactory.of("b3") + ), Color.BLACK); + + assertThat(pawn.createMovablePositions(pieces.getPieces()).getPositions().size()).isEqualTo(0); + } +} diff --git a/src/test/java/wooteco/chess/domain/piece/PieceTest.java b/src/test/java/wooteco/chess/domain/piece/PieceTest.java new file mode 100644 index 0000000000..867a785a8c --- /dev/null +++ b/src/test/java/wooteco/chess/domain/piece/PieceTest.java @@ -0,0 +1,29 @@ +package wooteco.chess.domain.piece; + +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.PositionFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PieceTest { + + private Position position; + private Piece piece; + + @BeforeEach + void setUp() { + position = PositionFactory.of("a4"); + piece = TestPieceFactory.createKing(position, Color.WHITE); + } + + @DisplayName("move piece 원래 position으로 이동하려하면 예외처리") + @Test + void move_when_same_position_throw_exception() { + assertThatThrownBy(() -> piece.move(position)) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessage("같은 위치로 이동할 수 없습니다."); + } +} \ No newline at end of file diff --git a/src/test/java/wooteco/chess/domain/piece/QueenTest.java b/src/test/java/wooteco/chess/domain/piece/QueenTest.java new file mode 100644 index 0000000000..62f3f5a487 --- /dev/null +++ b/src/test/java/wooteco/chess/domain/piece/QueenTest.java @@ -0,0 +1,76 @@ +package wooteco.chess.domain.piece; + +import wooteco.chess.domain.piece.pieces.Pieces; +import wooteco.chess.domain.piece.pieces.TestPiecesFactory; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.PositionFactory; +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.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +public class QueenTest { + + @DisplayName("createMovablePositions 유효한 position입력시 정상 동작") + @ParameterizedTest + @ValueSource(strings = {"a1", "b2", "c3", "e5", "f6", "g7", "h8", "g1", + "f2", "e3", "c5", "b6", "a7", "a4", "b4", "c4", "e4", "f4", "g4", + "h4", "d1", "d2", "d3", "d5", "d6", "d7", "d8"}) + void move_normal_test(String input) { + Position position = PositionFactory.of("d4"); + Piece queen = TestPieceFactory.createQueen(position, Color.WHITE); + + assertThat(queen.createMovablePositions(Collections.emptyList()).getPositions()).contains(PositionFactory.of(input)); + } + + @DisplayName("createMovablePositions 코너 유효한 position입력시 정상 동작") + @ParameterizedTest + @ValueSource(strings = {"a2", "a3", "a4", "a5", "a6", "a7", "a8", "b1", "c1", "d1", "e1", "f1", "g1", "h1", "b2", "c3", "d4", "e5", "f6", "g7", "h8"}) + void move_normal_corner_test(String input) { + Position position = PositionFactory.of("a1"); + Piece queen = TestPieceFactory.createQueen(position, Color.WHITE); + + assertThat(queen.createMovablePositions(Collections.emptyList()).getPositions()).contains(PositionFactory.of(input)); + } + + @DisplayName("createMovablePositions 아군 말이 경로를 막고있는 경우 갈 수 있는 Position의 개수 반환 테스트") + @Test + void createMovablePositions_blocking_count_test() { + Position position = PositionFactory.of("d4"); + Piece queen = TestPieceFactory.createQueen(position, Color.WHITE); + + Pieces pieces = TestPiecesFactory.of(Arrays.asList( + PositionFactory.of("b2"), + PositionFactory.of("c3"), + PositionFactory.of("e5"), + PositionFactory.of("f6"), + PositionFactory.of("d5") + )); + + assertThat(queen.createMovablePositions(pieces.getPieces()).getPositions().size()).isEqualTo(16); + } + + @DisplayName("createMovablePositions 아군 말이 경로를 막고있는 경우 갈 수 있는 Position 반환 테스트") + @ParameterizedTest + @ValueSource(strings = {"g1", "f2", "e3", "c5", "b6", "a7", "a4", "b4", "c4", "e4", + "f4", "g4", "h4", "d1", "d2", "d3"}) + void createMovablePositions_blocking_test(String input) { + Position position = PositionFactory.of("d4"); + Piece queen = TestPieceFactory.createQueen(position, Color.WHITE); + + Pieces pieces = TestPiecesFactory.of(Arrays.asList( + PositionFactory.of("b2"), + PositionFactory.of("c3"), + PositionFactory.of("e5"), + PositionFactory.of("f6"), + PositionFactory.of("d5") + )); + + assertThat(queen.createMovablePositions(pieces.getPieces()).getPositions()).contains(PositionFactory.of(input)); + } +} diff --git a/src/test/java/wooteco/chess/domain/piece/RookTest.java b/src/test/java/wooteco/chess/domain/piece/RookTest.java new file mode 100644 index 0000000000..eb2753bee2 --- /dev/null +++ b/src/test/java/wooteco/chess/domain/piece/RookTest.java @@ -0,0 +1,67 @@ +package wooteco.chess.domain.piece; + +import wooteco.chess.domain.piece.pieces.Pieces; +import wooteco.chess.domain.piece.pieces.TestPiecesFactory; +import wooteco.chess.domain.position.Position; +import wooteco.chess.domain.position.PositionFactory; +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.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RookTest { + + @DisplayName("createMovablePositions 유효한 position입력시 정상 동작") + @ParameterizedTest + @ValueSource(strings = {"c1", "c2", "c4", "c5", "c6", "c7", "c8", "a3", "b3", "d3", "e3", "f3", "g3", "h3"}) + void move_normal_test(String input) { + Position position = PositionFactory.of("c3"); + Piece rook = TestPieceFactory.createRook(position, Color.WHITE); + + assertThat(rook.createMovablePositions(Collections.emptyList()).getPositions()).contains(PositionFactory.of(input)); + } + + @DisplayName("createMovablePositions 코너 유효한 position입력시 정상 동작") + @ParameterizedTest + @ValueSource(strings = {"a2", "a3", "a4", "a5", "a6", "a7", "a8", "b1", "c1", "d1", "e1", "f1", "g1", "h1"}) + void move_normal_corner_test(String input) { + Position position = PositionFactory.of("a1"); + Piece rook = TestPieceFactory.createRook(position, Color.WHITE); + + assertThat(rook.createMovablePositions(Collections.emptyList()).getPositions()).contains(PositionFactory.of(input)); + } + + @DisplayName("createMovablePositions 아군 말이 경로를 막고있는 경우 갈 수 있는 Position의 개수 반환 테스트") + @Test + void createMovablePositions_blocking_count_test() { + Position position = PositionFactory.of("c3"); + Piece rook = TestPieceFactory.createRook(position, Color.WHITE); + + Pieces pieces = TestPiecesFactory.of(Arrays.asList( + PositionFactory.of("c1"), + PositionFactory.of("b3") + )); + + assertThat(rook.createMovablePositions(pieces.getPieces()).getPositions()).size().isEqualTo(11); + } + + @DisplayName("createMovablePositions 아군 말이 경로를 막고있는 경우 갈 수 있는 Position 반환 테스트") + @ParameterizedTest + @ValueSource(strings = {"c2", "c4", "c5", "c6", "c7", "c8", "d3", "e3", "f3", "g3", "h3"}) + void createMovablePositions_blocking_test(String input) { + Position position = PositionFactory.of("c3"); + Piece rook = TestPieceFactory.createRook(position, Color.WHITE); + + Pieces pieces = TestPiecesFactory.of(Arrays.asList( + PositionFactory.of("c1"), + PositionFactory.of("b3") + )); + + assertThat(rook.createMovablePositions(pieces.getPieces()).getPositions()).contains(PositionFactory.of(input)); + } +} diff --git a/src/test/java/wooteco/chess/domain/piece/TestPieceFactory.java b/src/test/java/wooteco/chess/domain/piece/TestPieceFactory.java new file mode 100644 index 0000000000..5beaac76f3 --- /dev/null +++ b/src/test/java/wooteco/chess/domain/piece/TestPieceFactory.java @@ -0,0 +1,32 @@ +package wooteco.chess.domain.piece; + +import wooteco.chess.domain.position.Position; + +public class TestPieceFactory { + public static Piece createKing(Position position, Color color) { + return new Piece(position, PieceType.KING, color); + } + + public static Piece createQueen(Position position, Color color) { + return new Piece(position, PieceType.QUEEN, color); + } + + public static Piece createKnight(Position position, Color color) { + return new Piece(position, PieceType.KNIGHT, color); + } + + public static Piece createRook(Position position, Color color) { + return new Piece(position, PieceType.ROOK, color); + } + + public static Piece createBishop(Position position, Color color) { + return new Piece(position, PieceType.BISHOP, color); + } + + public static Piece createPawn(Position position, Color color) { + if (color.isWhite()) { + return new Piece(position, PieceType.WHITE_PAWN, color); + } + return new Piece(position, PieceType.BLACK_PAWN, color); + } +} diff --git a/src/test/java/wooteco/chess/domain/piece/pieces/PiecesTest.java b/src/test/java/wooteco/chess/domain/piece/pieces/PiecesTest.java new file mode 100644 index 0000000000..88b2a58055 --- /dev/null +++ b/src/test/java/wooteco/chess/domain/piece/pieces/PiecesTest.java @@ -0,0 +1,31 @@ +package wooteco.chess.domain.piece.pieces; + +import wooteco.chess.domain.piece.Color; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PiecesTest { + + @DisplayName("isKingDead 모든 킹이 살아있다면 false 반환") + @Test + void isKingDead_all_king_alive_return_false() { + Pieces pieces = PiecesInitializer.operate(); + assertThat(pieces.isKingDead()).isFalse(); + } + + @DisplayName("isKingDead 킹이 하나만 살아있다면 true 반환") + @Test + void isKingDead_one_king_alive_return_true() { + Pieces pieces = TestPiecesFactory.createOnlyWhite(); + assertThat(pieces.isKingDead()).isTrue(); + } + + @DisplayName("isKingDead 하얀 킹 하나만 살아있다면 WHITE 반환") + @Test + void getAliveKingColor_white_king_alive_return_white() { + Pieces pieces = TestPiecesFactory.createOnlyWhite(); + assertThat(pieces.getAliveKingColor()).isEqualTo(Color.WHITE); + } +} \ No newline at end of file diff --git a/src/test/java/wooteco/chess/domain/piece/pieces/TestPiecesFactory.java b/src/test/java/wooteco/chess/domain/piece/pieces/TestPiecesFactory.java new file mode 100644 index 0000000000..2efb2d97c8 --- /dev/null +++ b/src/test/java/wooteco/chess/domain/piece/pieces/TestPiecesFactory.java @@ -0,0 +1,43 @@ +package wooteco.chess.domain.piece.pieces; + +import wooteco.chess.domain.piece.Color; +import wooteco.chess.domain.piece.Piece; +import wooteco.chess.domain.piece.TestPieceFactory; +import wooteco.chess.domain.position.Position; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class TestPiecesFactory { + public static Pieces of(List positions) { + List pieces = new ArrayList<>(); + + for (Position position : positions) { + pieces.add(TestPieceFactory.createRook(position, Color.WHITE)); + } + return new Pieces(pieces); + } + + public static Pieces of(List positions, Color color) { + List pieces = new ArrayList<>(); + + for (Position position : positions) { + pieces.add(TestPieceFactory.createRook(position, color)); + } + return new Pieces(pieces); + } + + public static Pieces createBy(List inputPieces) { + List pieces = new ArrayList<>(inputPieces); + return new Pieces(pieces); + } + + public static Pieces createOnlyWhite() { + List pieces = PiecesInitializer.operate().getPieces().stream() + .filter(piece -> piece.getColor().isWhite()) + .collect(Collectors.toList()); + + return new Pieces(pieces); + } +} diff --git a/src/test/java/wooteco/chess/domain/position/PositionFactoryTest.java b/src/test/java/wooteco/chess/domain/position/PositionFactoryTest.java new file mode 100644 index 0000000000..c692cabd57 --- /dev/null +++ b/src/test/java/wooteco/chess/domain/position/PositionFactoryTest.java @@ -0,0 +1,30 @@ +package wooteco.chess.domain.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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PositionFactoryTest { + + @DisplayName("of 유효한 입력시 해당 Position 반환 테스트") + @Test + void of_when_valid_input_return_correct_position() { + Position position1 = PositionFactory.of("a1"); + Position position2 = PositionFactory.of("a1"); + + assertThat(position1).isEqualTo(position2); + } + + @DisplayName("of 유효하지 않은 입력시 예외처리 테스트") + @ParameterizedTest + @ValueSource(strings = {"aaa", "123", "a9", "z1"}) + void of_when_invalid_input_throw_exception(String input) { + assertThatThrownBy(() -> PositionFactory.of(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("옳지 않은 좌표 입력입니다."); + } +} \ No newline at end of file diff --git a/src/test/java/wooteco/chess/domain/position/PositionTest.java b/src/test/java/wooteco/chess/domain/position/PositionTest.java new file mode 100644 index 0000000000..30f4466bf9 --- /dev/null +++ b/src/test/java/wooteco/chess/domain/position/PositionTest.java @@ -0,0 +1,47 @@ +package wooteco.chess.domain.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.EmptySource; +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 PositionTest { + + @DisplayName("Position 생성자 작동 테스트") + @Test + void create_normal_constructor() { + assertThat(new Position("a1")).isInstanceOf(Position.class); + } + + @DisplayName("Position 생성자 Null 입력 예외 테스트") + @ParameterizedTest + @NullSource + void create_null_exception(String nullInput) { + assertThatThrownBy(() -> new Position(nullInput)) + .isInstanceOf(NullPointerException.class) + .hasMessage("옳지 않은 좌표 입력입니다."); + } + + @DisplayName("Position 생성자 빈 문자열 입력 예외 테스트") + @ParameterizedTest + @EmptySource + void create_empty_exception(String emptyInput) { + assertThatThrownBy(() -> new Position(emptyInput)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("옳지 않은 좌표 입력입니다."); + } + + @DisplayName("Position 생성자 형식에서 벗어나는 입력 예외 테스트") + @ParameterizedTest + @ValueSource(strings = {"aaa", "123", "a9", "z1"}) + void create_invalid_exception(String invalidInput) { + assertThatThrownBy(() -> new Position(invalidInput)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("옳지 않은 좌표 입력입니다."); + } +} \ No newline at end of file diff --git a/src/test/java/wooteco/chess/web/FakeMovingPositionDaoTest.java b/src/test/java/wooteco/chess/web/FakeMovingPositionDaoTest.java new file mode 100644 index 0000000000..8d1902a529 --- /dev/null +++ b/src/test/java/wooteco/chess/web/FakeMovingPositionDaoTest.java @@ -0,0 +1,42 @@ +package wooteco.chess.web; + +import wooteco.chess.dao.FakeHistoryDao; +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.junit.jupiter.api.Assertions.assertTrue; + +class FakeMovingPositionDaoTest { + + @DisplayName("selectAll 동작 확인") + @ParameterizedTest + @CsvSource(value = {"1,a2,a4", "2,a7,a6", "3,a4,a5", "4,b7,b5"}) + void selectAll_normal_test(int iter, String start, String end) { + FakeHistoryDao fakeHistoryDao = new FakeHistoryDao(); + assertThat(fakeHistoryDao.selectAll().get(iter).getStart()).isEqualTo(start); + assertThat(fakeHistoryDao.selectAll().get(iter).getEnd()).isEqualTo(end); + + } + + @DisplayName("clear 동작 확인") + @Test + public void clear_normal_test() { + FakeHistoryDao fakeHistoryDao = new FakeHistoryDao(); + fakeHistoryDao.clear(); + assertTrue(fakeHistoryDao.selectAll().isEmpty()); + } + + @DisplayName("insert 동작 확인") + @ParameterizedTest + @CsvSource(value = {"1,a2,a4", "2,a7,a6", "3,a4,a5", "4,b7,b5", "5,b2,b4"}) + void insert_normal_test(int iter, String start, String end) { + FakeHistoryDao fakeHistoryDao = new FakeHistoryDao(); + fakeHistoryDao.insert("b2", "b4"); + assertThat(fakeHistoryDao.selectAll().get(iter).getStart()).isEqualTo(start); + assertThat(fakeHistoryDao.selectAll().get(iter).getEnd()).isEqualTo(end); + + } +} \ No newline at end of file