diff --git a/.gitignore b/.gitignore index 6c01878138..44c3140a8c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ out/ ### VS Code ### .vscode/ +src/main/resources/application.properties \ No newline at end of file diff --git a/build.gradle b/build.gradle index ce846f70cc..31eda7379c 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,9 @@ dependencies { testImplementation platform('org.assertj:assertj-bom:3.27.3') testImplementation('org.junit.jupiter:junit-jupiter') testImplementation('org.assertj:assertj-core') + + implementation 'com.mysql:mysql-connector-j:9.3.0' + testImplementation 'com.h2database:h2:2.2.224' } java { diff --git a/init.sql b/init.sql new file mode 100644 index 0000000000..7294506328 --- /dev/null +++ b/init.sql @@ -0,0 +1,20 @@ +-- 1. 게임 테이블 (메타데이터) +CREATE TABLE game ( + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE, + status VARCHAR(20) NOT NULL, -- PLAYING, CHO_WIN, HAN_WIN, DRAW + current_turn VARCHAR(10) NOT NULL -- CHO, HAN +); + +-- 2. 기물 상태 테이블 (1번 테이블을 참조) +CREATE TABLE piece ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + game_id VARCHAR(36) NOT NULL, -- 어떤 게임판에 속해있는지 (FK) + piece_name VARCHAR(20) NOT NULL, -- CHO_SOLDIER, HAN_CHARIOT 등 + camp VARCHAR(3) NOT NULL, + row_index INT NOT NULL, -- Y 좌표 (0~9) + column_index INT NOT NULL, -- X 좌표 (0~8) + + + FOREIGN KEY (game_id) REFERENCES game(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/src/main/java/janggi/config/AppConfig.java b/src/main/java/janggi/config/AppConfig.java index 7c14ecd5b0..80f28de66f 100644 --- a/src/main/java/janggi/config/AppConfig.java +++ b/src/main/java/janggi/config/AppConfig.java @@ -1,11 +1,58 @@ package janggi.config; import janggi.controller.JanggiController; +import janggi.persistence.GameRepositoryImpl; +import janggi.persistence.dao.GameDao; +import janggi.persistence.dao.JdbcGameDao; +import janggi.persistence.dao.JdbcPieceDao; +import janggi.persistence.dao.PieceDao; +import janggi.persistence.mapper.GameMapper; +import janggi.persistence.mapper.PieceMapper; +import janggi.service.GameRepository; +import janggi.service.JanggiService; import janggi.view.InputView; import janggi.view.OutputView; public class AppConfig { + private final ConnectionPool connectionPool = DatabaseConfig.getPool(); + public JanggiController janggiController() { - return new JanggiController(new InputView(), new OutputView()); + return new JanggiController(inputView(), outputView(), janggiService()); + } + + public InputView inputView() { + return new InputView(); + } + + public OutputView outputView() { + return new OutputView(); + } + + public JanggiService janggiService() { + return new JanggiService(gameRepository(), transactionManager()); + } + + public TransactionManager transactionManager() { + return new TransactionManager(connectionPool); + } + + public GameRepository gameRepository() { + return new GameRepositoryImpl(gameDao(), pieceDao(), gameMapper(), pieceMapper()); + } + + public GameDao gameDao() { + return new JdbcGameDao(connectionPool); + } + + public PieceDao pieceDao() { + return new JdbcPieceDao(connectionPool); + } + + public GameMapper gameMapper() { + return new GameMapper(); + } + + public PieceMapper pieceMapper() { + return new PieceMapper(); } } diff --git a/src/main/java/janggi/config/ConnectionPool.java b/src/main/java/janggi/config/ConnectionPool.java new file mode 100644 index 0000000000..0fd0703f5a --- /dev/null +++ b/src/main/java/janggi/config/ConnectionPool.java @@ -0,0 +1,99 @@ +package janggi.config; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +public class ConnectionPool { + private final BlockingQueue pool; + private final String url; + private final String username; + private final String password; + private final long timeoutMillis; + + private final int poolSize; + + ConnectionPool(String url, String username, String password, int poolSize) { + this.url = url; + this.username = username; + this.password = password; + this.timeoutMillis = 5000; + this.poolSize = poolSize; + + this.pool = new ArrayBlockingQueue<>(this.poolSize); + + try { + for (int i = 0; i < this.poolSize; i++) { + pool.offer(createConnection()); + } + } catch (SQLException e) { + throw new RuntimeException("커넥션 풀 초기화 실패", e); + } + } + + private Connection createConnection() throws SQLException { + return DriverManager.getConnection(url, username, password); + } + + public Connection getConnection() { + try { + Connection conn = pool.poll(timeoutMillis, TimeUnit.MILLISECONDS); + if (conn == null) { + throw new RuntimeException("커넥션 획득 대기 시간 초과 (Timeout)"); + } + + if (!conn.isValid(1)) { + try { + conn.close(); + } catch (SQLException ignored) {} + conn = createConnection(); + } + + return conn; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("커넥션 대기 중 인터럽트 발생", e); + } catch (SQLException e) { + throw new RuntimeException("유효하지 않은 커넥션 재설정 및 검증 실패", e); + } + } + + public void release(Connection conn) { + if (conn != null) { + try { + if (!conn.isClosed()) { + conn.setAutoCommit(true); + pool.offer(conn); + } + } catch (SQLException e) { + try { + conn.close(); + } catch (SQLException ignored) {} + } + } + } + + public void shutdown() { + Connection conn; + while ((conn = pool.poll()) != null) { + try { + conn.close(); + } catch (SQLException ignored) {} + } + } + + public PooledConnection getPooledConnection() { + return new PooledConnection(getConnection(), this); + } + + public int getAvailableCount() { + return pool.size(); + } + + public int getActiveCount() { + return poolSize - pool.size(); + } +} \ No newline at end of file diff --git a/src/main/java/janggi/config/DatabaseConfig.java b/src/main/java/janggi/config/DatabaseConfig.java new file mode 100644 index 0000000000..1cdfd37f30 --- /dev/null +++ b/src/main/java/janggi/config/DatabaseConfig.java @@ -0,0 +1,42 @@ +package janggi.config; + +import java.io.InputStream; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +public class DatabaseConfig { + private static final Properties properties = new Properties(); + private static final ConnectionPool connectionPool; + + static { + try (InputStream input = DatabaseConfig.class.getResourceAsStream("/application.properties")) { + if (input == null) { + throw new RuntimeException("application.properties 파일을 찾을 수 없습니다."); + } + properties.load(input); + } catch (Exception e) { + throw new RuntimeException("JDBC 초기화 실패", e); + } + + connectionPool = new ConnectionPool( + properties.getProperty("db.url"), + properties.getProperty("db.id"), + properties.getProperty("db.password"), + 5 + ); + } + + public static void shutdown() { + connectionPool.shutdown(); + } + + public static PooledConnection getConnection() { + return connectionPool.getPooledConnection(); + } + + public static ConnectionPool getPool() { + return connectionPool; + } +} diff --git a/src/main/java/janggi/config/PooledConnection.java b/src/main/java/janggi/config/PooledConnection.java new file mode 100644 index 0000000000..d360a2a9fb --- /dev/null +++ b/src/main/java/janggi/config/PooledConnection.java @@ -0,0 +1,22 @@ +package janggi.config; + +import java.sql.Connection; + +public class PooledConnection implements AutoCloseable{ + private final Connection conn; + private final ConnectionPool pool; + + PooledConnection(Connection conn, ConnectionPool pool) { + this.conn = conn; + this.pool = pool; + } + + public Connection getConnection() { + return conn; + } + + @Override + public void close() { + pool.release(conn); + } +} diff --git a/src/main/java/janggi/config/TransactionCallback.java b/src/main/java/janggi/config/TransactionCallback.java new file mode 100644 index 0000000000..51cc758127 --- /dev/null +++ b/src/main/java/janggi/config/TransactionCallback.java @@ -0,0 +1,9 @@ +package janggi.config; + +import java.sql.Connection; +import java.sql.SQLException; + +@FunctionalInterface +public interface TransactionCallback { + T run(Connection conn) throws SQLException; +} diff --git a/src/main/java/janggi/config/TransactionManager.java b/src/main/java/janggi/config/TransactionManager.java new file mode 100644 index 0000000000..efb9f4b2d6 --- /dev/null +++ b/src/main/java/janggi/config/TransactionManager.java @@ -0,0 +1,28 @@ +package janggi.config; + +import java.sql.Connection; +import java.sql.SQLException; + +public class TransactionManager { + private final ConnectionPool pool; + + public TransactionManager(ConnectionPool pool) { + this.pool = pool; + } + + public T execute(TransactionCallback callback) { + PooledConnection pooledConn = pool.getPooledConnection(); + Connection conn = pooledConn.getConnection(); + try { + conn.setAutoCommit(false); + T result = callback.run(conn); + conn.commit(); + return result; + } catch (Exception e) { + try { conn.rollback(); } catch (SQLException ignored) {} + throw new RuntimeException("트랜잭션 실행 중 오류가 발생했습니다.", e); + } finally { + pooledConn.close(); + } + } +} diff --git a/src/main/java/janggi/controller/JanggiController.java b/src/main/java/janggi/controller/JanggiController.java index d998157dda..2c374f7b1f 100644 --- a/src/main/java/janggi/controller/JanggiController.java +++ b/src/main/java/janggi/controller/JanggiController.java @@ -1,54 +1,160 @@ package janggi.controller; +import janggi.controller.dto.PositionRequest; import janggi.domain.Janggi; import janggi.domain.position.Position; +import janggi.exception.DuplicateGameException; +import janggi.service.JanggiService; import janggi.view.InputView; import janggi.view.OutputView; -import janggi.view.dto.PositionRequest; +import java.util.List; import java.util.Optional; public class JanggiController { private final InputView inputView; private final OutputView outputView; + private final JanggiService janggiService; - public JanggiController(InputView inputView, OutputView outputView) { + public JanggiController(InputView inputView, OutputView outputView, JanggiService janggiService) { this.inputView = inputView; this.outputView = outputView; + this.janggiService = janggiService; } public void run() { - Janggi janggi = createGame(); + while (true) { + int option = inputView.readMenuOption(); + + if (option == 3) { + deleteGame(); + continue; + } + + if (option == 4) { + return; + } + + Optional gameIdOpt = selectGame(option); + + if (gameIdOpt.isEmpty()) { + continue; + } + + String gameId = gameIdOpt.get(); + + while (janggiService.isRunning(gameId)) { + outputView.printBoard(janggiService.getBoardStatus(gameId), janggiService.getCurrentCamp(gameId)); + playTurn(gameId); + } - while (janggi.isRunning()) { - outputView.printBoard(janggi.getBoard(), janggi.currentCamp()); - playTurn(janggi); + outputView.printGameResult(janggiService.getWinner(gameId)); } } - private Janggi createGame() { - int choFormation = inputView.readFormationChoice(1); - int hanFormation = inputView.readFormationChoice(2); - return Janggi.start(choFormation, hanFormation); + private void deleteGame() { + List names = janggiService.findAllNames(); + Optional gameName = inputView.readGameName(names); + + if (gameName.isEmpty()) { + return; + } + + janggiService.deleteByName(gameName.get()); } - private void playTurn(Janggi janggi) { + private Optional selectGame(int option) { + if (option == 1) { + return createGame(); + } + if (option == 2) { + return loadGame(); + } + return Optional.empty(); + } + + private Optional createGame() { while (true) { + Optional gameName = inputView.readGameName(); + if (gameName.isEmpty()) { + return Optional.empty(); + } + + Optional choFormation = inputView.readFormationChoice("초나라"); + if (choFormation.isEmpty()) { + return Optional.empty(); + } + + Optional hanFormation = inputView.readFormationChoice("한나라"); + if (hanFormation.isEmpty()) { + return Optional.empty(); + } + try { + String gameId = janggiService.createGame( + gameName.get(), + choFormation.get(), + hanFormation.get() + ); + + return Optional.of(gameId); + } catch (DuplicateGameException e) { + System.out.println("[ERROR] " + e.getMessage()); + continue; + + } catch (RuntimeException e) { + System.out.println("[ERROR] 치명적인 시스템 오류가 발생했습니다: " + e.getMessage()); + return Optional.empty(); + } + } + } + + private Optional loadGame() { + List gameNames = janggiService.findAllNames(); + Optional gameNameOpt = inputView.readGameName(gameNames); + + if (gameNameOpt.isEmpty()) { + return Optional.empty(); + } + String gameId= janggiService.findGameByName(gameNameOpt.get()); + return Optional.of(gameId); + } + + private void playTurn(String gameId) { + while (true) { + try { + int command = inputView.readCommand(); + + if (command == 1) { + janggiService.surrender(gameId); + return; + } + if (command == 2) { + if (inputView.confirmDraw()) { + janggiService.draw(gameId); + return; + } + outputView.printErrorMessage("상대가 무승부를 거절했습니다."); + continue; + } + Optional selection = inputView.readPieceSelection(); if (selection.isEmpty()) { continue; } + Position from = selection.get().toPosition(); + janggiService.validateTurn(gameId, from); + Optional destination = inputView.readMoveDestination(); + if (destination.isEmpty()) { continue; } - Position from = Position.of(selection.get().row(), selection.get().column()); - Position to = Position.of(destination.get().row(), destination.get().column()); + Position to = destination.get().toPosition(); - janggi.play(from, to); + janggiService.play(gameId, from, to); break; } catch (IllegalArgumentException e) { outputView.printErrorMessage(e.getMessage()); diff --git a/src/main/java/janggi/controller/dto/FormationRequest.java b/src/main/java/janggi/controller/dto/FormationRequest.java new file mode 100644 index 0000000000..8f14cd9823 --- /dev/null +++ b/src/main/java/janggi/controller/dto/FormationRequest.java @@ -0,0 +1,7 @@ +package janggi.controller.dto; + +public record FormationRequest( + int choFormationNumber, + int hanFormationNumber +) { +} diff --git a/src/main/java/janggi/view/dto/PositionRequest.java b/src/main/java/janggi/controller/dto/PositionRequest.java similarity index 65% rename from src/main/java/janggi/view/dto/PositionRequest.java rename to src/main/java/janggi/controller/dto/PositionRequest.java index 89a63260d2..bf7f6d7367 100644 --- a/src/main/java/janggi/view/dto/PositionRequest.java +++ b/src/main/java/janggi/controller/dto/PositionRequest.java @@ -1,6 +1,11 @@ -package janggi.view.dto; +package janggi.controller.dto; -public record PositionRequest(int row, int column) { +import janggi.domain.position.Position; + +public record PositionRequest( + int row, + int column +) { public static PositionRequest from(String input) { int spaceIndex = input.indexOf(" "); if (spaceIndex == -1) { @@ -10,4 +15,8 @@ public static PositionRequest from(String input) { int column = Integer.parseInt(input.substring(spaceIndex + 1).trim()); return new PositionRequest(row, column); } + + public Position toPosition(){ + return Position.of(this.row, this.column); + } } diff --git a/src/main/java/janggi/domain/Janggi.java b/src/main/java/janggi/domain/Janggi.java index a93a0eb6cb..f3b3782124 100644 --- a/src/main/java/janggi/domain/Janggi.java +++ b/src/main/java/janggi/domain/Janggi.java @@ -1,56 +1,49 @@ package janggi.domain; import janggi.domain.board.Board; -import janggi.domain.board.BoardFactory; -import janggi.domain.board.strategy.ElephantHorseElephantHorse; -import janggi.domain.board.strategy.ElephantHorseHorseElephant; -import janggi.domain.board.strategy.FormationStrategy; -import janggi.domain.board.strategy.HorseElephantElephantHorse; -import janggi.domain.board.strategy.HorseElephantHorseElephant; import janggi.domain.piece.Piece; import janggi.domain.position.Position; -import java.util.List; import java.util.Map; public class Janggi { - private static final List FORMATIONS = List.of( - new HorseElephantElephantHorse(), - new HorseElephantHorseElephant(), - new ElephantHorseHorseElephant(), - new ElephantHorseElephantHorse() - ); - private final Board board; private Camp currentCamp; private boolean running; - private Janggi(Board board) { + private Janggi(Board board, Camp currentCamp, boolean running) { this.board = board; - this.currentCamp = Camp.CHO; - this.running = true; + this.currentCamp = currentCamp; + this.running = running; } - public static Janggi start(int choFormationNumber, int hanFormationNumber) { - return new Janggi(BoardFactory.create( - readFormation(choFormationNumber), - readFormation(hanFormationNumber))); + public Janggi(Janggi original) { + this(original.board.clone(), original.currentCamp, original.running); } - private static FormationStrategy readFormation(int choice) { - if (choice < 1 || choice > FORMATIONS.size()) { - throw new IllegalArgumentException("1~4 중 선택해주세요."); - } - return FORMATIONS.get(choice - 1); + private Janggi(Board board) { + this(board, Camp.CHO, true); + } + + public static Janggi start(Board board) { + return new Janggi(board); + } + + public static Janggi load(Board board, Camp currentCamp, boolean running) { + return new Janggi(board, currentCamp, running); } public void play(Position from, Position to) { validateTurn(from); board.movePiece(from, to); + if (!board.isAliveEssentialPiece(currentCamp.next())) { + finish(); + return; + } currentCamp = currentCamp.next(); } - private void validateTurn(Position from) { + public void validateTurn(Position from) { Piece piece = board.selectPiece(from); if (!piece.isSameCamp(currentCamp)) { throw new IllegalArgumentException("자신의 기물만 선택할 수 있습니다."); @@ -69,6 +62,24 @@ public Camp currentCamp() { return currentCamp; } + public void surrender() { + currentCamp = currentCamp.next(); + finish(); + } + + public void draw() { + currentCamp = board.calculateScoreResult(); + finish(); + } + + public Janggi clone() { + return new Janggi(this); + } + + public Camp winner() { + return currentCamp; + } + public Map getBoard() { return board.janggiBoard(); } diff --git a/src/main/java/janggi/domain/Palace.java b/src/main/java/janggi/domain/Palace.java new file mode 100644 index 0000000000..6871abfa67 --- /dev/null +++ b/src/main/java/janggi/domain/Palace.java @@ -0,0 +1,60 @@ +package janggi.domain; + +import janggi.domain.position.Direction; +import janggi.domain.position.Position; + +import java.util.List; +import java.util.Set; + +public class Palace { + private static final Palace CHO = new Palace( + Position.of(1, 4), + Set.of( + Position.of(0, 3), Position.of(0, 4), Position.of(0, 5), + Position.of(1, 3), Position.of(1, 4), Position.of(1, 5), + Position.of(2, 3), Position.of(2, 4), Position.of(2, 5) + )); + + private static final Palace HAN = new Palace(Position.of(8, 4), + Set.of( + Position.of(7, 3), Position.of(7, 4), Position.of(7, 5), + Position.of(8, 3), Position.of(8, 4), Position.of(8, 5), + Position.of(9, 3), Position.of(9, 4), Position.of(9, 5) + )); + + private final Position center; + private final Set area; + + private Palace(Position center, Set area) { + this.center = center; + this.area = area; + } + + public static Palace cho() { + return CHO; + } + + public static Palace han() { + return HAN; + } + + public boolean contains(Position position) { + return area.contains(position); + } + + public List diagonalDirectionsAt(Position position) { + if (!contains(position)) { + return List.of(); + } + + if (position.equals(center)) { + return Direction.diagonal(); + } + + return Direction.diagonal().stream() + .filter(dir -> position.move(dir) + .map(next -> next.equals(center)) + .orElse(false)) + .toList(); + } +} diff --git a/src/main/java/janggi/domain/Palaces.java b/src/main/java/janggi/domain/Palaces.java new file mode 100644 index 0000000000..2c801c263e --- /dev/null +++ b/src/main/java/janggi/domain/Palaces.java @@ -0,0 +1,37 @@ +package janggi.domain; + +import janggi.domain.position.Direction; +import janggi.domain.position.Position; + +import java.util.List; +import java.util.Optional; + +public class Palaces { + private static final Palaces STANDARD = new Palaces(List.of(Palace.cho(), Palace.han())); + + private final List elements; + + private Palaces(List elements) { + this.elements = elements; + } + + public static Palaces of() { + return STANDARD; + } + + public List diagonalDirectionsAt(Position position) { + return findPalace(position) + .map(palace -> palace.diagonalDirectionsAt(position)) + .orElse(List.of()); + } + + public boolean containsAny(Position position) { + return findPalace(position).isPresent(); + } + + private Optional findPalace(Position position) { + return elements.stream() + .filter(palace -> palace.contains(position)) + .findFirst(); + } +} diff --git a/src/main/java/janggi/domain/Score.java b/src/main/java/janggi/domain/Score.java new file mode 100644 index 0000000000..963b392ce9 --- /dev/null +++ b/src/main/java/janggi/domain/Score.java @@ -0,0 +1,40 @@ +package janggi.domain; + +public class Score { + private final double point; + + public Score(double point) { + validate(point); + this.point = point; + } + + private void validate(double point) { + if (point < 0) { + throw new IllegalArgumentException("점수는 음수가 될 수 없습니다."); + } + } + + public Score plus(Score other) { + return new Score(point + other.point); + } + + public Score multiply(double factor) { + return new Score(this.point * factor); + } + + public boolean isGreaterThan(Score other) { + return point > other.point; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Score score)) return false; + + return Double.compare(point, score.point) == 0; + } + + @Override + public int hashCode() { + return Double.hashCode(point); + } +} diff --git a/src/main/java/janggi/domain/board/Board.java b/src/main/java/janggi/domain/board/Board.java index 12b78d4520..87d1419045 100644 --- a/src/main/java/janggi/domain/board/Board.java +++ b/src/main/java/janggi/domain/board/Board.java @@ -1,11 +1,14 @@ package janggi.domain.board; +import janggi.domain.Camp; import janggi.domain.Path; import janggi.domain.Paths; +import janggi.domain.Score; import janggi.domain.piece.Piece; import janggi.domain.position.Position; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -63,7 +66,37 @@ private void executeMove(Piece piece, Position from, Position to) { janggiBoard.put(to, piece); } + public boolean isAliveEssentialPiece(Camp camp) { + return janggiBoard.values().stream() + .filter(piece -> piece.isSameCamp(camp)) + .anyMatch(Piece::isEssential); + } + + public Board clone() { + Map copiedMap = new HashMap<>(this.janggiBoard); + return new Board(copiedMap); + } + public Map janggiBoard() { return Collections.unmodifiableMap(janggiBoard); } + + public Camp calculateScoreResult() { + Score choScore = janggiBoard.values().stream() + .filter(piece -> piece.isSameCamp(Camp.CHO)) + .map(Piece::score) + .reduce(new Score(0), Score::plus); + + Score hanScore = janggiBoard.values().stream() + .filter(piece -> piece.isSameCamp(Camp.HAN)) + .map(Piece::score) + .reduce(new Score(0), Score::plus) + .multiply(1.5); + + if (choScore.isGreaterThan(hanScore)) { + return Camp.CHO; + } + + return Camp.HAN; + } } diff --git a/src/main/java/janggi/domain/board/BoardFactory.java b/src/main/java/janggi/domain/board/BoardFactory.java index 0120f4bff9..1ce5472d14 100644 --- a/src/main/java/janggi/domain/board/BoardFactory.java +++ b/src/main/java/janggi/domain/board/BoardFactory.java @@ -58,6 +58,11 @@ public static Map placeFormation( return result; } + + public static Board load(Map pieceMap) { + + return new Board(pieceMap); + } } diff --git a/src/main/java/janggi/domain/board/FormationStrategyFactory.java b/src/main/java/janggi/domain/board/FormationStrategyFactory.java new file mode 100644 index 0000000000..fb6e417401 --- /dev/null +++ b/src/main/java/janggi/domain/board/FormationStrategyFactory.java @@ -0,0 +1,25 @@ +package janggi.domain.board; + +import janggi.domain.board.strategy.ElephantHorseElephantHorse; +import janggi.domain.board.strategy.ElephantHorseHorseElephant; +import janggi.domain.board.strategy.FormationStrategy; +import janggi.domain.board.strategy.HorseElephantElephantHorse; +import janggi.domain.board.strategy.HorseElephantHorseElephant; + +import java.util.List; + +public class FormationStrategyFactory { + private static final List FORMATIONS = List.of( + new HorseElephantElephantHorse(), + new HorseElephantHorseElephant(), + new ElephantHorseHorseElephant(), + new ElephantHorseElephantHorse() + ); + + public static FormationStrategy from(int number) { + if (number < 1 || number > FORMATIONS.size()) { + throw new IllegalArgumentException("마상 전략은 1 ~ 4까지만 입력이 가능합니다."); + } + return FORMATIONS.get(number - 1); + } +} diff --git a/src/main/java/janggi/domain/piece/Advisor.java b/src/main/java/janggi/domain/piece/Advisor.java index ccd7edea6f..c3a3666303 100644 --- a/src/main/java/janggi/domain/piece/Advisor.java +++ b/src/main/java/janggi/domain/piece/Advisor.java @@ -1,16 +1,27 @@ package janggi.domain.piece; import janggi.domain.Camp; -import janggi.domain.piece.strategy.PalaceStrategy; +import janggi.domain.Score; +import janggi.domain.piece.strategy.ChoPalaceStrategy; +import janggi.domain.piece.strategy.HanPalaceStrategy; +import janggi.domain.piece.strategy.MoveStrategy; import janggi.domain.position.Position; import java.util.Map; public class Advisor extends Piece { - private static final PalaceStrategy PALACE_STRATEGY = new PalaceStrategy(); + private static final PieceName ADVISOR_NAME = new PieceName("士", "仕"); + private static final Score ADVISOR_SCORE = new Score(3); public Advisor(Camp camp) { - super(camp, PALACE_STRATEGY); + super(camp, createStrategy(camp), ADVISOR_NAME, ADVISOR_SCORE); + } + + private static MoveStrategy createStrategy(Camp camp) { + if (camp.isCho()) { + return ChoPalaceStrategy.getInstance(); + } + return HanPalaceStrategy.getInstance(); } @Override @@ -34,13 +45,7 @@ public boolean canBeCaughtByCannon() { } @Override - public String choDisplayName() { - return "士"; - } - - @Override - public String hanDisplayName() { - return "仕"; + public boolean isEssential() { + return false; } - } diff --git a/src/main/java/janggi/domain/piece/Cannon.java b/src/main/java/janggi/domain/piece/Cannon.java index 65a4e884f4..49697a01ac 100644 --- a/src/main/java/janggi/domain/piece/Cannon.java +++ b/src/main/java/janggi/domain/piece/Cannon.java @@ -1,16 +1,19 @@ package janggi.domain.piece; import janggi.domain.Camp; +import janggi.domain.Palaces; +import janggi.domain.Score; import janggi.domain.piece.strategy.LinearStrategy; import janggi.domain.position.Position; import java.util.Map; public class Cannon extends Piece { - private static final LinearStrategy LINEAR_STRATEGY = new LinearStrategy(); + private static final PieceName CANNON_NAME = new PieceName("包", "砲"); + private static final Score CANNON_SCORE = new Score(7); public Cannon(Camp camp) { - super(camp, LINEAR_STRATEGY); + super(camp, new LinearStrategy(Palaces.of()), CANNON_NAME, CANNON_SCORE); } @Override @@ -44,12 +47,7 @@ public boolean canBeCaughtByCannon() { } @Override - public String choDisplayName() { - return "包"; - } - - @Override - public String hanDisplayName() { - return "砲"; + public boolean isEssential() { + return false; } } diff --git a/src/main/java/janggi/domain/piece/Chariot.java b/src/main/java/janggi/domain/piece/Chariot.java index 7f84f9d582..0e88779cda 100644 --- a/src/main/java/janggi/domain/piece/Chariot.java +++ b/src/main/java/janggi/domain/piece/Chariot.java @@ -1,16 +1,19 @@ package janggi.domain.piece; import janggi.domain.Camp; +import janggi.domain.Palaces; +import janggi.domain.Score; import janggi.domain.piece.strategy.LinearStrategy; import janggi.domain.position.Position; import java.util.Map; public class Chariot extends Piece { - private static final LinearStrategy LINEAR_STRATEGY = new LinearStrategy(); + private static final PieceName CHARIOT_NAME = new PieceName("車", "車"); + private static final Score CHARIOT_SCORE = new Score(13); public Chariot(Camp camp) { - super(camp, LINEAR_STRATEGY); + super(camp, new LinearStrategy(Palaces.of()), CHARIOT_NAME, CHARIOT_SCORE); } @Override @@ -34,12 +37,7 @@ public boolean canBeCaughtByCannon() { } @Override - public String choDisplayName() { - return "車"; - } - - @Override - public String hanDisplayName() { - return "車"; + public boolean isEssential() { + return false; } } diff --git a/src/main/java/janggi/domain/piece/Elephant.java b/src/main/java/janggi/domain/piece/Elephant.java index 6ad3785d42..6ced90833a 100644 --- a/src/main/java/janggi/domain/piece/Elephant.java +++ b/src/main/java/janggi/domain/piece/Elephant.java @@ -1,16 +1,18 @@ package janggi.domain.piece; import janggi.domain.Camp; +import janggi.domain.Score; import janggi.domain.piece.strategy.ElephantStrategy; import janggi.domain.position.Position; import java.util.Map; public class Elephant extends Piece { - private static final ElephantStrategy ELEPHANT_STRATEGY = new ElephantStrategy(); + private static final PieceName ELEPHANT_NAME = new PieceName("象", "象"); + private static final Score ELEPHANT_SCORE = new Score(3); public Elephant(Camp camp) { - super(camp, ELEPHANT_STRATEGY); + super(camp, ElephantStrategy.getInstance(), ELEPHANT_NAME, ELEPHANT_SCORE); } @Override @@ -34,12 +36,7 @@ public boolean canBeCaughtByCannon() { } @Override - public String choDisplayName() { - return "象"; - } - - @Override - public String hanDisplayName() { - return "象"; + public boolean isEssential() { + return false; } } diff --git a/src/main/java/janggi/domain/piece/General.java b/src/main/java/janggi/domain/piece/General.java index 5e80e8689f..76011616ee 100644 --- a/src/main/java/janggi/domain/piece/General.java +++ b/src/main/java/janggi/domain/piece/General.java @@ -1,16 +1,27 @@ package janggi.domain.piece; import janggi.domain.Camp; -import janggi.domain.piece.strategy.PalaceStrategy; +import janggi.domain.Score; +import janggi.domain.piece.strategy.ChoPalaceStrategy; +import janggi.domain.piece.strategy.HanPalaceStrategy; +import janggi.domain.piece.strategy.MoveStrategy; import janggi.domain.position.Position; import java.util.Map; public class General extends Piece { - private static final PalaceStrategy PALACE_STRATEGY = new PalaceStrategy(); + private static final PieceName GENERAL_NAME = new PieceName("楚", "漢"); + private static final Score GENERAL_SCORE = new Score(0); public General(Camp camp) { - super(camp, PALACE_STRATEGY); + super(camp, createStrategy(camp), GENERAL_NAME, GENERAL_SCORE); + } + + private static MoveStrategy createStrategy(Camp camp) { + if (camp.isCho()) { + return ChoPalaceStrategy.getInstance(); + } + return HanPalaceStrategy.getInstance(); } @Override @@ -34,12 +45,7 @@ public boolean canBeCaughtByCannon() { } @Override - public String choDisplayName() { - return "楚"; - } - - @Override - public String hanDisplayName() { - return "漢"; + public boolean isEssential() { + return true; } } diff --git a/src/main/java/janggi/domain/piece/Horse.java b/src/main/java/janggi/domain/piece/Horse.java index 25b5bcd8dc..ba8bbbd83b 100644 --- a/src/main/java/janggi/domain/piece/Horse.java +++ b/src/main/java/janggi/domain/piece/Horse.java @@ -1,16 +1,18 @@ package janggi.domain.piece; import janggi.domain.Camp; +import janggi.domain.Score; import janggi.domain.piece.strategy.HorseStrategy; import janggi.domain.position.Position; import java.util.Map; public class Horse extends Piece { - private static final HorseStrategy HORSE_STRATEGY = new HorseStrategy(); + private static final PieceName HORSE_NAME = new PieceName("馬", "馬"); + private static final Score HORSE_SCORE = new Score(5); public Horse(Camp camp) { - super(camp, HORSE_STRATEGY); + super(camp, HorseStrategy.getInstance(), HORSE_NAME, HORSE_SCORE); } @Override @@ -34,12 +36,7 @@ public boolean canBeCaughtByCannon() { } @Override - public String choDisplayName() { - return "馬"; - } - - @Override - public String hanDisplayName() { - return "馬"; + public boolean isEssential() { + return false; } } diff --git a/src/main/java/janggi/domain/piece/Piece.java b/src/main/java/janggi/domain/piece/Piece.java index b69736f84d..49eed1730e 100644 --- a/src/main/java/janggi/domain/piece/Piece.java +++ b/src/main/java/janggi/domain/piece/Piece.java @@ -2,6 +2,7 @@ import janggi.domain.Camp; import janggi.domain.Paths; +import janggi.domain.Score; import janggi.domain.piece.strategy.MoveStrategy; import janggi.domain.position.Position; @@ -10,10 +11,14 @@ public abstract class Piece { private final Camp camp; private final MoveStrategy moveStrategy; + private final PieceName pieceName; + private final Score score; - Piece(Camp camp, MoveStrategy moveStrategy) { + Piece(Camp camp, MoveStrategy moveStrategy, PieceName pieceName, Score score) { this.camp = camp; this.moveStrategy = moveStrategy; + this.pieceName = pieceName; + this.score = score; } public Paths findMovablePaths(Position current) { @@ -28,11 +33,16 @@ public boolean isSameCamp(Camp camp) { return this.camp.isSameCamp(camp); } - public String displayName() { - if (camp.isCho()) { - return choDisplayName(); - } - return hanDisplayName(); + public String pieceName() { + return pieceName.of(camp); + } + + public Camp pieceCamp() { + return camp; + } + + public Score score() { + return score; } abstract public boolean canPassRoute(Map piecesInPath); @@ -43,7 +53,5 @@ public String displayName() { abstract public boolean canBeCaughtByCannon(); - abstract public String choDisplayName(); - - abstract public String hanDisplayName(); + abstract public boolean isEssential(); } diff --git a/src/main/java/janggi/domain/piece/PieceFactory.java b/src/main/java/janggi/domain/piece/PieceFactory.java new file mode 100644 index 0000000000..c09627f761 --- /dev/null +++ b/src/main/java/janggi/domain/piece/PieceFactory.java @@ -0,0 +1,60 @@ +package janggi.domain.piece; + +import janggi.domain.Camp; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +public class PieceFactory { + private static final Map> pieceCreators; + + + static { + Map> creators = new HashMap<>(); + + + creators.put("車", Chariot::new); + + creators.put("包", Cannon::new); + creators.put("砲", Cannon::new); + + creators.put("馬", Horse::new); + + creators.put("士", Advisor::new); + creators.put("仕", Advisor::new); + + creators.put("楚", General::new); + creators.put("漢", General::new); + + creators.put("象", Elephant::new); + creators.put("相", Elephant::new); + + creators.put("卒", Soldier::new); + creators.put("兵", Soldier::new); + + pieceCreators = Collections.unmodifiableMap(creators); + } + + public static Piece create(String pieceNameStr, String campStr) { + Camp camp = parseCamp(campStr); + String nameKey = pieceNameStr.toUpperCase(); + + Function creator = pieceCreators.get(nameKey); + + if (creator == null) { + throw new IllegalArgumentException("존재하지 않는 기물입니다: " + pieceNameStr); + } + + return creator.apply(camp); + } + + private static Camp parseCamp(String campStr) { + try { + return Camp.valueOf(campStr.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("존재하지 않는 진영입니다: " + campStr); + } + } +} diff --git a/src/main/java/janggi/domain/piece/PieceName.java b/src/main/java/janggi/domain/piece/PieceName.java new file mode 100644 index 0000000000..ca70389941 --- /dev/null +++ b/src/main/java/janggi/domain/piece/PieceName.java @@ -0,0 +1,20 @@ +package janggi.domain.piece; + +import janggi.domain.Camp; + +public class PieceName { + private final String choName; + private final String hanName; + + public PieceName(String choName, String hanName) { + this.choName = choName; + this.hanName = hanName; + } + + public String of(Camp camp) { + if (camp.isCho()) { + return choName; + } + return hanName; + } +} diff --git a/src/main/java/janggi/domain/piece/Soldier.java b/src/main/java/janggi/domain/piece/Soldier.java index a7b364a974..c1f731a81c 100644 --- a/src/main/java/janggi/domain/piece/Soldier.java +++ b/src/main/java/janggi/domain/piece/Soldier.java @@ -1,6 +1,7 @@ package janggi.domain.piece; import janggi.domain.Camp; +import janggi.domain.Score; import janggi.domain.piece.strategy.ChoSoldierStrategy; import janggi.domain.piece.strategy.HanSoldierStrategy; import janggi.domain.piece.strategy.MoveStrategy; @@ -9,18 +10,18 @@ import java.util.Map; public class Soldier extends Piece { - private static final ChoSoldierStrategy CHO_SOLDIER_STRATEGY = new ChoSoldierStrategy(); - private static final HanSoldierStrategy HAN_SOLDIER_STRATEGY = new HanSoldierStrategy(); + private static final PieceName SOLDIER_NAME = new PieceName("卒", "兵"); + private static final Score SOLDIER_SCORE = new Score(2); public Soldier(Camp camp) { - super(camp, createStrategy(camp)); + super(camp, createStrategy(camp), SOLDIER_NAME, SOLDIER_SCORE); } private static MoveStrategy createStrategy(Camp camp) { if (camp.isCho()) { - return CHO_SOLDIER_STRATEGY; + return ChoSoldierStrategy.getInstance(); } - return HAN_SOLDIER_STRATEGY; + return HanSoldierStrategy.getInstance(); } @Override @@ -44,12 +45,7 @@ public boolean canBeCaughtByCannon() { } @Override - public String choDisplayName() { - return "卒"; - } - - @Override - public String hanDisplayName() { - return "兵"; + public boolean isEssential() { + return false; } } diff --git a/src/main/java/janggi/domain/piece/strategy/ChoPalaceStrategy.java b/src/main/java/janggi/domain/piece/strategy/ChoPalaceStrategy.java new file mode 100644 index 0000000000..916a12931e --- /dev/null +++ b/src/main/java/janggi/domain/piece/strategy/ChoPalaceStrategy.java @@ -0,0 +1,15 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Palace; + +public class ChoPalaceStrategy extends PalaceStrategy { + private static final ChoPalaceStrategy INSTANCE = new ChoPalaceStrategy(); + + private ChoPalaceStrategy() { + super(Palace.cho()); + } + + public static ChoPalaceStrategy getInstance() { + return INSTANCE; + } +} diff --git a/src/main/java/janggi/domain/piece/strategy/ChoSoldierStrategy.java b/src/main/java/janggi/domain/piece/strategy/ChoSoldierStrategy.java index f5fe89d15d..f7046067a3 100644 --- a/src/main/java/janggi/domain/piece/strategy/ChoSoldierStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/ChoSoldierStrategy.java @@ -1,24 +1,15 @@ package janggi.domain.piece.strategy; -import janggi.domain.Path; -import janggi.domain.Paths; import janggi.domain.position.Direction; -import janggi.domain.position.Position; -import java.util.Optional; -import java.util.stream.Stream; +public class ChoSoldierStrategy extends SoldierStrategy { + private static final ChoSoldierStrategy INSTANCE = new ChoSoldierStrategy(); -public class ChoSoldierStrategy implements MoveStrategy { - @Override - public Paths findMovablePaths(Position current) { - return new Paths(Stream.of( - current.move(Direction.UP), - current.move(Direction.LEFT), - current.move(Direction.RIGHT) - ) - .filter(Optional::isPresent) - .map(Optional::get) - .map(Path::of) - .toList()); + private ChoSoldierStrategy() { + super(Direction.UP); + } + + public static ChoSoldierStrategy getInstance() { + return INSTANCE; } } diff --git a/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java b/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java index 251a33b6a5..98f3a53331 100644 --- a/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/ElephantStrategy.java @@ -10,6 +10,13 @@ import java.util.stream.Stream; public class ElephantStrategy implements MoveStrategy { + private ElephantStrategy() { + } + + public static ElephantStrategy getInstance() { + return SingleInstanceHolder.INSTANCE; + } + @Override public Paths findMovablePaths(Position current) { return new Paths(Stream.of( @@ -33,4 +40,8 @@ public Optional createPath(Position current, Direction straight, Direction .flatMap(wp2 -> wp2.move(diagonal) .map(dest -> Path.of(List.of(wp1, wp2), dest)))); } + + private static class SingleInstanceHolder { + private static final ElephantStrategy INSTANCE = new ElephantStrategy(); + } } diff --git a/src/main/java/janggi/domain/piece/strategy/HanPalaceStrategy.java b/src/main/java/janggi/domain/piece/strategy/HanPalaceStrategy.java new file mode 100644 index 0000000000..486c2637a2 --- /dev/null +++ b/src/main/java/janggi/domain/piece/strategy/HanPalaceStrategy.java @@ -0,0 +1,15 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Palace; + +public class HanPalaceStrategy extends PalaceStrategy { + private static final HanPalaceStrategy INSTANCE = new HanPalaceStrategy(); + + private HanPalaceStrategy() { + super(Palace.han()); + } + + public static HanPalaceStrategy getInstance() { + return INSTANCE; + } +} diff --git a/src/main/java/janggi/domain/piece/strategy/HanSoldierStrategy.java b/src/main/java/janggi/domain/piece/strategy/HanSoldierStrategy.java index fc41bc74d9..6fcbf4a66c 100644 --- a/src/main/java/janggi/domain/piece/strategy/HanSoldierStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/HanSoldierStrategy.java @@ -1,24 +1,15 @@ package janggi.domain.piece.strategy; -import janggi.domain.Path; -import janggi.domain.Paths; import janggi.domain.position.Direction; -import janggi.domain.position.Position; -import java.util.Optional; -import java.util.stream.Stream; +public class HanSoldierStrategy extends SoldierStrategy { + private static final HanSoldierStrategy INSTANCE = new HanSoldierStrategy(); -public class HanSoldierStrategy implements MoveStrategy { - @Override - public Paths findMovablePaths(Position current) { - return new Paths(Stream.of( - current.move(Direction.DOWN), - current.move(Direction.LEFT), - current.move(Direction.RIGHT) - ) - .filter(Optional::isPresent) - .map(Optional::get) - .map(Path::of) - .toList()); + private HanSoldierStrategy() { + super(Direction.DOWN); + } + + public static HanSoldierStrategy getInstance() { + return INSTANCE; } } diff --git a/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java b/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java index c762eb6bf3..2e6d39bf83 100644 --- a/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/HorseStrategy.java @@ -10,6 +10,13 @@ import java.util.stream.Stream; public class HorseStrategy implements MoveStrategy { + private HorseStrategy() { + } + + public static HorseStrategy getInstance() { + return SingleInstanceHolder.INSTANCE; + } + @Override public Paths findMovablePaths(Position current) { return new Paths(Stream.of( @@ -32,4 +39,8 @@ public Optional createPath(Position current, Direction straight, Direction .flatMap(wp -> wp.move(diagonal) .map(dest -> Path.of(List.of(wp), dest))); } + + private static class SingleInstanceHolder { + private static final HorseStrategy INSTANCE = new HorseStrategy(); + } } diff --git a/src/main/java/janggi/domain/piece/strategy/LinearStrategy.java b/src/main/java/janggi/domain/piece/strategy/LinearStrategy.java index 8f8148c909..8d1506b7cf 100644 --- a/src/main/java/janggi/domain/piece/strategy/LinearStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/LinearStrategy.java @@ -1,5 +1,6 @@ package janggi.domain.piece.strategy; +import janggi.domain.Palaces; import janggi.domain.Path; import janggi.domain.Paths; import janggi.domain.position.Direction; @@ -10,12 +11,23 @@ import java.util.Optional; public class LinearStrategy implements MoveStrategy { + private final Palaces palaces; + + public LinearStrategy(Palaces palaces) { + this.palaces = palaces; + } + @Override public Paths findMovablePaths(Position current) { List paths = new ArrayList<>(); + for (Direction direction : Direction.straight()) { paths.addAll(collectPaths(current, direction)); } + + for (Direction direction : palaces.diagonalDirectionsAt(current)) { + paths.addAll(collectPalacePaths(current, direction)); + } return new Paths(paths); } @@ -32,4 +44,19 @@ private List collectPaths(Position current, Direction direction) { } return paths; } + + private List collectPalacePaths(Position current, Direction direction) { + List paths = new ArrayList<>(); + List waypoints = new ArrayList<>(); + Optional next = current.move(direction); + + while (next.isPresent() && palaces.containsAny(next.get())) { + Position destination = next.get(); + paths.add(Path.of(List.copyOf(waypoints), destination)); + waypoints.add(destination); + next = destination.move(direction); + } + return paths; + } } + diff --git a/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java b/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java index 94bcc5ee7d..46110d14a8 100644 --- a/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java +++ b/src/main/java/janggi/domain/piece/strategy/PalaceStrategy.java @@ -1,20 +1,33 @@ package janggi.domain.piece.strategy; +import janggi.domain.Palace; import janggi.domain.Path; import janggi.domain.Paths; import janggi.domain.position.Direction; import janggi.domain.position.Position; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; public class PalaceStrategy implements MoveStrategy { + private final Palace palace; + + PalaceStrategy(Palace palace) { + this.palace = palace; + } + @Override public Paths findMovablePaths(Position current) { + List directions = new ArrayList<>(Direction.straight()); + + directions.addAll(palace.diagonalDirectionsAt(current)); + return new Paths( - Direction.straight().stream() + directions.stream() .map(current::move) - .filter(Optional::isPresent) - .map(Optional::get) + .flatMap(Optional::stream) + .filter(palace::contains) .map(Path::of) .toList() ); diff --git a/src/main/java/janggi/domain/piece/strategy/SoldierStrategy.java b/src/main/java/janggi/domain/piece/strategy/SoldierStrategy.java new file mode 100644 index 0000000000..b9b3dd0ed1 --- /dev/null +++ b/src/main/java/janggi/domain/piece/strategy/SoldierStrategy.java @@ -0,0 +1,43 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Palaces; +import janggi.domain.Path; +import janggi.domain.Paths; +import janggi.domain.position.Direction; +import janggi.domain.position.Position; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public abstract class SoldierStrategy implements MoveStrategy { + private final Direction forward; + private final Palaces palaces; + + SoldierStrategy(Direction forward) { + this.forward = forward; + this.palaces = Palaces.of(); + } + + @Override + public Paths findMovablePaths(Position current) { + List directions = new ArrayList<>(List.of(forward, Direction.LEFT, Direction.RIGHT)); + + palaces.diagonalDirectionsAt(current).stream() + .filter(this::isForwardDiagonal) + .forEach(directions::add); + + return new Paths(directions.stream() + .map(current::move) + .flatMap(Optional::stream) + .map(Path::of) + .toList()); + } + + private boolean isForwardDiagonal(Direction direction) { + if (forward == Direction.UP) { + return direction.isUp(); + } + return direction.isDown(); + } +} diff --git a/src/main/java/janggi/domain/position/Column.java b/src/main/java/janggi/domain/position/Column.java index e9a37ab6ef..de4ce172f9 100644 --- a/src/main/java/janggi/domain/position/Column.java +++ b/src/main/java/janggi/domain/position/Column.java @@ -1,30 +1,45 @@ package janggi.domain.position; import java.util.List; +import java.util.Optional; import java.util.stream.IntStream; public class Column { private static final int MAX_SIZE = 8; + private static final List ALL_COLUMN = IntStream.rangeClosed(0, MAX_SIZE) + .mapToObj(Column::new) + .toList(); private final int value; - Column(int value) { - validate(value); + private Column(int value) { this.value = value; } - static List all() { - return IntStream.rangeClosed(0, MAX_SIZE) - .mapToObj(Column::new) - .toList(); + public static Column from(int value) { + validate(value); + return ALL_COLUMN.get(value); } - private void validate(int value) { - if (value < 0 || value > MAX_SIZE) { + private static void validate(int value) { + if (isOutOfBounds(value)) { throw new IllegalArgumentException("열은 0 ~ 8 입니다."); } } + private static boolean isOutOfBounds(int value) { + return value < 0 || value > MAX_SIZE; + } + + public Optional move(int distance) { + int nextValue = this.value + distance; + + if (isOutOfBounds(nextValue)) { + return Optional.empty(); + } + return Optional.of(ALL_COLUMN.get(nextValue)); + } + int value() { return value; } diff --git a/src/main/java/janggi/domain/position/Direction.java b/src/main/java/janggi/domain/position/Direction.java index 62b2d9e10b..d1ee6b9ebf 100644 --- a/src/main/java/janggi/domain/position/Direction.java +++ b/src/main/java/janggi/domain/position/Direction.java @@ -24,10 +24,22 @@ public static List straight() { return List.of(UP, DOWN, LEFT, RIGHT); } + public static List diagonal() { + return List.of(UP_RIGHT, UP_LEFT, DOWN_LEFT, DOWN_RIGHT); + } + public static List all() { return List.of(values()); } + public boolean isUp() { + return dr > 0; + } + + public boolean isDown() { + return dr < 0; + } + int dr() { return dr; } diff --git a/src/main/java/janggi/domain/position/Position.java b/src/main/java/janggi/domain/position/Position.java index 21269ce5a4..64c7cf05cb 100644 --- a/src/main/java/janggi/domain/position/Position.java +++ b/src/main/java/janggi/domain/position/Position.java @@ -1,23 +1,9 @@ package janggi.domain.position; -import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; public class Position { - private static final Map ALL_POSITION; - - static { - ALL_POSITION = Row.all().stream() - .flatMap(row -> Column.all().stream() - .map(column -> new Position(row, column))) - .collect(Collectors.toMap( - p -> createKey(p.row.value(), p.column.value()), - p -> p - )); - } - private final Row row; private final Column column; @@ -27,20 +13,26 @@ private Position(Row row, Column column) { } public static Position of(int row, int column) { - Position position = ALL_POSITION.get(createKey(row, column)); - if (position == null) { - throw new IllegalArgumentException("잘못된 좌표입니다."); - } - return position; + return new Position(Row.from(row), Column.from(column)); } public Optional move(Direction direction) { - return Optional.ofNullable( - ALL_POSITION.get(createKey(row.value() + direction.dr(), column.value() + direction.dc())) - ); + Optional nextRow = row.move(direction.dr()); + Optional nextColumn = column.move(direction.dc()); + + if (nextRow.isPresent() && nextColumn.isPresent()) { + return Optional.of(new Position(nextRow.get(), nextColumn.get())); + } + + return Optional.empty(); + } + + public int row() { + return row.value(); } - private static String createKey(int row, int column) { - return row + "," + column; + + public int column() { + return column.value(); } @Override diff --git a/src/main/java/janggi/domain/position/Row.java b/src/main/java/janggi/domain/position/Row.java index 3e48f20940..2a9b943dad 100644 --- a/src/main/java/janggi/domain/position/Row.java +++ b/src/main/java/janggi/domain/position/Row.java @@ -1,30 +1,46 @@ package janggi.domain.position; import java.util.List; +import java.util.Optional; import java.util.stream.IntStream; public class Row { private static final int MAX_SIZE = 9; + private static final List ALL_ROW = IntStream.rangeClosed(0, MAX_SIZE) + .mapToObj(Row::new) + .toList(); private final int value; - Row(int value) { - validate(value); + private Row(int value) { this.value = value; } - static List all() { - return IntStream.rangeClosed(0, MAX_SIZE) - .mapToObj(Row::new) - .toList(); + public static Row from(int value) { + validate(value); + return ALL_ROW.get(value); } - private void validate(int value) { - if (value < 0 || value > MAX_SIZE) { + private static void validate(int value) { + if (isOutOfBounds(value)) { throw new IllegalArgumentException("행은 0 ~ 9 입니다."); } } + private static boolean isOutOfBounds(int value) { + return value < 0 || value > MAX_SIZE; + } + + public Optional move(int distance) { + int nextValue = this.value + distance; + + if (isOutOfBounds(nextValue)) { + return Optional.empty(); + } + + return Optional.ofNullable(ALL_ROW.get(nextValue)); + } + int value() { return value; } diff --git a/src/main/java/janggi/exception/DuplicateGameException.java b/src/main/java/janggi/exception/DuplicateGameException.java new file mode 100644 index 0000000000..0689b13fc1 --- /dev/null +++ b/src/main/java/janggi/exception/DuplicateGameException.java @@ -0,0 +1,11 @@ +package janggi.exception; + +public class DuplicateGameException extends RuntimeException { + public DuplicateGameException(String message) { + super(message); + } + + public DuplicateGameException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/janggi/persistence/GameRepositoryImpl.java b/src/main/java/janggi/persistence/GameRepositoryImpl.java new file mode 100644 index 0000000000..174222571a --- /dev/null +++ b/src/main/java/janggi/persistence/GameRepositoryImpl.java @@ -0,0 +1,111 @@ +package janggi.persistence; + + +import janggi.domain.Camp; +import janggi.domain.Janggi; +import janggi.domain.piece.Piece; +import janggi.domain.position.Position; +import janggi.persistence.dao.GameDao; +import janggi.persistence.dao.PieceDao; +import janggi.persistence.entity.GameEntity; +import janggi.persistence.entity.PieceEntity; +import janggi.persistence.entity.vo.Status; +import janggi.persistence.mapper.GameMapper; +import janggi.persistence.mapper.PieceMapper; +import janggi.service.GameRepository; + +import java.sql.Connection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class GameRepositoryImpl implements GameRepository { + private final GameDao gameDao; + private final PieceDao pieceDao; + private final GameMapper gameMapper; + private final PieceMapper pieceMapper; + + private final Map activeGames = new ConcurrentHashMap<>(); + + public GameRepositoryImpl(GameDao gameDao, PieceDao pieceDao, GameMapper gameMapper, PieceMapper pieceMapper) { + this.gameDao = gameDao; + this.pieceDao = pieceDao; + this.gameMapper = gameMapper; + this.pieceMapper = pieceMapper; + } + + @Override + public String save(Connection conn, String name, Janggi janggi) { + String newGameId = UUID.randomUUID().toString(); + + GameEntity gameEntity = gameMapper.toGameEntity(newGameId, name, janggi); + gameDao.create(conn, gameEntity); + + List pieceEntities = pieceMapper.toPieceEntity(newGameId, janggi); + pieceDao.createAll(conn, pieceEntities); + + activeGames.put(newGameId, janggi.clone()); + + return newGameId; + } + + @Override + public Optional findById(String gameId) { + Janggi janggi = activeGames.get(gameId); + if (janggi != null) { + return Optional.of(janggi.clone()); + } + + Optional gameEntityOpt = gameDao.findById(gameId); + if (gameEntityOpt.isEmpty()) { + return Optional.empty(); + } + + GameEntity gameEntity = gameEntityOpt.get(); + List pieceEntities = pieceDao.findByGameId(gameId); + Map board = pieceMapper.toBoard(pieceEntities); + Janggi resurrectedJanggi = gameMapper.toJanggi(gameEntity, board); + + activeGames.put(gameId, resurrectedJanggi); + + return Optional.of(resurrectedJanggi.clone()); + } + + @Override + public void update(Connection conn, String gameId, Janggi janggi) { + gameDao.updateStatus(conn, gameId, janggi.currentCamp(), Status.of(janggi)); + pieceDao.deleteByGameId(conn, gameId); + pieceDao.createAll(conn, pieceMapper.toPieceEntity(gameId, janggi)); + + activeGames.put(gameId, janggi.clone()); + } + + @Override + public void updateGameResult(Connection conn,String gameId, Janggi janggi) { + Status status = Status.of(janggi); + + gameDao.updateStatus(conn, gameId, janggi.currentCamp(), status); + + activeGames.remove(gameId); + } + + @Override + public List findAllNames() { + return gameDao.findAllNames(); + } + + @Override + public Optional findByName(String gameName) { + return gameDao.findByName(gameName); + } + + @Override + public void deleteById(Connection conn,String gameId) { + pieceDao.deleteByGameId(conn, gameId); + gameDao.deleteById(conn, gameId); + + activeGames.remove(gameId); + } +} diff --git a/src/main/java/janggi/persistence/dao/GameDao.java b/src/main/java/janggi/persistence/dao/GameDao.java new file mode 100644 index 0000000000..461bc4a1ee --- /dev/null +++ b/src/main/java/janggi/persistence/dao/GameDao.java @@ -0,0 +1,23 @@ +package janggi.persistence.dao; + +import janggi.domain.Camp; +import janggi.persistence.entity.GameEntity; +import janggi.persistence.entity.vo.Status; + +import java.sql.Connection; +import java.util.List; +import java.util.Optional; + +public interface GameDao { + void create(Connection conn, GameEntity gameEntity); + + List findAllNames(); + + Optional findByName(String name); + + void deleteById(Connection conn, String id); + + Optional findById(String id); + + void updateStatus(Connection conn, String gameId, Camp camp, Status status); +} diff --git a/src/main/java/janggi/persistence/dao/JdbcGameDao.java b/src/main/java/janggi/persistence/dao/JdbcGameDao.java new file mode 100644 index 0000000000..42c03c1cf3 --- /dev/null +++ b/src/main/java/janggi/persistence/dao/JdbcGameDao.java @@ -0,0 +1,165 @@ +package janggi.persistence.dao; + +import janggi.config.ConnectionPool; +import janggi.config.PooledConnection; +import janggi.domain.Camp; +import janggi.exception.DuplicateGameException; +import janggi.persistence.entity.GameEntity; +import janggi.persistence.entity.vo.Status; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class JdbcGameDao implements GameDao { + private final ConnectionPool pool; + + public JdbcGameDao(ConnectionPool pool) { + this.pool = pool; + } + + private static final String MYSQL_DUPLICATE_STATE = "23000"; + private static final String H2_DUPLICATE_STATE = "23505"; + + @Override + public void create(Connection conn, GameEntity gameEntity) { + String sql = """ + INSERT INTO game (id, name, status, current_turn) + VALUES (?, ?, ?, ?) + """; + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + + pstmt.setString(1, gameEntity.id()); + pstmt.setString(2, gameEntity.name()); + pstmt.setString(3, gameEntity.status().name()); + pstmt.setString(4, gameEntity.camp().name()); + + pstmt.executeUpdate(); + } catch (SQLException e) { + System.out.println("ErrorCode: " + e.getErrorCode()); + System.out.println("SQLState: " + e.getSQLState()); + System.out.println("Message: " + e.getMessage()); + validateDuplicate(e); + throw new RuntimeException("게임을 생성하는 중 데이터베이스 오류가 발생했습니다.", e); + } + } + + private void validateDuplicate(SQLException e) { + String state = e.getSQLState(); + if (MYSQL_DUPLICATE_STATE.equals(state) || H2_DUPLICATE_STATE.equals(state)) { + throw new DuplicateGameException("이미 존재하는 게임입니다.", e); + } + } + + @Override + public List findAllNames() { + String sql = """ + SELECT name + FROM game + """; + + List names = new ArrayList<>(); + + try (PooledConnection pooled = pool.getPooledConnection(); + PreparedStatement pstmt = pooled.getConnection().prepareStatement(sql); + ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + names.add(rs.getString("name")); + } + + } catch (SQLException e) { + throw new RuntimeException("게임 목록을 불러오는 중 오류가 발생했습니다.", e); + } + + return names; + } + + @Override + public Optional findByName(String gameName) { + String sql = """ + SELECT id FROM game + WHERE name = ? + """; + + try (PooledConnection pooled = pool.getPooledConnection(); + PreparedStatement pstmt =pooled.getConnection().prepareStatement(sql)) { + pstmt.setString(1, gameName); + try (ResultSet rs = pstmt.executeQuery()) { + if (rs.next()) { + String gameId = rs.getString("id"); + return Optional.of(gameId); + } + } + } catch (SQLException e) { + throw new RuntimeException("게임 조회 도중 오류가 발생했습니다.", e); + } + return Optional.empty(); + } + + @Override + public void deleteById(Connection conn, String id) { + String sql = """ + DELETE FROM game + WHERE id = ? + """; + + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setString(1, id); + pstmt.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("게임 삭제 도중 오류가 발생했습니다.", e); + } + } + + @Override + public Optional findById(String id) { + String sql = """ + SELECT * FROM game + WHERE id = ? + """; + + try (PooledConnection pooled = pool.getPooledConnection(); + PreparedStatement pstmt = pool.getConnection().prepareStatement(sql)) { + pstmt.setString(1, id); + + try (ResultSet rs = pstmt.executeQuery()) { + if (rs.next()) { + GameEntity gameEntity = new GameEntity( + rs.getString("id"), + rs.getString("name"), + Status.valueOf(rs.getString("status")), + Camp.valueOf(rs.getString("current_turn")) + ); + return Optional.of(gameEntity); + } + } + } catch (SQLException e) { + throw new RuntimeException("게임 조회 도중 오류가 발생했습니다.", e); + } + return Optional.empty(); + } + + @Override + public void updateStatus(Connection conn, String gameId, Camp camp, Status status) { + String sql = """ + UPDATE game + SET status = ?, current_turn = ? + WHERE id = ? + """; + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + + pstmt.setString(1, status.name()); + pstmt.setString(2, camp.name()); + pstmt.setString(3, gameId); + + pstmt.executeUpdate(); + + } catch (SQLException e) { + throw new RuntimeException("게임 상태를 업데이트하는 중 오류가 발생했습니다.", e); + } + } +} diff --git a/src/main/java/janggi/persistence/dao/JdbcPieceDao.java b/src/main/java/janggi/persistence/dao/JdbcPieceDao.java new file mode 100644 index 0000000000..a2cac24c3b --- /dev/null +++ b/src/main/java/janggi/persistence/dao/JdbcPieceDao.java @@ -0,0 +1,118 @@ +package janggi.persistence.dao; + +import janggi.config.ConnectionPool; +import janggi.config.PooledConnection; +import janggi.persistence.entity.PieceEntity; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import static janggi.config.DatabaseConfig.getConnection; + +public class JdbcPieceDao implements PieceDao { + private final ConnectionPool pool; + + public JdbcPieceDao(ConnectionPool pool) { + this.pool = pool; + } + + @Override + public void createAll(Connection conn, List entities) { + String sql = """ + INSERT INTO piece (game_id, piece_name, camp, row_index, column_index) + VALUES (?, ?, ?, ?, ?) + """; + + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + + for (PieceEntity piece : entities) { + pstmt.setString(1, piece.gameId()); + pstmt.setString(2, piece.pieceName()); + pstmt.setString(3, piece.pieceCamp()); + pstmt.setInt(4, piece.row()); + pstmt.setInt(5, piece.column()); + + pstmt.addBatch(); + } + + pstmt.executeBatch(); + } catch (SQLException e) { + throw new RuntimeException("게임을 시작하는 중 기물 생성에 오류가 발생했습니다.", e); + } + } + + @Override + public List findByGameId(String gameId) { + String sql = """ + SELECT * FROM piece + WHERE game_id = ? + """; + + List pieces = new ArrayList<>(); + + try (PooledConnection pooled = pool.getPooledConnection(); + PreparedStatement pstmt = pooled.getConnection().prepareStatement(sql)) { + + pstmt.setString(1, gameId); + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + PieceEntity piece = new PieceEntity( + rs.getLong("id"), + rs.getString("game_id"), + rs.getString("piece_name"), + rs.getString("camp"), + rs.getInt("row_index"), + rs.getInt("column_index")); + pieces.add(piece); + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + return pieces; + } + + @Override + public void deleteByGameId(Connection conn, String gameId) { + String sql = "DELETE FROM piece WHERE game_id = ?"; + + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + + pstmt.setString(1, gameId); + + pstmt.executeUpdate(); + + } catch (SQLException e) { + throw new RuntimeException("해당 게임의 기물을 삭제하는 데 실패했습니다.", e); + } + } + + @Override + public void updateAll(Connection conn, String gameId, List entities) { + String sql = """ + UPDATE piece + SET piece_name = ?, camp = ?, row_index = ?, column = ? + WHERE id = ? AND game_id = ? + """; + + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + + for (PieceEntity piece : entities) { + pstmt.setString(1, piece.pieceName()); + pstmt.setString(2, piece.pieceCamp()); + pstmt.setInt(3, piece.row()); + pstmt.setInt(4, piece.column()); + pstmt.setLong(6, piece.id()); + pstmt.setString(7, gameId); + pstmt.addBatch(); + } + pstmt.executeBatch(); + } catch (SQLException e) { + throw new RuntimeException("기물 상태 업데이트 실패", e); + } + } +} diff --git a/src/main/java/janggi/persistence/dao/PieceDao.java b/src/main/java/janggi/persistence/dao/PieceDao.java new file mode 100644 index 0000000000..3b99d1ec61 --- /dev/null +++ b/src/main/java/janggi/persistence/dao/PieceDao.java @@ -0,0 +1,16 @@ +package janggi.persistence.dao; + +import janggi.persistence.entity.PieceEntity; + +import java.sql.Connection; +import java.util.List; + +public interface PieceDao { + void createAll(Connection conn, List entities); + + List findByGameId(String gameId); + + void deleteByGameId(Connection conn, String gameId); + + void updateAll(Connection conn, String gameId, List entities); +} diff --git a/src/main/java/janggi/persistence/entity/GameEntity.java b/src/main/java/janggi/persistence/entity/GameEntity.java new file mode 100644 index 0000000000..f95d606363 --- /dev/null +++ b/src/main/java/janggi/persistence/entity/GameEntity.java @@ -0,0 +1,54 @@ +package janggi.persistence.entity; + +import janggi.domain.Camp; +import janggi.persistence.entity.vo.Status; + +import java.util.Objects; + +public class GameEntity { + private String id; + private String name; + private Status status; + private Camp camp; + + public GameEntity(String id, String name, Status status, Camp camp) { + validateNameLength(name); + this.id = id; + this.name = name; + this.status = status; + this.camp = camp; + } + + private void validateNameLength(String name) { + if (name.length() < 1 || name.length() > 50) { + throw new IllegalArgumentException("게임 이름 길이는 1이상 50 이하여야 합니다."); + } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof GameEntity other)) return false; + return Objects.equals(id, other.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public Status status() { + return status; + } + + public Camp camp() { + return camp; + } +} diff --git a/src/main/java/janggi/persistence/entity/PieceEntity.java b/src/main/java/janggi/persistence/entity/PieceEntity.java new file mode 100644 index 0000000000..fba85ee8fc --- /dev/null +++ b/src/main/java/janggi/persistence/entity/PieceEntity.java @@ -0,0 +1,43 @@ +package janggi.persistence.entity; + +public class PieceEntity { + private Long id; + private String gameId; + private String pieceName; + private String pieceCamp; + private Integer rowIndex; + private Integer columnIndex; + + public PieceEntity(Long id, String gameId, String pieceName, String pieceCamp, Integer rowIndex, Integer columnIndex) { + this.id = id; + this.gameId = gameId; + this.pieceName = pieceName; + this.pieceCamp = pieceCamp; + this.rowIndex = rowIndex; + this.columnIndex = columnIndex; + } + + public Long id() { + return id; + } + + public String gameId() { + return gameId; + } + + public String pieceName() { + return pieceName; + } + + public String pieceCamp() { + return pieceCamp; + } + + public Integer row() { + return rowIndex; + } + + public Integer column() { + return columnIndex; + } +} diff --git a/src/main/java/janggi/persistence/entity/vo/Status.java b/src/main/java/janggi/persistence/entity/vo/Status.java new file mode 100644 index 0000000000..baf3e1953e --- /dev/null +++ b/src/main/java/janggi/persistence/entity/vo/Status.java @@ -0,0 +1,25 @@ +package janggi.persistence.entity.vo; + +import janggi.domain.Janggi; + +public enum Status { + PLAYING, + CHO_WIN, + HAN_WIN; + + public static Status of(Janggi janggi) { + if (janggi.isRunning()) { + return Status.PLAYING; + } + if (janggi.winner().isCho()) { + return Status.CHO_WIN; + } + return Status.HAN_WIN; + } + + public boolean isRunning() { + return this == PLAYING; + } + + +} diff --git a/src/main/java/janggi/persistence/mapper/GameMapper.java b/src/main/java/janggi/persistence/mapper/GameMapper.java new file mode 100644 index 0000000000..cd737e8e30 --- /dev/null +++ b/src/main/java/janggi/persistence/mapper/GameMapper.java @@ -0,0 +1,36 @@ +package janggi.persistence.mapper; + +import janggi.domain.Camp; +import janggi.domain.Janggi; +import janggi.domain.board.BoardFactory; +import janggi.domain.piece.Piece; +import janggi.domain.position.Position; +import janggi.persistence.entity.GameEntity; +import janggi.persistence.entity.vo.Status; + +import java.util.Map; + +public class GameMapper { + + public GameEntity toGameEntity(String id, String name, Janggi janggi) { + return new GameEntity( + id, + name, + Status.of(janggi), + janggi.currentCamp() + ); + } + + public Janggi toJanggi(GameEntity gameEntity, Map board) { + return Janggi.load( + BoardFactory.load(board), + gameEntity.camp(), + isRunning(gameEntity.status()) + ); + } + + + private boolean isRunning(Status status) { + return status.isRunning(); + } +} diff --git a/src/main/java/janggi/persistence/mapper/PieceMapper.java b/src/main/java/janggi/persistence/mapper/PieceMapper.java new file mode 100644 index 0000000000..3cd9dff297 --- /dev/null +++ b/src/main/java/janggi/persistence/mapper/PieceMapper.java @@ -0,0 +1,39 @@ +package janggi.persistence.mapper; + +import janggi.domain.Janggi; +import janggi.domain.piece.Piece; +import janggi.domain.piece.PieceFactory; +import janggi.domain.position.Position; +import janggi.persistence.entity.PieceEntity; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PieceMapper { + + public List toPieceEntity(String gameId, Janggi janggi) { + return janggi.getBoard().entrySet().stream() + .map(entry -> new PieceEntity( + null, + gameId, + entry.getValue().pieceName(), + entry.getValue().pieceCamp().toString(), + entry.getKey().row(), + entry.getKey().column() + )) + .toList(); + } + + public Map toBoard(List pieceEntities) { + Map pieceMap = new HashMap<>(); + + for (PieceEntity pieceEntity : pieceEntities) { + Position position = Position.of(pieceEntity.row(), pieceEntity.column()); + Piece piece = PieceFactory.create(pieceEntity.pieceName(), pieceEntity.pieceCamp()); + pieceMap.put(position, piece); + } + + return pieceMap; + } +} diff --git a/src/main/java/janggi/service/GameRepository.java b/src/main/java/janggi/service/GameRepository.java new file mode 100644 index 0000000000..8501c4ed89 --- /dev/null +++ b/src/main/java/janggi/service/GameRepository.java @@ -0,0 +1,23 @@ +package janggi.service; + +import janggi.domain.Janggi; + +import java.sql.Connection; +import java.util.List; +import java.util.Optional; + +public interface GameRepository { + Optional findById(String id); + + String save(Connection conn, String name, Janggi janggi); + + List findAllNames(); + + Optional findByName(String gameName); + + void updateGameResult(Connection conn, String gameId, Janggi janggi); + + void update(Connection conn, String id, Janggi janggi); + + void deleteById(Connection conn, String gameId); +} diff --git a/src/main/java/janggi/service/JanggiService.java b/src/main/java/janggi/service/JanggiService.java new file mode 100644 index 0000000000..4b1757f12d --- /dev/null +++ b/src/main/java/janggi/service/JanggiService.java @@ -0,0 +1,119 @@ +package janggi.service; + +import janggi.config.TransactionManager; +import janggi.domain.Camp; +import janggi.domain.Janggi; +import janggi.domain.board.BoardFactory; +import janggi.domain.board.FormationStrategyFactory; +import janggi.domain.piece.Piece; +import janggi.domain.position.Position; +import janggi.exception.DuplicateGameException; + +import java.util.List; +import java.util.Map; + +public class JanggiService { + private final GameRepository gameRepository; + private final TransactionManager transactionManager; + + public JanggiService(GameRepository gameRepository, TransactionManager transactionManager) { + this.gameRepository = gameRepository; + this.transactionManager = transactionManager; + } + + public String createGame(String name, int choFormation, int hanFormation) { + if (gameRepository.findByName(name).isPresent()) { + throw new DuplicateGameException("이미 존재하는 게임입니다."); + } + + Janggi janggi = Janggi.start( + BoardFactory.create( + FormationStrategyFactory.from(choFormation), + FormationStrategyFactory.from(hanFormation)) + ); + + return transactionManager.execute(conn -> + gameRepository.save(conn, name, janggi)); + } + + public List findAllNames() { + return gameRepository.findAllNames(); + } + + public void surrender(String gameId) { + Janggi janggi = findGameById(gameId); + janggi.surrender(); + transactionManager.execute(conn -> { + gameRepository.updateGameResult(conn, gameId, janggi); + return null; + }); + } + + public void draw(String gameId) { + Janggi janggi = findGameById(gameId); + janggi.draw(); + transactionManager.execute(conn -> { + gameRepository.updateGameResult(conn, gameId, janggi); + return null; + }); + } + + public void validateTurn(String gameId, Position from) { + Janggi janggi = findGameById(gameId); + + if (!janggi.isRunning()) { + throw new IllegalStateException("이미 종료된 게임입니다."); + } + + janggi.validateTurn(from); + } + + public void play(String gameId, Position from, Position to) { + Janggi janggi = findGameById(gameId); + + if (!janggi.isRunning()) { + throw new IllegalStateException("이미 종료된 게임입니다."); + } + + janggi.play(from, to); + + transactionManager.execute(conn -> { + gameRepository.update(conn, gameId, janggi); + return null; + }); + } + + public Map getBoardStatus(String gameId) { + return findGameById(gameId).getBoard(); + } + + public boolean isRunning(String gameId) { + return findGameById(gameId).isRunning(); + } + + public String findGameByName(String gameName) { + return gameRepository.findByName(gameName) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게임입니다.")); + } + + public void deleteByName(String gameName) { + String gameId = findGameByName(gameName); + transactionManager.execute(conn -> { + gameRepository.deleteById(conn, gameId); + return null; + }); + } + + public Camp getCurrentCamp(String gameId) { + return findGameById(gameId).currentCamp(); + } + + public Camp getWinner(String gameId) { + return findGameById(gameId).winner(); + } + + private Janggi findGameById(String gameId) { + return gameRepository.findById(gameId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게임입니다.")); + } +} diff --git a/src/main/java/janggi/view/InputView.java b/src/main/java/janggi/view/InputView.java index 94ab425477..84741f03ec 100644 --- a/src/main/java/janggi/view/InputView.java +++ b/src/main/java/janggi/view/InputView.java @@ -1,7 +1,8 @@ package janggi.view; -import janggi.view.dto.PositionRequest; +import janggi.controller.dto.PositionRequest; +import java.util.List; import java.util.Optional; import java.util.Scanner; @@ -9,14 +10,57 @@ public class InputView { private static final String QUIT_COMMAND = "q"; private final Scanner scanner = new Scanner(System.in); - public int readFormationChoice(int playerNumber) { - System.out.println(playerNumber + "P 마상 배치를 선택해주세요."); + public int readMenuOption() { + System.out.println("1. 새로 시작 2. 불러 오기 3. 게임 삭제 4. 나가기"); + return Integer.parseInt(scanner.nextLine().trim()); + } + + public Optional readGameName() { + System.out.println("이 게임의 이름을 입력해주세요. (예: 장기1 / q: 취소)"); + String gameName = scanner.nextLine().trim(); + + if (isQuit(gameName)) { + return Optional.empty(); + } + return Optional.of(gameName); + } + + public Optional readGameName(List gameNames) { + System.out.println("게임을 선택해주세요. (예: 장기1 / q: 취소)"); + + for (int i = 0; i < gameNames.size(); i++) { + System.out.println((i + 1) + ". " + gameNames.get(i)); + } + String input = scanner.nextLine().trim(); + if (isQuit(input)) { + return Optional.empty(); + } + return Optional.of(input); + } + + public Optional readFormationChoice(String camp) { + System.out.println(camp + "의 마상 배치를 선택해주세요. (예: 1 / q: 취소)"); System.out.println("1. 마상상마 2. 마상마상 3. 상마마상 4. 상마상마"); + String input = scanner.nextLine().trim(); + if (isQuit(input)) { + return Optional.empty(); + } + return Optional.of(Integer.parseInt(input)); + } + + public int readCommand() { + System.out.println("1. 항복 2. 무승부 3. 계속"); return Integer.parseInt(scanner.nextLine().trim()); } + public boolean confirmDraw() { + System.out.println("상대방이 무승부를 제안합니다. 수락하시겠습니까? (y/n)"); + String input = scanner.nextLine().trim(); + return input.equals("y"); + } + public Optional readPieceSelection() { - System.out.println("기물을 선택해주세요. (예시: 0 3)"); + System.out.println("기물을 선택해주세요. (x y / q: 취소)"); String input = scanner.nextLine().trim(); if (isQuit(input)) { return Optional.empty(); diff --git a/src/main/java/janggi/view/OutputView.java b/src/main/java/janggi/view/OutputView.java index b9bf659848..21bdc56ad6 100644 --- a/src/main/java/janggi/view/OutputView.java +++ b/src/main/java/janggi/view/OutputView.java @@ -25,8 +25,8 @@ public class OutputView { public void printBoard(Map board, Camp currentCamp) { System.out.println(); - int rowStart = currentCamp.initRowPosition(); - int rowStep = -currentCamp.direction(); + int rowStart = currentCamp.isCho() ? 9 : 0; + int rowStep = currentCamp.isCho() ? -1 : 1; List rows = IntStream.iterate(rowStart, r -> r + rowStep) .limit(10) @@ -52,7 +52,7 @@ private String intersectionRow(int row, Map board) { private String renderCell(int row, int col, Map board) { return Optional.ofNullable(board.get(Position.of(row, col))) - .map(piece -> COLOR_MAP.get(piece.isSameCamp(Camp.CHO)) + piece.displayName() + RESET) + .map(piece -> COLOR_MAP.get(piece.isSameCamp(Camp.CHO)) + piece.pieceName() + RESET) .orElse("+"); } @@ -62,7 +62,17 @@ private String verticalRow() { .collect(Collectors.joining(" ")); } + public void printGameResult(Camp camp) { + if (camp.isCho()) { + System.out.println("초나라가 승리하였습니다."); + return; + } + System.out.println("한나라가 승리하였습니다."); + } + public void printErrorMessage(String message) { System.out.println(message); } + + } diff --git a/src/main/resources/application.properties.template b/src/main/resources/application.properties.template new file mode 100644 index 0000000000..b14bbb8612 --- /dev/null +++ b/src/main/resources/application.properties.template @@ -0,0 +1,5 @@ +# JDBC 설정 + +db.url=jdbc:mysql://127.0.0.1:3306/ +db.id=your_db_id +db.password=your_db_password \ No newline at end of file diff --git a/src/test/java/janggi/config/TestConnectionPool.java b/src/test/java/janggi/config/TestConnectionPool.java new file mode 100644 index 0000000000..54e8e8e83f --- /dev/null +++ b/src/test/java/janggi/config/TestConnectionPool.java @@ -0,0 +1,12 @@ +package janggi.config; + +public class TestConnectionPool { + public static ConnectionPool create() { + return new ConnectionPool( + "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", + "sa", + "", + 10 + ); + } +} diff --git a/src/test/java/janggi/config/TestDBInitializer.java b/src/test/java/janggi/config/TestDBInitializer.java new file mode 100644 index 0000000000..545e96cb9b --- /dev/null +++ b/src/test/java/janggi/config/TestDBInitializer.java @@ -0,0 +1,46 @@ +package janggi.config; + +import java.io.IOException; +import java.io.InputStream; +import java.sql.SQLException; +import java.sql.Statement; + +public class TestDBInitializer { + + public static void initSchema(ConnectionPool pool) { + String sql = readSqlFile("/schema.sql"); + + try (PooledConnection pooled = pool.getPooledConnection(); + Statement stmt = pooled.getConnection().createStatement()) { + for (String query : sql.split(";")) { + String trimmed = query.trim(); + if (!trimmed.isEmpty()) { + stmt.execute(trimmed); + } + } + } catch (SQLException e) { + throw new RuntimeException("테스트 스키마 초기화 실패", e); + } + } + + private static String readSqlFile(String path) { + try (InputStream is = TestDBInitializer.class.getResourceAsStream(path)) { + if (is == null) { + throw new RuntimeException("SQL 파일을 찾을 수 없습니다: " + path); + } + return new String(is.readAllBytes()); + } catch (IOException e) { + throw new RuntimeException("SQL 파일 읽기 실패", e); + } + } + + public static void clearAll(ConnectionPool pool) { + try (PooledConnection pooled = pool.getPooledConnection(); + Statement stmt = pooled.getConnection().createStatement()) { + stmt.execute("DELETE FROM piece"); + stmt.execute("DELETE FROM game"); + } catch (SQLException e) { + throw new RuntimeException("테스트 데이터 정리 실패", e); + } + } +} diff --git a/src/test/java/janggi/domain/JanggiTest.java b/src/test/java/janggi/domain/JanggiTest.java index e82f48a591..22a22a9782 100644 --- a/src/test/java/janggi/domain/JanggiTest.java +++ b/src/test/java/janggi/domain/JanggiTest.java @@ -1,5 +1,7 @@ package janggi.domain; +import janggi.domain.board.BoardFactory; +import janggi.domain.board.FormationStrategyFactory; import janggi.domain.position.Position; import org.junit.jupiter.api.Test; @@ -11,7 +13,9 @@ class JanggiTest { @Test void 자신의_턴일때_상대의_기물을_선택하면_예외_처리한다() { - Janggi janggi = Janggi.start(1, 1); + Janggi janggi = Janggi.start(BoardFactory.create( + FormationStrategyFactory.from(1), + FormationStrategyFactory.from(1))); assertThatThrownBy(() -> janggi.play(Position.of(9, 0), Position.of(1, 0))) .isInstanceOf(IllegalArgumentException.class) @@ -20,14 +24,18 @@ class JanggiTest { @Test void 자신의_턴일때_정상_입력하면_다음_턴으로_넘어간다() { - Janggi janggi = Janggi.start(1, 1); + Janggi janggi = Janggi.start(BoardFactory.create( + FormationStrategyFactory.from(1), + FormationStrategyFactory.from(1))); janggi.play(Position.of(0, 0), Position.of(2, 0)); assertThat(janggi.currentCamp().isCho()).isFalse(); } @Test void 장기_게임이_끝나지_않으면_true를_반환한다() { - Janggi janggi = Janggi.start(1, 1); + Janggi janggi = Janggi.start(BoardFactory.create( + FormationStrategyFactory.from(1), + FormationStrategyFactory.from(1))); boolean running = janggi.isRunning(); assertThat(running).isTrue(); @@ -35,7 +43,9 @@ class JanggiTest { @Test void 장기_게임이_끝나면_false로_변환한다() { - Janggi janggi = Janggi.start(1, 1); + Janggi janggi = Janggi.start(BoardFactory.create( + FormationStrategyFactory.from(1), + FormationStrategyFactory.from(1))); janggi.finish(); boolean running = janggi.isRunning(); diff --git a/src/test/java/janggi/domain/PalaceTest.java b/src/test/java/janggi/domain/PalaceTest.java new file mode 100644 index 0000000000..724690c3f6 --- /dev/null +++ b/src/test/java/janggi/domain/PalaceTest.java @@ -0,0 +1,106 @@ +package janggi.domain; + +import janggi.domain.position.Direction; +import janggi.domain.position.Position; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class PalaceTest { + + static Stream palaceTestCases() { + return Stream.of( + // 초나라 궁성 + Arguments.of(Palace.cho(), Position.of(0, 3), List.of(Direction.UP_RIGHT)), + Arguments.of(Palace.cho(), Position.of(0, 5), List.of(Direction.UP_LEFT)), + Arguments.of(Palace.cho(), Position.of(2, 3), List.of(Direction.DOWN_RIGHT)), + Arguments.of(Palace.cho(), Position.of(2, 5), List.of(Direction.DOWN_LEFT)), + Arguments.of(Palace.cho(), Position.of(0, 4), List.of()), + Arguments.of(Palace.cho(), Position.of(1, 3), List.of()), + Arguments.of(Palace.cho(), Position.of(1, 5), List.of()), + Arguments.of(Palace.cho(), Position.of(2, 4), List.of()), + Arguments.of(Palace.cho(), Position.of(1, 4), Direction.diagonal()), + + // 한나라 궁성 + Arguments.of(Palace.han(), Position.of(7, 3), List.of(Direction.UP_RIGHT)), + Arguments.of(Palace.han(), Position.of(7, 5), List.of(Direction.UP_LEFT)), + Arguments.of(Palace.han(), Position.of(9, 3), List.of(Direction.DOWN_RIGHT)), + Arguments.of(Palace.han(), Position.of(9, 5), List.of(Direction.DOWN_LEFT)), + Arguments.of(Palace.han(), Position.of(9, 4), List.of()), + Arguments.of(Palace.han(), Position.of(8, 3), List.of()), + Arguments.of(Palace.han(), Position.of(8, 5), List.of()), + Arguments.of(Palace.han(), Position.of(7, 4), List.of()), + Arguments.of(Palace.han(), Position.of(8, 4), Direction.diagonal()) + ); + } + + @ParameterizedTest + @CsvSource(value = { + "0, 3", "0, 4", "0, 5", + "1, 3", "1, 4", "1, 5", + "2, 3", "2, 4", "2, 5" + }) + void 포지션이_초나라_궁성에_존재하면_true를_반환한다(int row, int column) { + + Palace cho = Palace.cho(); + + boolean result = cho.contains(Position.of(row, column)); + + assertThat(result).isTrue(); + } + + @ParameterizedTest + @CsvSource(value = { + "7, 3", "7, 4", "7, 5", + "8, 3", "8, 4", "8, 5", + "9, 3", "9, 4", "9, 5" + }) + void 포지션이_한나라_궁성에_존재하면_true를_반환한다(int row, int column) { + Palace han = Palace.han(); + + boolean result = han.contains(Position.of(row, column)); + + assertThat(result).isTrue(); + } + + @ParameterizedTest + @CsvSource(value = { + "0, 2", "3, 3", "0, 6", + "6, 4", "7, 2", "9, 6" + }) + void 포지션이_궁성_밖에_존재하면_false를_반환한다(int row, int column) { + boolean isChoContains = Palace.cho().contains(Position.of(row, column)); + boolean isHanContains = Palace.han().contains(Position.of(row, column)); + + assertThat(isChoContains).isFalse(); + assertThat(isHanContains).isFalse(); + } + + @ParameterizedTest + @CsvSource(value = { + "0, 2", "3, 3", "0, 6", + "1, 2", "3, 4", "1, 6", + "2, 2", "3, 5", "2, 6" + }) + void 포지션이_궁성_안에_없다면_빈_리스트를_반환한다(int row, int column) { + Palace cho = Palace.cho(); + + List directions = cho.diagonalDirectionsAt(Position.of(row, column)); + + assertThat(directions).isEqualTo(List.of()); + } + + @ParameterizedTest + @MethodSource("palaceTestCases") + void 궁성_위치에_따라_올바른_대각선을_반환한다(Palace palace, Position position, List expected) { + List directions = palace.diagonalDirectionsAt(position); + + assertThat(directions).containsExactlyInAnyOrderElementsOf(expected); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/PalacesTest.java b/src/test/java/janggi/domain/PalacesTest.java new file mode 100644 index 0000000000..7896750dcc --- /dev/null +++ b/src/test/java/janggi/domain/PalacesTest.java @@ -0,0 +1,51 @@ +package janggi.domain; + +import janggi.domain.position.Direction; +import janggi.domain.position.Position; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +class PalacesTest { + + private final Palaces palaces = Palaces.of(); + + @ParameterizedTest + @CsvSource(value = { + "1, 4", + "8, 4" + }) + void 궁성_안_중앙이면_대각선_4방향을_반환한다(int row, int column) { + List directions = palaces.diagonalDirectionsAt(Position.of(row, column)); + + assertThat(directions).containsExactlyInAnyOrderElementsOf(Direction.diagonal()); + } + + @ParameterizedTest + @CsvSource(value = { + "4, 4", + "5, 5", + "3, 3" + }) + void 궁성_밖이면_빈_리스트를_반환한다(int row, int column) { + List directions = palaces.diagonalDirectionsAt(Position.of(row, column)); + + assertThat(directions).isEmpty(); + } + + @ParameterizedTest + @CsvSource(value = { + "1, 4, true", + "8, 4, true", + "4, 4, false", + "0, 0, false" + }) + void 궁성_안에_포함되는지_확인한다(int row, int column, boolean expected) { + boolean result = palaces.containsAny(Position.of(row, column)); + + assertThat(result).isEqualTo(expected); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/ScoreTest.java b/src/test/java/janggi/domain/ScoreTest.java new file mode 100644 index 0000000000..6f04dccb59 --- /dev/null +++ b/src/test/java/janggi/domain/ScoreTest.java @@ -0,0 +1,55 @@ +package janggi.domain; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ScoreTest { + + @Test + void 점수는_음수가_될_수_없다() { + assertThatThrownBy(() -> new Score(-1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("점수는 음수가 될 수 없습니다."); + } + + @Test + void 점수끼리_더하면_합산된_점수를_반환한다() { + Score score1 = new Score(1); + Score score2 = new Score(1); + + Score result = score1.plus(score2); + + assertThat(result).isEqualTo(new Score(2)); + } + + @Test + void 점수가_더_크면_true를_반환한다() { + Score higher = new Score(2); + Score lower = new Score(1); + + boolean result = higher.isGreaterThan(lower); + + assertThat(result).isTrue(); + } + + @Test + void 점수가_더_작으면_false를_반환한다() { + Score lower = new Score(1); + Score higher = new Score(2); + + boolean result = lower.isGreaterThan(higher); + + assertThat(result).isFalse(); + } + + @Test + void 점수_객체에_배율을_곱해_새로운_점수를_생성한다() { + Score score = new Score(5); + + Score multiply = score.multiply(1.5); + + assertThat(multiply.isGreaterThan(score)).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/board/BoardTest.java b/src/test/java/janggi/domain/board/BoardTest.java index 99d40529cd..66d3a0f7ec 100644 --- a/src/test/java/janggi/domain/board/BoardTest.java +++ b/src/test/java/janggi/domain/board/BoardTest.java @@ -14,6 +14,8 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import java.util.Map; @@ -139,4 +141,35 @@ void initializeToBoard_Always_ReturnCorrectBoard() { assertThat(movedPiece.isSameCamp(Camp.CHO)).isTrue(); assertThat(board.janggiBoard()).hasSize(31); } + + @ParameterizedTest + @EnumSource(Camp.class) + void 선택한_나라의_장이_있으면_true를_반환한다(Camp camp) { + Board board = BoardFactory.create(new ElephantHorseElephantHorse(), new ElephantHorseElephantHorse()); + + boolean essentialPiece = board.isAliveEssentialPiece(camp); + + assertThat(essentialPiece).isTrue(); + } + + @Test + void 한나라보다_초나라의_기물_점수가_더_크면_초나라가_이긴다() { + Board board = BoardFactory.create(new ElephantHorseElephantHorse(), new ElephantHorseElephantHorse()); + board.movePiece(Position.of(3, 0), Position.of(3, 1)); + board.movePiece(Position.of(0, 0), Position.of(6, 0)); + board.movePiece(Position.of(6, 0), Position.of(9, 0)); + board.movePiece(Position.of(9, 0), Position.of(9, 1)); + board.movePiece(Position.of(9, 1), Position.of(9, 2)); + board.movePiece(Position.of(9, 2), Position.of(9, 3)); + board.movePiece(Position.of(9, 3), Position.of(9, 5)); + + assertThat(board.calculateScoreResult()).isEqualTo(Camp.CHO); + } + + @Test + void 초나라보다_한나라의_기물_점수가_더_크면_한나라가_이긴다() { + Board board = BoardFactory.create(new ElephantHorseElephantHorse(), new ElephantHorseElephantHorse()); + + assertThat(board.calculateScoreResult()).isEqualTo(Camp.HAN); + } } diff --git a/src/test/java/janggi/domain/piece/AdvisorTest.java b/src/test/java/janggi/domain/piece/AdvisorTest.java index b4bec264d0..11bdab22fb 100644 --- a/src/test/java/janggi/domain/piece/AdvisorTest.java +++ b/src/test/java/janggi/domain/piece/AdvisorTest.java @@ -3,8 +3,6 @@ import janggi.domain.Camp; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; import java.util.HashMap; @@ -47,4 +45,12 @@ void canCatch_DestinationPieceIsNotSameCamp_ReturnTrue() { Piece desinationPiece = new Chariot(Camp.HAN); assertThat(piece.canCatch(desinationPiece)).isTrue(); } + + @Test + void 필수적인_기물이_아니면_false를_출력한다() { + Piece piece = new Advisor(Camp.CHO); + boolean essential = piece.isEssential(); + + assertThat(essential).isFalse(); + } } diff --git a/src/test/java/janggi/domain/piece/CannonTest.java b/src/test/java/janggi/domain/piece/CannonTest.java index 2957a670f0..7fd209f332 100644 --- a/src/test/java/janggi/domain/piece/CannonTest.java +++ b/src/test/java/janggi/domain/piece/CannonTest.java @@ -84,4 +84,12 @@ void canCatch_DestinationPieceIsNotCannonAndIsNotSameCamp_ReturnTrue() { Piece destinationPiece = new Elephant(Camp.HAN); assertThat(piece.canCatch(destinationPiece)).isTrue(); } + + @Test + void 필수적인_기물이_아니면_false를_출력한다() { + Piece piece = new Cannon(Camp.CHO); + boolean essential = piece.isEssential(); + + assertThat(essential).isFalse(); + } } diff --git a/src/test/java/janggi/domain/piece/ChariotTest.java b/src/test/java/janggi/domain/piece/ChariotTest.java index 8bccc195e0..b6366fabca 100644 --- a/src/test/java/janggi/domain/piece/ChariotTest.java +++ b/src/test/java/janggi/domain/piece/ChariotTest.java @@ -58,4 +58,12 @@ void canCatch_DestinationPieceIsNotSameCamp_ReturnTrue() { Piece destinationPiece = new Elephant(Camp.HAN); assertThat(piece.canCatch(destinationPiece)).isTrue(); } + + @Test + void 필수적인_기물이_아니면_false를_출력한다() { + Piece piece = new Chariot(Camp.CHO); + boolean essential = piece.isEssential(); + + assertThat(essential).isFalse(); + } } diff --git a/src/test/java/janggi/domain/piece/ElephantTest.java b/src/test/java/janggi/domain/piece/ElephantTest.java index da48613ad5..a766ba54c4 100644 --- a/src/test/java/janggi/domain/piece/ElephantTest.java +++ b/src/test/java/janggi/domain/piece/ElephantTest.java @@ -66,4 +66,12 @@ void canBeJumpedOver() { assertThat(canCatch).isEqualTo(true); } + + @Test + void 필수적인_기물이_아니면_false를_출력한다() { + Piece piece = new Elephant(Camp.CHO); + boolean essential = piece.isEssential(); + + assertThat(essential).isFalse(); + } } diff --git a/src/test/java/janggi/domain/piece/GeneralTest.java b/src/test/java/janggi/domain/piece/GeneralTest.java index c962240f06..d530516980 100644 --- a/src/test/java/janggi/domain/piece/GeneralTest.java +++ b/src/test/java/janggi/domain/piece/GeneralTest.java @@ -45,4 +45,12 @@ void canCatch_DestinationPieceIsNotSameCamp_ReturnTrue() { Piece desinationPiece = new Chariot(Camp.HAN); assertThat(piece.canCatch(desinationPiece)).isTrue(); } + + @Test + void 필수적인_기물이면_true를_출력한다() { + Piece piece = new General(Camp.CHO); + boolean essential = piece.isEssential(); + + assertThat(essential).isTrue(); + } } diff --git a/src/test/java/janggi/domain/piece/HorseTest.java b/src/test/java/janggi/domain/piece/HorseTest.java index f37bae78cc..f92ec6f9ff 100644 --- a/src/test/java/janggi/domain/piece/HorseTest.java +++ b/src/test/java/janggi/domain/piece/HorseTest.java @@ -66,4 +66,12 @@ void canBeJumpedOver() { assertThat(canCatch).isEqualTo(true); } + + @Test + void 필수적인_기물이_아니면_false를_출력한다() { + Piece piece = new Horse(Camp.CHO); + boolean essential = piece.isEssential(); + + assertThat(essential).isFalse(); + } } diff --git a/src/test/java/janggi/domain/piece/SoldierTest.java b/src/test/java/janggi/domain/piece/SoldierTest.java index 60121683ec..c21a257514 100644 --- a/src/test/java/janggi/domain/piece/SoldierTest.java +++ b/src/test/java/janggi/domain/piece/SoldierTest.java @@ -45,4 +45,12 @@ void canCatchPiece_ReturnBoolean(Camp destinationPieceCamp, boolean expected) { Soldier destinationSoldier = new Soldier(destinationPieceCamp); assertThat(soldier.canCatch(destinationSoldier)).isEqualTo(expected); } + + @Test + void 필수적인_기물이_아니면_false를_출력한다() { + Piece piece = new Soldier(Camp.CHO); + boolean essential = piece.isEssential(); + + assertThat(essential).isFalse(); + } } diff --git a/src/test/java/janggi/domain/piece/strategy/ElephantStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/ElephantStrategyTest.java new file mode 100644 index 0000000000..ada1f2d00a --- /dev/null +++ b/src/test/java/janggi/domain/piece/strategy/ElephantStrategyTest.java @@ -0,0 +1,25 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Paths; +import janggi.domain.position.Position; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ElephantStrategyTest { + + @Test + void 상은_직선_한칸_후_대각선_두칸으로_이동한다() { + MoveStrategy strategy = ElephantStrategy.getInstance(); + Paths paths = strategy.findMovablePaths(Position.of(4, 4)); + + assertThat(paths.findPathByDestination(Position.of(7, 6)).isDestination(Position.of(7, 6))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(6, 7)).isDestination(Position.of(6, 7))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(2, 7)).isDestination(Position.of(2, 7))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(1, 6)).isDestination(Position.of(1, 6))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(7, 2)).isDestination(Position.of(7, 2))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(6, 1)).isDestination(Position.of(6, 1))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(2, 1)).isDestination(Position.of(2, 1))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(1, 2)).isDestination(Position.of(1, 2))).isTrue(); + } +} diff --git a/src/test/java/janggi/domain/piece/strategy/HorseStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/HorseStrategyTest.java new file mode 100644 index 0000000000..2219b20063 --- /dev/null +++ b/src/test/java/janggi/domain/piece/strategy/HorseStrategyTest.java @@ -0,0 +1,25 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Paths; +import janggi.domain.position.Position; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HorseStrategyTest { + + @Test + void 마는_직선_한칸_후_대각선_한칸으로_이동한다() { + MoveStrategy strategy = HorseStrategy.getInstance(); + Paths paths = strategy.findMovablePaths(Position.of(4, 4)); + + assertThat(paths.findPathByDestination(Position.of(6, 5)).isDestination(Position.of(6, 5))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(5, 6)).isDestination(Position.of(5, 6))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(3, 6)).isDestination(Position.of(3, 6))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(2, 5)).isDestination(Position.of(2, 5))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(6, 3)).isDestination(Position.of(6, 3))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(5, 2)).isDestination(Position.of(5, 2))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(3, 2)).isDestination(Position.of(3, 2))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(2, 3)).isDestination(Position.of(2, 3))).isTrue(); + } +} diff --git a/src/test/java/janggi/domain/piece/strategy/LinearStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/LinearStrategyTest.java new file mode 100644 index 0000000000..90dc9046c8 --- /dev/null +++ b/src/test/java/janggi/domain/piece/strategy/LinearStrategyTest.java @@ -0,0 +1,56 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Palaces; +import janggi.domain.Paths; +import janggi.domain.position.Position; +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; + +class LinearStrategyTest { + + @Test + @DisplayName("직선 4방향을 갈 수있다.") + void findMovablePaths_ReturnAllLinearCandidates() { + MoveStrategy strategy = new LinearStrategy(Palaces.of()); + Paths paths = strategy.findMovablePaths(Position.of(4, 4)); + + for (int c = 0; c <= 8; c++) { + if (c == 4) { + continue; + } + assertThat(paths.findPathByDestination(Position.of(4, c)).isDestination(Position.of(4, c))).isTrue(); + } + for (int r = 0; r <= 9; r++) { + if (r == 4) { + continue; + } + assertThat(paths.findPathByDestination(Position.of(r, 4)).isDestination(Position.of(r, 4))).isTrue(); + } + + assertThatThrownBy(() -> paths.findPathByDestination(Position.of(4, 4))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 좌표입니다."); + } + + @Test + void 궁성_중앙이라면_4방향의_대각선으로_이동이_가능하다() { + MoveStrategy strategy = new LinearStrategy(Palaces.of()); + Paths paths = strategy.findMovablePaths(Position.of(1, 4)); + + assertThat(paths.findPathByDestination(Position.of(2, 3)).isDestination(Position.of(2, 3))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(2, 5)).isDestination(Position.of(2, 5))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(0, 3)).isDestination(Position.of(0, 3))).isTrue(); + assertThat(paths.findPathByDestination(Position.of(0, 5)).isDestination(Position.of(0, 5))).isTrue(); + } + + @Test + void 궁성_꼭짓점이라면_1방향의_대각선으로_이동이_가능하다() { + MoveStrategy strategy = new LinearStrategy(Palaces.of()); + Paths paths = strategy.findMovablePaths(Position.of(2, 3)); + + assertThat(paths.findPathByDestination(Position.of(1, 4)).isDestination(Position.of(1, 4))).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/piece/strategy/MoveStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/MoveStrategyTest.java deleted file mode 100644 index c1a89332c4..0000000000 --- a/src/test/java/janggi/domain/piece/strategy/MoveStrategyTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package janggi.domain.piece.strategy; - -import janggi.domain.Path; -import janggi.domain.Paths; -import janggi.domain.position.Position; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - -class MoveStrategyTest { - private static final int MIN_INDEX = 0; - private static final int MAX_ROW_INDEX = 9; - private static final int MAX_COL_INDEX = 8; - - @Nested - class 병_이동_테스트 { - @Test - void 초나라_병은_앞과_양_옆으로_움직인다() { - MoveStrategy strategy = new ChoSoldierStrategy(); - Paths paths = strategy.findMovablePaths(Position.of(4, 4)); - assertThat(paths.findPathByDestination(Position.of(5, 4))).isEqualTo(Path.of(Position.of(5, 4))); - assertThat(paths.findPathByDestination(Position.of(4, 5))).isEqualTo(Path.of(Position.of(4, 5))); - assertThat(paths.findPathByDestination(Position.of(4, 3))).isEqualTo(Path.of(Position.of(4, 3))); - } - - @Test - void 한나라_병은_앞과_양_옆으로_움직인다() { - MoveStrategy strategy = new HanSoldierStrategy(); - Paths paths = strategy.findMovablePaths(Position.of(4, 4)); - assertThat(paths.findPathByDestination(Position.of(3, 4))).isEqualTo(Path.of(Position.of(3, 4))); - assertThat(paths.findPathByDestination(Position.of(4, 5))).isEqualTo(Path.of(Position.of(4, 5))); - assertThat(paths.findPathByDestination(Position.of(4, 3))).isEqualTo(Path.of(Position.of(4, 3))); - } - } - - @Nested - class 궁_내부_이동_테스트 { - - @DisplayName("사는 현재 위치에서 앞, 뒤, 양 옆을 1칸씩의 좌표를 도착지점 후보로 반환한다") - @Test - void 사는_앞뒤_양옆으로_움직인다() { - MoveStrategy strategy = new PalaceStrategy(); - Paths paths = strategy.findMovablePaths(Position.of(4, 4)); - assertThat(paths.findPathByDestination(Position.of(5, 4))).isEqualTo(Path.of(Position.of(5, 4))); - assertThat(paths.findPathByDestination(Position.of(3, 4))).isEqualTo(Path.of(Position.of(3, 4))); - assertThat(paths.findPathByDestination(Position.of(4, 5))).isEqualTo(Path.of(Position.of(4, 5))); - assertThat(paths.findPathByDestination(Position.of(4, 3))).isEqualTo(Path.of(Position.of(4, 3))); - } - } - - @Nested - class 마_이동_테스트 { - - @Test - void 마는_직선_한칸_후_대각선_한칸으로_이동한다() { - MoveStrategy strategy = new HorseStrategy(); - Paths paths = strategy.findMovablePaths(Position.of(4, 4)); - - assertThat(paths.findPathByDestination(Position.of(6, 5)).isDestination(Position.of(6, 5))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(5, 6)).isDestination(Position.of(5, 6))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(3, 6)).isDestination(Position.of(3, 6))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(2, 5)).isDestination(Position.of(2, 5))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(6, 3)).isDestination(Position.of(6, 3))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(5, 2)).isDestination(Position.of(5, 2))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(3, 2)).isDestination(Position.of(3, 2))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(2, 3)).isDestination(Position.of(2, 3))).isTrue(); - } - } - - @Nested - class 상_이동_테스트 { - - @Test - void 상은_직선_한칸_후_대각선_두칸으로_이동한다() { - MoveStrategy strategy = new ElephantStrategy(); - Paths paths = strategy.findMovablePaths(Position.of(4, 4)); - - assertThat(paths.findPathByDestination(Position.of(7, 6)).isDestination(Position.of(7, 6))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(6, 7)).isDestination(Position.of(6, 7))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(2, 7)).isDestination(Position.of(2, 7))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(1, 6)).isDestination(Position.of(1, 6))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(7, 2)).isDestination(Position.of(7, 2))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(6, 1)).isDestination(Position.of(6, 1))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(2, 1)).isDestination(Position.of(2, 1))).isTrue(); - assertThat(paths.findPathByDestination(Position.of(1, 2)).isDestination(Position.of(1, 2))).isTrue(); - } - } - - @Nested - class 직선_이동_테스트 { - @Test - @DisplayName("포는 현재 위치에서 가로와 세로 직선상의 모든 좌표를 후보로 반환한다") - void findMovablePaths_ReturnAllLinearCandidates() { - MoveStrategy strategy = new LinearStrategy(); - Paths paths = strategy.findMovablePaths(Position.of(4, 4)); - - for (int c = 0; c <= 8; c++) { - if (c == 4) { - continue; - } - assertThat(paths.findPathByDestination(Position.of(4, c)).isDestination(Position.of(4, c))).isTrue(); - } - for (int r = 0; r <= 9; r++) { - if (r == 4) { - continue; - } - assertThat(paths.findPathByDestination(Position.of(r, 4)).isDestination(Position.of(r, 4))).isTrue(); - } - - assertThatThrownBy(() -> paths.findPathByDestination(Position.of(4, 4))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("이동할 수 없는 좌표입니다."); - } - } -} diff --git a/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java new file mode 100644 index 0000000000..450f33e641 --- /dev/null +++ b/src/test/java/janggi/domain/piece/strategy/PalaceStrategyTest.java @@ -0,0 +1,67 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Path; +import janggi.domain.Paths; +import janggi.domain.position.Position; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class PalaceStrategyTest { + + @Nested + class 초나라_궁_내부_기물_이동_테스트 { + + @Test + void 초나라_궁성_내부_기물은_궁성_내부에서_앞뒤_양옆_대각선으로_움직인다() { + MoveStrategy strategy = ChoPalaceStrategy.getInstance(); + Paths paths = strategy.findMovablePaths(Position.of(1, 4)); + assertThat(paths.findPathByDestination(Position.of(0, 4))).isEqualTo(Path.of(Position.of(0, 4))); + assertThat(paths.findPathByDestination(Position.of(2, 4))).isEqualTo(Path.of(Position.of(2, 4))); + assertThat(paths.findPathByDestination(Position.of(1, 5))).isEqualTo(Path.of(Position.of(1, 5))); + assertThat(paths.findPathByDestination(Position.of(1, 3))).isEqualTo(Path.of(Position.of(1, 3))); + assertThat(paths.findPathByDestination(Position.of(2, 3))).isEqualTo(Path.of(Position.of(2, 3))); + assertThat(paths.findPathByDestination(Position.of(2, 5))).isEqualTo(Path.of(Position.of(2, 5))); + assertThat(paths.findPathByDestination(Position.of(0, 3))).isEqualTo(Path.of(Position.of(0, 3))); + assertThat(paths.findPathByDestination(Position.of(0, 5))).isEqualTo(Path.of(Position.of(0, 5))); + } + + @Test + void 초나라_궁성_기물은_궁성_외부에서_움직일_수_없다() { + MoveStrategy strategy = ChoPalaceStrategy.getInstance(); + Paths paths = strategy.findMovablePaths(Position.of(4, 4)); + assertThatThrownBy(() -> paths.findPathByDestination(Position.of(5, 4))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 좌표입니다."); + } + } + + @Nested + class 한나라_궁_내부_기물_이동_테스트 { + + @Test + void 한나라_궁성_내부_기물은_궁성_내부에서_앞뒤_양옆_대각선으로_움직인다() { + MoveStrategy strategy = HanPalaceStrategy.getInstance(); + Paths paths = strategy.findMovablePaths(Position.of(8, 4)); + assertThat(paths.findPathByDestination(Position.of(9, 4))).isEqualTo(Path.of(Position.of(9, 4))); + assertThat(paths.findPathByDestination(Position.of(7, 4))).isEqualTo(Path.of(Position.of(7, 4))); + assertThat(paths.findPathByDestination(Position.of(8, 5))).isEqualTo(Path.of(Position.of(8, 5))); + assertThat(paths.findPathByDestination(Position.of(8, 3))).isEqualTo(Path.of(Position.of(8, 3))); + assertThat(paths.findPathByDestination(Position.of(7, 3))).isEqualTo(Path.of(Position.of(7, 3))); + assertThat(paths.findPathByDestination(Position.of(7, 5))).isEqualTo(Path.of(Position.of(7, 5))); + assertThat(paths.findPathByDestination(Position.of(9, 3))).isEqualTo(Path.of(Position.of(9, 3))); + assertThat(paths.findPathByDestination(Position.of(9, 5))).isEqualTo(Path.of(Position.of(9, 5))); + } + + @Test + void 한나라_궁성_기물은_궁성_외부에서_움직일_수_없다() { + MoveStrategy strategy = HanPalaceStrategy.getInstance(); + Paths paths = strategy.findMovablePaths(Position.of(4, 4)); + assertThatThrownBy(() -> paths.findPathByDestination(Position.of(5, 4))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이동할 수 없는 좌표입니다."); + } + } +} diff --git a/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java b/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java new file mode 100644 index 0000000000..95de6fb6fe --- /dev/null +++ b/src/test/java/janggi/domain/piece/strategy/SoldierStrategyTest.java @@ -0,0 +1,90 @@ +package janggi.domain.piece.strategy; + +import janggi.domain.Path; +import janggi.domain.Paths; +import janggi.domain.position.Position; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SoldierStrategyTest { + + @Nested + class 초나라_병_이동_테스트 { + @Test + void 초나라_병은_앞과_양_옆으로_움직인다() { + MoveStrategy strategy = ChoSoldierStrategy.getInstance(); + Paths paths = strategy.findMovablePaths(Position.of(4, 4)); + assertThat(paths.findPathByDestination(Position.of(5, 4))).isEqualTo(Path.of(Position.of(5, 4))); + assertThat(paths.findPathByDestination(Position.of(4, 5))).isEqualTo(Path.of(Position.of(4, 5))); + assertThat(paths.findPathByDestination(Position.of(4, 3))).isEqualTo(Path.of(Position.of(4, 3))); + } + + @Test + void 초나라_병이_적진_궁성_중앙에_있을_때_전진_방향_대각선_두_곳을_경로에_포함한다() { + MoveStrategy strategy = ChoSoldierStrategy.getInstance(); + Position center = Position.of(8, 4); + + Paths paths = strategy.findMovablePaths(center); + + assertThat(paths.findPathByDestination(Position.of(9, 5))).isEqualTo(Path.of(Position.of(9, 5))); + assertThat(paths.findPathByDestination(Position.of(9, 4))).isEqualTo(Path.of(Position.of(9, 4))); + assertThat(paths.findPathByDestination(Position.of(9, 3))).isEqualTo(Path.of(Position.of(9, 3))); + assertThat(paths.findPathByDestination(Position.of(8, 5))).isEqualTo(Path.of(Position.of(8, 5))); + assertThat(paths.findPathByDestination(Position.of(8, 3))).isEqualTo(Path.of(Position.of(8, 3))); + } + + @Test + void 초나라_병이_적진_궁성_상단_모서리에_있을_때_중앙으로_향하는_대각선을_포함한다() { + MoveStrategy strategy = ChoSoldierStrategy.getInstance(); + Position center = Position.of(7, 3); + + Paths paths = strategy.findMovablePaths(center); + + assertThat(paths.findPathByDestination(Position.of(8, 4))).isEqualTo(Path.of(Position.of(8, 4))); + assertThat(paths.findPathByDestination(Position.of(8, 3))).isEqualTo(Path.of(Position.of(8, 3))); + assertThat(paths.findPathByDestination(Position.of(7, 2))).isEqualTo(Path.of(Position.of(7, 2))); + assertThat(paths.findPathByDestination(Position.of(7, 4))).isEqualTo(Path.of(Position.of(7, 4))); + } + } + + @Nested + class 한나라_병_이동_테스트 { + @Test + void 한나라_병은_앞과_양_옆으로_움직인다() { + MoveStrategy strategy = HanSoldierStrategy.getInstance(); + Paths paths = strategy.findMovablePaths(Position.of(4, 4)); + assertThat(paths.findPathByDestination(Position.of(3, 4))).isEqualTo(Path.of(Position.of(3, 4))); + assertThat(paths.findPathByDestination(Position.of(4, 5))).isEqualTo(Path.of(Position.of(4, 5))); + assertThat(paths.findPathByDestination(Position.of(4, 3))).isEqualTo(Path.of(Position.of(4, 3))); + } + + @Test + void 한나라_병이_적진_궁성_중앙에_있을_때_전진_방향_대각선_두_곳을_경로에_포함한다() { + MoveStrategy strategy = HanSoldierStrategy.getInstance(); + Position center = Position.of(1, 4); + + Paths paths = strategy.findMovablePaths(center); + + assertThat(paths.findPathByDestination(Position.of(0, 5))).isEqualTo(Path.of(Position.of(0, 5))); + assertThat(paths.findPathByDestination(Position.of(0, 4))).isEqualTo(Path.of(Position.of(0, 4))); + assertThat(paths.findPathByDestination(Position.of(0, 3))).isEqualTo(Path.of(Position.of(0, 3))); + assertThat(paths.findPathByDestination(Position.of(1, 5))).isEqualTo(Path.of(Position.of(1, 5))); + assertThat(paths.findPathByDestination(Position.of(1, 3))).isEqualTo(Path.of(Position.of(1, 3))); + } + + @Test + void 한나라_병이_적진_궁성_상단_모서리에_있을_때_중앙으로_향하는_대각선을_포함한다() { + MoveStrategy strategy = HanSoldierStrategy.getInstance(); + Position center = Position.of(2, 3); + + Paths paths = strategy.findMovablePaths(center); + + assertThat(paths.findPathByDestination(Position.of(1, 4))).isEqualTo(Path.of(Position.of(1, 4))); + assertThat(paths.findPathByDestination(Position.of(1, 3))).isEqualTo(Path.of(Position.of(1, 3))); + assertThat(paths.findPathByDestination(Position.of(2, 2))).isEqualTo(Path.of(Position.of(2, 2))); + assertThat(paths.findPathByDestination(Position.of(2, 4))).isEqualTo(Path.of(Position.of(2, 4))); + } + } +} diff --git a/src/test/java/janggi/domain/position/ColumnTest.java b/src/test/java/janggi/domain/position/ColumnTest.java index 34d8c4981e..c6f3f0af19 100644 --- a/src/test/java/janggi/domain/position/ColumnTest.java +++ b/src/test/java/janggi/domain/position/ColumnTest.java @@ -11,14 +11,14 @@ class ColumnTest { @ParameterizedTest @CsvSource({"0", "8"}) void 열은_0과_8_사이_값이다(int input) { - Column column = new Column(input); + Column column = Column.from(input); Assertions.assertThat(column.value()).isEqualTo(input); } @ParameterizedTest @CsvSource({"-1", "9"}) void 열에_0과_8_범위_밖의_값이_들어오면_예외처리한다(int input) { - assertThatThrownBy(() -> new Column(input)) + assertThatThrownBy(() -> Column.from(input)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("열은 0 ~ 8 입니다."); } diff --git a/src/test/java/janggi/domain/position/DirectionTest.java b/src/test/java/janggi/domain/position/DirectionTest.java new file mode 100644 index 0000000000..3f5fb73c76 --- /dev/null +++ b/src/test/java/janggi/domain/position/DirectionTest.java @@ -0,0 +1,35 @@ +package janggi.domain.position; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class DirectionTest { + + @Test + void 행이_0보다_크면_위_방향이다() { + Direction upDirection = Direction.UP; + boolean up = upDirection.isUp(); + + Assertions.assertThat(up).isTrue(); + } + + @Test + void 행이_0보다_작으면_아래_방향이다() { + Direction upDirection = Direction.DOWN; + boolean up = upDirection.isDown(); + + Assertions.assertThat(up).isTrue(); + } + + @Test + void 대각선_위_방향도_isUp이_true이다() { + Assertions.assertThat(Direction.UP_RIGHT.isUp()).isTrue(); + Assertions.assertThat(Direction.UP_LEFT.isUp()).isTrue(); + } + + @Test + void 대각선_아래_방향도_isDown이_true이다() { + Assertions.assertThat(Direction.DOWN_RIGHT.isDown()).isTrue(); + Assertions.assertThat(Direction.DOWN_LEFT.isDown()).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/domain/position/PositionTest.java b/src/test/java/janggi/domain/position/PositionTest.java index fe9c05351f..4811176925 100644 --- a/src/test/java/janggi/domain/position/PositionTest.java +++ b/src/test/java/janggi/domain/position/PositionTest.java @@ -20,8 +20,7 @@ public class PositionTest { }) void of_OutOfRangePosition_ThrowException(int row, int column) { assertThatThrownBy(() -> Position.of(row, column)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("잘못된 좌표입니다."); + .isInstanceOf(IllegalArgumentException.class); } @Test diff --git a/src/test/java/janggi/domain/position/RowTest.java b/src/test/java/janggi/domain/position/RowTest.java index d572462b18..754c021ec7 100644 --- a/src/test/java/janggi/domain/position/RowTest.java +++ b/src/test/java/janggi/domain/position/RowTest.java @@ -10,14 +10,14 @@ class RowTest { @ParameterizedTest @CsvSource({"0", "9"}) void 행은_0과_9_사이_값이다(int input) { - Row row = new Row(input); + Row row = Row.from(input); Assertions.assertThat(row.value()).isEqualTo(input); } @ParameterizedTest @CsvSource({"-1", "10"}) void 행에_0과_9_범위_밖의_값이_들어오면_예외처리한다(int input) { - assertThatThrownBy(() -> new Row(input)) + assertThatThrownBy(() -> Row.from(input)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("행은 0 ~ 9 입니다."); } diff --git a/src/test/java/janggi/persistence/GameRepositoryImplTest.java b/src/test/java/janggi/persistence/GameRepositoryImplTest.java new file mode 100644 index 0000000000..d303466126 --- /dev/null +++ b/src/test/java/janggi/persistence/GameRepositoryImplTest.java @@ -0,0 +1,72 @@ +package janggi.persistence; + +import janggi.domain.Janggi; +import janggi.domain.board.BoardFactory; +import janggi.domain.board.FormationStrategyFactory; +import janggi.persistence.dao.FakeGameDao; +import janggi.persistence.dao.FakePieceDao; +import janggi.persistence.mapper.GameMapper; +import janggi.persistence.mapper.PieceMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class GameRepositoryImplTest { + private GameRepositoryImpl repository; + private FakeGameDao fakeGameDao; + + @BeforeEach + void setUp() { + fakeGameDao = new FakeGameDao(); + repository = new GameRepositoryImpl( + fakeGameDao, + new FakePieceDao(), + new GameMapper(), + new PieceMapper() + ); + } + + @Test + void 게임을_저장하고_조회할_수_있다() { + Janggi janggi = createTestJanggi(); + + String gameId = repository.save(null, "테스트게임", janggi); + + Optional result = repository.findById(gameId); + assertThat(result).isPresent(); + } + + @Test + void 게임을_삭제하면_조회되지_않는다() { + Janggi janggi = createTestJanggi(); + + String gameId = repository.save(null, "삭제게임", janggi); + repository.deleteById(null, gameId); + + Optional result = repository.findById(gameId); + assertThat(result).isEmpty(); + } + + @Test + void 같은_게임을_두번_조회하면_캐시에서_반환한다() { + Janggi janggi = createTestJanggi(); + + String gameId = repository.save(null, "캐시게임", janggi); + + repository.findById(gameId); + repository.findById(gameId); + + assertThat(fakeGameDao.getFindByIdCount()).isZero(); + } + + private Janggi createTestJanggi() { + return Janggi.start( + BoardFactory.create( + FormationStrategyFactory.from(1), + FormationStrategyFactory.from(1)) + ); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/persistence/dao/FakeGameDao.java b/src/test/java/janggi/persistence/dao/FakeGameDao.java new file mode 100644 index 0000000000..1ce34e86b7 --- /dev/null +++ b/src/test/java/janggi/persistence/dao/FakeGameDao.java @@ -0,0 +1,60 @@ +package janggi.persistence.dao; + + +import janggi.domain.Camp; +import janggi.exception.DuplicateGameException; +import janggi.persistence.entity.GameEntity; +import janggi.persistence.entity.vo.Status; + +import java.sql.Connection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class FakeGameDao implements GameDao { + private final Map store = new HashMap<>(); + private int findByIdCount = 0; + + @Override + public void create(Connection conn, GameEntity gameEntity) { + if (store.values().stream().anyMatch(e -> e.name().equals(gameEntity.name()))) { + throw new DuplicateGameException("이미 존재하는 게임입니다.", null); + } + store.put(gameEntity.id(), gameEntity); + } + + @Override + public List findAllNames() { + return store.values().stream().map(GameEntity::name).toList(); + } + + @Override + public Optional findByName(String name) { + return store.values().stream() + .filter(e -> e.name().equals(name)) + .map(GameEntity::id) + .findFirst(); + } + + @Override + public void deleteById(Connection conn, String id) { + store.remove(id); + } + + @Override + public Optional findById(String id) { + findByIdCount++; + return Optional.ofNullable(store.get(id)); + } + + @Override + public void updateStatus(Connection conn, String gameId, Camp camp, Status status) { + GameEntity old = store.get(gameId); + store.put(gameId, new GameEntity(old.id(), old.name(), status, camp)); + } + + public int getFindByIdCount() { + return findByIdCount; + } +} \ No newline at end of file diff --git a/src/test/java/janggi/persistence/dao/FakePieceDao.java b/src/test/java/janggi/persistence/dao/FakePieceDao.java new file mode 100644 index 0000000000..45391af231 --- /dev/null +++ b/src/test/java/janggi/persistence/dao/FakePieceDao.java @@ -0,0 +1,37 @@ +package janggi.persistence.dao; + +import janggi.persistence.entity.PieceEntity; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FakePieceDao implements PieceDao{ + private final Map> store = new HashMap<>(); + + @Override + public void createAll(Connection conn, List entities) { + if (entities.isEmpty()) { + return; + } + String gameId = entities.getFirst().gameId(); + store.computeIfAbsent(gameId, k -> new ArrayList<>()).addAll(entities); + } + + @Override + public List findByGameId(String gameId) { + return store.getOrDefault(gameId, List.of()); + } + + @Override + public void deleteByGameId(Connection conn, String gameId) { + store.remove(gameId); + } + + @Override + public void updateAll(Connection conn, String gameId, List entities) { + store.put(gameId, new ArrayList<>(entities)); + } +} diff --git a/src/test/java/janggi/persistence/dao/JdbcGameDaoTest.java b/src/test/java/janggi/persistence/dao/JdbcGameDaoTest.java new file mode 100644 index 0000000000..96e1dcded9 --- /dev/null +++ b/src/test/java/janggi/persistence/dao/JdbcGameDaoTest.java @@ -0,0 +1,124 @@ +package janggi.persistence.dao; + +import janggi.config.ConnectionPool; +import janggi.config.PooledConnection; +import janggi.config.TestConnectionPool; +import janggi.config.TestDBInitializer; +import janggi.domain.Camp; +import janggi.exception.DuplicateGameException; +import janggi.persistence.entity.GameEntity; +import janggi.persistence.entity.vo.Status; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class JdbcGameDaoTest { + private static ConnectionPool pool; + private GameDao gameDao; + + @BeforeAll + static void setUpPool() { + pool = TestConnectionPool.create(); + TestDBInitializer.initSchema(pool); + } + + @BeforeEach + void setUp() { + gameDao = new JdbcGameDao(pool); + TestDBInitializer.clearAll(pool); + } + + @Test + void 게임을_저장하고_조회할_수_있다() { + GameEntity entity = new GameEntity("id-1", "테스트게임", Status.PLAYING, Camp.CHO); + + try (PooledConnection pooled = pool.getPooledConnection()) { + gameDao.create(pooled.getConnection(), entity); + } + + Optional result = gameDao.findById("id-1"); + + assertThat(result).isPresent(); + assertThat(result.get().name()).isEqualTo("테스트게임"); + } + + @Test + void 같은_이름으로_저장하면_예외가_발생한다() { + GameEntity entity1 = new GameEntity("id-1", "같은이름", Status.PLAYING, Camp.CHO); + GameEntity entity2 = new GameEntity("id-2", "같은이름", Status.PLAYING, Camp.CHO); + + try (PooledConnection pooled = pool.getPooledConnection()) { + gameDao.create(pooled.getConnection(), entity1); + + assertThatThrownBy(() -> gameDao.create(pooled.getConnection(), entity2)) + .isInstanceOf(DuplicateGameException.class); + } + } + + @Test + void 게임을_삭제할_수_있다() { + GameEntity entity = new GameEntity("id-1", "삭제게임", Status.PLAYING, Camp.CHO); + + try (PooledConnection pooled = pool.getPooledConnection()) { + gameDao.create(pooled.getConnection(), entity); + gameDao.deleteById(pooled.getConnection(), "id-1"); + } + + Optional result = gameDao.findById("id-1"); + assertThat(result).isEmpty(); + } + + @Test + void 모든_게임_이름을_조회할_수_있다() { + try (PooledConnection pooled = pool.getPooledConnection()) { + gameDao.create(pooled.getConnection(), + new GameEntity("id-1", "게임1", Status.PLAYING, Camp.CHO)); + gameDao.create(pooled.getConnection(), + new GameEntity("id-2", "게임2", Status.PLAYING, Camp.HAN)); + } + + List names = gameDao.findAllNames(); + + assertThat(names).containsExactlyInAnyOrder("게임1", "게임2"); + } + + @Test + void 이름으로_게임ID를_조회할_수_있다() { + try (PooledConnection pooled = pool.getPooledConnection()) { + gameDao.create(pooled.getConnection(), + new GameEntity("id-1", "찾을게임", Status.PLAYING, Camp.CHO)); + } + + Optional result = gameDao.findByName("찾을게임"); + + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo("id-1"); + } + + @Test + void 게임_상태를_업데이트할_수_있다() { + try (PooledConnection pooled = pool.getPooledConnection()) { + gameDao.create(pooled.getConnection(), + new GameEntity("id-1", "업데이트게임", Status.PLAYING, Camp.CHO)); + gameDao.updateStatus(pooled.getConnection(), "id-1", Camp.HAN, Status.HAN_WIN); + } + + Optional result = gameDao.findById("id-1"); + + assertThat(result).isPresent(); + assertThat(result.get().status()).isEqualTo(Status.HAN_WIN); + assertThat(result.get().camp()).isEqualTo(Camp.HAN); + } + + @AfterAll + static void tearDown() { + pool.shutdown(); + } +} \ No newline at end of file diff --git a/src/test/java/janggi/persistence/dao/JdbcPieceDaoTest.java b/src/test/java/janggi/persistence/dao/JdbcPieceDaoTest.java new file mode 100644 index 0000000000..736851d0a9 --- /dev/null +++ b/src/test/java/janggi/persistence/dao/JdbcPieceDaoTest.java @@ -0,0 +1,66 @@ +package janggi.persistence.dao; + +import janggi.config.ConnectionPool; +import janggi.config.PooledConnection; +import janggi.config.TestConnectionPool; +import janggi.config.TestDBInitializer; +import janggi.persistence.entity.PieceEntity; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JdbcPieceDaoTest { + private static ConnectionPool pool; + private PieceDao pieceDao; + + @BeforeAll + static void setUpPool() { + pool = TestConnectionPool.create(); + TestDBInitializer.initSchema(pool); + } + + @BeforeEach + void setUp() { + pieceDao = new JdbcPieceDao(pool); + TestDBInitializer.clearAll(pool); + } + + @Test + void 기물을_저장하고_조회할_수_있다() { + List pieces = List.of( + new PieceEntity(0L, "game-1", "차", "CHO", 0, 0), + new PieceEntity(0L, "game-1", "마", "CHO", 0, 1) + ); + + try (PooledConnection pooled = pool.getPooledConnection()) { + pieceDao.createAll(pooled.getConnection(), pieces); + } + + List result = pieceDao.findByGameId("game-1"); + assertThat(result).hasSize(2); + } + + @Test + void 게임ID로_기물을_삭제할_수_있다() { + List pieces = List.of( + new PieceEntity(0L, "game-1", "차", "CHO", 0, 0) + ); + + try (PooledConnection pooled = pool.getPooledConnection()) { + pieceDao.createAll(pooled.getConnection(), pieces); + pieceDao.deleteByGameId(pooled.getConnection(), "game-1"); + } + + assertThat(pieceDao.findByGameId("game-1")).isEmpty(); + } + + @AfterAll + static void tearDown() { + pool.shutdown(); + } +} diff --git a/src/test/java/janggi/persistence/entity/GameEntityTest.java b/src/test/java/janggi/persistence/entity/GameEntityTest.java new file mode 100644 index 0000000000..c91b40edb0 --- /dev/null +++ b/src/test/java/janggi/persistence/entity/GameEntityTest.java @@ -0,0 +1,43 @@ +package janggi.persistence.entity; + +import janggi.domain.Camp; +import janggi.persistence.entity.vo.Status; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class GameEntityTest { + + @ParameterizedTest + @CsvSource(value = { + "일", + "일이삼사오육칠팔구십일이삼사오육칠팔구십일이삼사오육칠팔구십일이삼사오육칠팔구십일이삼사오육칠팔구십" + }) + void 게임_이름이_1글자_이상_50글자_이하면_정상_생성된다(String name) { + GameEntity game = new GameEntity( + "id", + name, + Status.PLAYING, + Camp.CHO); + + assertThat(game).isNotNull(); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = { + "일이삼사오육칠팔구십일이삼사오육칠팔구십일이삼사오육칠팔구십일이삼사오육칠팔구십일이삼사오육칠팔구십십" + }) + void 게임_이름이_1글자_미만_50글자이하면_예외_처리한다(String name) { + assertThatThrownBy(() -> new GameEntity( + "id", + name, + Status.PLAYING, + Camp.CHO)); + } +} \ No newline at end of file diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 0000000000..46b5c7fa68 --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS game ( + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(50) UNIQUE, + status VARCHAR(20), + current_turn VARCHAR(10) +); + +CREATE TABLE IF NOT EXISTS piece ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + game_id VARCHAR(36), + piece_name VARCHAR(20), + camp VARCHAR(10), + row_index INT, + column_index INT +); \ No newline at end of file