-
Notifications
You must be signed in to change notification settings - Fork 166
[🚀 사이클1- 미션 (보드 초기화 + 기물 이동)] 루덴스 미션 제출합니다. #234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: poketopa
Are you sure you want to change the base?
Changes from all commits
4529e0c
b54e335
24e0205
d9e796e
2249919
102c9a9
f917dfd
a167ab1
f36482f
4bc50fd
210b039
74d0a0e
d9f8370
ff15330
a3e3d93
e4f4709
51e3613
3f01333
2c4084b
69e688f
cb7c525
21321cd
e5ffff4
87c8986
e55c900
004fe1d
1fe72f6
af00787
d5732e3
9c3417b
93447f8
a78b294
991559d
45f0dc6
146434b
4800bea
68ccf14
b8b5f9d
3310d18
d70139a
c0577ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,75 @@ | ||
| # java-janggi | ||
|
|
||
| 장기 미션 저장소 | ||
|
|
||
| ### 기능 요구사항 체크리스트 | ||
|
|
||
| #### step 1 | ||
| - [x] 10x9 장기판 좌표를 `Point(y, x)`로 표현하고, 범위를 벗어난 좌표 생성 시 예외를 발생시킨다. | ||
| - [x] 기물 타입(`PieceType`)과 진영(`Team`)을 분리해 도메인 모델링한다. | ||
| - [x] 빈 칸을 `NonePiece`로 포장해 `Intersection.empty()`로 관리한다. | ||
| - [x] 장기판 초기화 시 전체 좌표를 빈 칸으로 채운 뒤, 생성기(`IntersectionGenerator`)가 전달한 기물을 배치한다. | ||
| - [x] 초/한 기본 배치(졸, 포, 장군, 사, 차)를 자동 생성한다. | ||
| - [x] 상/마 차림(`Formation`) 4가지를 지원하고, 진영별로 대칭 배치한다. | ||
| - [x] `Formation.valueOf(int)`로 입력 숫자를 차림 enum으로 변환한다. | ||
| - [x] 보드에서 좌표로 교차점(`Intersection`)을 조회할 수 있다. | ||
| - [x] 이동 규칙을 `MoveRule` 전략으로 분리하고, 기물 타입별 규칙 클래스를 연결한다. | ||
| - [x] 이동 경로를 `Direction`/`Directions`로 추상화해 “도착 가능 여부 + 경유 좌표”를 계산한다. | ||
| - [x] 차(`Chariot`)는 상하좌우 직선 이동만 가능하며, 경로 중간 장애물이 있으면 이동할 수 없다. | ||
| - [x] 마(`Horse`)는 2단계 경로(멱) 중 중간 칸이 막히면 이동할 수 없다. | ||
| - [x] 상(`Elephant`)은 3단계 경로 중 중간 칸이 막히면 이동할 수 없다. | ||
| - [x] 포(`Cannon`)는 정확히 1개의 장애물을 넘어야 하며, 포를 넘거나 포를 공격할 수 없다. | ||
| - [x] 장군(`General`)과 사(`Guard`)는 1칸 상하좌우 이동을 지원한다. | ||
| - [x] 졸(`Soldier`)은 좌/우 + 전진 이동을 지원하며, 진영에 따라 전진 방향이 다르다. | ||
| - [x] 같은 팀 기물이 있는 칸으로는 이동할 수 없다. | ||
| - [x] `JanggiBoard.tryToMove()` 수행 시 도착지는 출발 기물로 갱신되고, 출발지는 빈 칸이 된다. | ||
|
|
||
| ### 주요 로직 요약 | ||
|
|
||
| #### step 1 | ||
| - **보드 생성** | ||
| - `JanggiBoard`가 10x9 전체 좌표를 `Intersection.empty()`로 초기화한다. | ||
| - **초기 기물 배치** | ||
| - `JanggiGenerator`가 진영(`CHO`, `HAN`)별 기본 행/열 규칙으로 기물을 만든다. | ||
| - 상/마는 `Formation`에 정의된 열 인덱스 조합으로 배치한다. | ||
| - **좌표/교차점 모델링** | ||
| - `Point`는 생성 시 범위 검증을 수행하고, `next(Vector)`로 이동 좌표를 계산한다. | ||
| - `Intersection`은 기물 도착(`arrive`)과 이탈(`leave`) 책임을 가진다. | ||
| - **이동 규칙 선택** | ||
| - 보드는 출발 지점의 기물 타입을 기준으로 `MoveRule` 구현체를 선택한다. | ||
| - **경로 계산** | ||
| - 규칙별 `Directions.findPoints(from, to)`로 목적지까지의 경유 좌표 목록을 만든다. | ||
| - **규칙 검증** | ||
| - 공통적으로 같은 팀 도착지 여부를 검증한다. | ||
| - 차/마/상은 경로 장애물 조건을 검증한다. | ||
| - 포는 “장애물 1개 필수”, “포 넘기/포 공격 금지”를 추가 검증한다. | ||
| - **실제 이동 반영** | ||
| - 검증 통과 시 도착지 `arrive(from)`, 출발지 `leave()` 순서로 상태를 갱신한다. | ||
|
|
||
| ### 기물별 이동 규칙 정리 | ||
|
|
||
| - **차(`Chariot`)** | ||
| - 상/하/좌/우 직선 다칸 이동 | ||
| - 도착지 아군 금지 | ||
| - 경로 중간 장애물 금지 | ||
| - **포(`Cannon`)** | ||
| - 상/하/좌/우 직선 이동 | ||
| - 중간 장애물 정확히 1개 필요 | ||
| - 장애물/도착지가 포인 경우 금지 | ||
| - 도착지 아군 금지 | ||
| - **마(`Horse`)** | ||
| - 1칸 직선 + 1칸 대각(총 2스텝) | ||
| - 중간 경유 칸 장애물 금지 | ||
| - 도착지 아군 금지 | ||
| - **상(`Elephant`)** | ||
| - 1칸 직선 + 2칸 대각(총 3스텝) | ||
| - 경유 칸 장애물 금지 | ||
| - 도착지 아군 금지 | ||
| - **장군/사(`General`/`Guard`)** | ||
| - 1칸 상하좌우 이동 | ||
| - 도착지 아군 금지 | ||
| - (궁성 내부/대각 특수 규칙 미반영) | ||
| - **졸(`Soldier`)** | ||
| - 좌/우 + 전진 이동 | ||
| - CHO는 위쪽(UP), HAN은 아래쪽(DOWN) 전진 | ||
| - 도착지 아군 금지 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import controller.JanggiController; | ||
| import view.InputView; | ||
| import view.OutputView; | ||
|
|
||
| public class Application { | ||
|
|
||
| public static void main(String[] args) { | ||
| InputView inputView = new InputView(); | ||
| OutputView outputView = new OutputView(); | ||
|
|
||
| JanggiController janggiController = new JanggiController(inputView, outputView); | ||
| janggiController.run(); | ||
|
|
||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package controller; | ||
|
|
||
| import domain.board.Formation; | ||
| import domain.board.JanggiBoard; | ||
| import domain.board.JanggiGenerator; | ||
| import domain.game.Game; | ||
| import domain.team.Team; | ||
| import dto.MoveDTO; | ||
| import view.InputView; | ||
| import view.OutputView; | ||
|
|
||
| public class JanggiController { | ||
|
|
||
| private final InputView inputView; | ||
| private final OutputView outputView; | ||
|
|
||
| public JanggiController(InputView inputView, OutputView outputView) { | ||
| this.inputView = inputView; | ||
| this.outputView = outputView; | ||
| } | ||
|
|
||
| public void run() { | ||
| Formation hanFormation = Formation.valueOf(inputView.inputHanWingSetup()); | ||
| Formation choFormation = Formation.valueOf(inputView.inputChoWingSetup()); | ||
| JanggiGenerator janggiGenerator = new JanggiGenerator(hanFormation, choFormation); | ||
|
|
||
| Game game = new Game(new JanggiBoard(janggiGenerator)); | ||
poketopa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| while (true) { | ||
| try { | ||
| outputView.printCurrentBoardStatus(game.boardStatus()); | ||
| final Team turn = game.currentTurn(); | ||
| outputView.printCurrentTurn(turn); | ||
| MoveDTO move = new MoveDTO(inputView.inputMovePiecePoint(), inputView.inputDestinationPoint()); | ||
| game.processTurn(move); | ||
| } catch (IllegalArgumentException e) { | ||
| outputView.printErrorMessage(e.getMessage()); | ||
| } | ||
| } | ||
|
Comment on lines
+29
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. game 승리 조건이 생기면 바뀔 수도 있긴 하겠지만, 얘기해보면 좋을 내용이라 남겨봅니다 :
그래서 웬만하면 명시적인 종료 조건을 while 문에 넣는 것을 권장해요.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 승리로직을 간단하게 구현하여 왕이 모두 살아있지 않은 상황을 턴마다 확인하도록 했습니다. public boolean isGameRunning() {
int generalCount = 0;
for (Intersection intersection : intersections.values()) {
if (intersection.isSamePiece(PieceType.GENERAL)) {
generalCount++;
}
}
if (generalCount == 2) {
return true;
}
return false;
}다만, 승리를 확인한 뒤에도 다음 턴으로 넘어가는 로직은 수행되기에 승자를 표시하기 위해서는 역보정을 해야했고, public void processTurn(MoveDto move) {
Point from = move.getFrom();
Point to = move.getTo();
checkCurrentTurnTeam(from);
janggiBoard.tryToMove(from, to);
isGameRunning = janggiBoard.isGameRunning();
turn = turn.nextTurn();
}
final Team winnerTeam = game.currentTurn().nextTurn();
outputView.printWinnerTeam(winnerTeam); |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package domain.board; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.List; | ||
|
|
||
| public enum Formation { | ||
|
|
||
| HORSE_ELEPHANT_HORSE_ELEPHANT(1, List.of(2, 7), List.of(1, 6)), | ||
| HORSE_ELEPHANT_ELEPHANT_HORSE(2, List.of(2, 6), List.of(1, 7)), | ||
| ELEPHANT_HORSE_ELEPHANT_HORSE(3, List.of(1, 6), List.of(2, 7)), | ||
| ELEPHANT_HORSE_HORSE_ELEPHANT(4, List.of(1, 7), List.of(2, 6)), | ||
| ; | ||
|
|
||
| private final int formatNumber; | ||
| private final List<Integer> elephantFormations; | ||
| private final List<Integer> horseFormations; | ||
|
|
||
| Formation(int formatNumber, List<Integer> elephantFormations, List<Integer> horseFormations) { | ||
| this.formatNumber = formatNumber; | ||
| this.elephantFormations = elephantFormations; | ||
| this.horseFormations = horseFormations; | ||
| } | ||
|
|
||
| public static Formation valueOf(int formatNumber) { | ||
| return Arrays.stream(Formation.values()) | ||
| .filter(formation -> formation.formatNumber == formatNumber) | ||
| .findFirst() | ||
| .orElseThrow(IllegalArgumentException::new); | ||
| } | ||
|
|
||
| public List<Integer> elephantFormations() { | ||
| return elephantFormations; | ||
| } | ||
|
|
||
| public List<Integer> horseFormations() { | ||
| return horseFormations; | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package domain.board; | ||
|
|
||
| import domain.intersection.Intersection; | ||
| import java.util.List; | ||
|
|
||
| public interface IntersectionGenerator { | ||
|
|
||
| List<Intersection> makeIntersection(); | ||
|
|
||
| } |
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 장기 미션을 수행하면서 불변 객체의 사용 이유들을 찾아보며 공부했고, 제가 생각하게 된 불변 객체 사용의 이유는 예시로, 어떤 기물의 동작이나 생성이 잘못되었을 때 게임이 진행되는 과정에서 그 영향이 확산되어 어떤 지점에서 잘못된 동작이 발생했는지 발견하기 힘들 수 있습니다. 하지만, 장기 관련 객체가 모두 불변이라면 특정 기물이 과연 그렇다면 JVM의 GC는 강력하기에 메모리 점유는 문제가 되지 않고, 장기 미션 수준에서는 그럼에도 불변 객체의 사용을 선택으로 남겨놓는 것은 현업에서는 모든 값을 불변으로 사용한다는 것 자체가 매우 힘들기 때문인 것인지, 그 수준에서는 메모리 낭비에 대한 압박이 있는 것인지, 혹은 그저 취향 차이인 것인지에 대한 궁금증이 있습니다. 이에 대한 피케이의 의견이 궁금합니다!
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 참고로 이는 구현이 거의 끝나갈 때 쯤, 장기판 자체를 불변으로 만들 수 있다는 사실을 알게 되면서 생긴 궁금증이었습니다. 제 코드에서도 장기판을 불변으로 만든다고 하면 머리가 아픈데, 실제 프로덕션 수준에서 그런 설계를 도입하는 것 자체가 상당한 오버헤드인가? 하는 생각도 듭니다..! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q. 불변은 취향인가 오버헤드인가 필수인가 제가 생각했을 때 장기게임에서 불변이 선택사항인 이유는 크게 두 가지입니다 :
상태변화가 자주 일어나는 것들을 어떻게든 불변으로 만들면 즉 불변은 '모든 것을 불변으로 만들겠다'는 생각으로 만든다기 보단, 실제 개발을 할 때도, 그리고 더 나아가 Java 와 Spring 진영 로직들도 다 비슷한 생각으로 만들어집니다.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 피케이의 생각 들려주셔서 감사합니다! 처음엔 불변 객체를 사용하는 이유가 불변으로 만들어 놓으면 수정이 안되니까 실수를 사전에 방지한다? 그렇다면 new로 만들어질 때 실수하는 것도 똑같이 위험하지 않은가? 하는 질문이 계속 생겼거든요. 지금은 그럼에도 불변 객체를 만드는 것이 좋은 이유를 구체적으로 알아가고 있지만,
그럼에도 조금의 모호함이 느껴지는 것 같습니다. 피케이의 코멘트에서도 조금의 의문이 생기는데요,
여기서 혹은 정말 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| package domain.board; | ||
|
|
||
| import domain.intersection.Intersection; | ||
| import domain.piece.move.CannonMoveRule; | ||
| import domain.piece.move.ChariotMoveRule; | ||
| import domain.piece.move.ElephantMoveRule; | ||
| import domain.piece.move.GeneralMoveRule; | ||
| import domain.piece.move.GuardMoveRule; | ||
| import domain.piece.move.HorseMoveRule; | ||
| import domain.piece.move.MoveRule; | ||
| import domain.piece.move.SoliderMoveRule; | ||
| import domain.point.Point; | ||
| import dto.BoardStatusDTO; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.stream.Collectors; | ||
| import java.util.stream.IntStream; | ||
| import java.util.stream.Stream; | ||
|
|
||
| public class JanggiBoard { | ||
|
|
||
| private static final int MAX_ROW = 10; | ||
| private static final int MAX_FILE = 9; | ||
|
|
||
| private final Map<Point, Intersection> intersections; | ||
| private final List<MoveRule> moveRules; | ||
|
|
||
| public JanggiBoard(IntersectionGenerator intersectionGenerator) { | ||
| this.intersections = fillEmptyIntersections(); | ||
| this.moveRules = setMoveRules(); | ||
| for (Intersection intersection : intersectionGenerator.makeIntersection()) { | ||
| intersections.put(intersection.getPoint(), intersection); | ||
| } | ||
| } | ||
|
|
||
| private static Stream<Point> getAllPoints() { | ||
| return range(MAX_ROW).boxed() | ||
| .flatMap(row -> range(MAX_FILE).mapToObj(f -> new Point(row, f))); | ||
| } | ||
|
|
||
| private static IntStream range(int maxRange) { | ||
| return IntStream.range(0, maxRange); | ||
| } | ||
poketopa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| public void tryToMove(Point start, Point end) { | ||
| Intersection from = findIntersection(start); | ||
| Intersection to = findIntersection(end); | ||
|
|
||
| validateMoveRule(from, to); | ||
| move(to, from); | ||
| } | ||
|
|
||
| private void move(Intersection to, Intersection from) { | ||
| to.arrive(from); | ||
| from.leave(); | ||
| } | ||
|
|
||
| private void validateMoveRule(Intersection from, Intersection to) { | ||
| MoveRule moveRule = findMoveRule(from); | ||
| List<Point> possiblePoints = moveRule.findPossiblePoints(from, to); | ||
| List<Intersection> path = findPath(possiblePoints); | ||
| moveRule.checkMoveRule(from, path); | ||
| } | ||
|
|
||
| public MoveRule findMoveRule(Intersection from) { | ||
| return moveRules.stream() | ||
| .filter(moveRule -> moveRule.support(from)) | ||
| .findFirst() | ||
| .orElseThrow(() -> new IllegalArgumentException("선택한 좌표에 이동 가능한 기물이 없습니다.")); | ||
| } | ||
|
|
||
| private List<Intersection> findPath(List<Point> possiblePoints) { | ||
| return possiblePoints.stream() | ||
| .map(this::findIntersection) | ||
| .toList(); | ||
| } | ||
|
|
||
| public BoardStatusDTO boardStatus() { | ||
| return new BoardStatusDTO(intersections); | ||
| } | ||
|
|
||
| public Intersection findIntersection(Point point) { | ||
| return intersections.get(point); | ||
| } | ||
|
|
||
| private List<MoveRule> setMoveRules() { | ||
| return List.of( | ||
| new ChariotMoveRule(), | ||
| new GeneralMoveRule(), | ||
| new GuardMoveRule(), | ||
| new ElephantMoveRule(), | ||
| new SoliderMoveRule(), | ||
| new CannonMoveRule(), | ||
| new HorseMoveRule() | ||
| ); | ||
| } | ||
|
|
||
| private Map<Point, Intersection> fillEmptyIntersections() { | ||
| return getAllPoints() | ||
| .collect(Collectors.toMap(point -> point, Intersection::empty)); | ||
| } | ||
|
Comment on lines
+98
to
+101
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 일부 클래스의 메서드 순서가 조금 뒤섞여 있어요.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 컨벤션을 확인하고 필요한 부분에 반영했습니다! 클린코드에서 |
||
|
|
||
|
|
||
|
Comment on lines
+102
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 불필요한 빈 줄이 있어요. 사소하지만 이 역시 코드를 깔끔하지 않게 보이게 하니 정리하고 신경 써보면 좋을 듯 해요.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수정했습니다! |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
애플리케이션을 직접 실행해보니 범위 밖 좌표를 입력했을 때 화면에 "null"이 출력됩니다.
사용자한테 "null"이 보이면 뭐가 잘못된 건지 알 수가 없으니, 예외 메시지를 넣어주면 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
잘못된 좌표가 입력되었을 때 에러 메시지가 출력되도록 수정했습니다.