-
Notifications
You must be signed in to change notification settings - Fork 166
[🚀 사이클1 - 미션 (보드 초기화 + 기물 이동)] 무빙 미션 제출합니다. #200
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: mvg01
Are you sure you want to change the base?
Changes from all commits
2d931ea
b841d4a
d2caeba
d1e853c
04f9dc6
96236d6
703c6cc
283a708
bccd555
1faa91f
455379e
7f4caa6
45894d7
ea4a672
4062415
fc807da
5168752
95df400
09263e1
43276c9
313d5e5
2daa947
c05930b
e9b78ba
60feacf
159f9a3
2bdd2aa
50e0b1f
ccf0c0a
280bbc3
7e3d518
16f5a0c
fd85740
3644d3c
25f5c80
34d3805
9542298
eb7ca57
ad3f207
095976c
26f7d5b
9e9ad83
3d8d512
c4ae3c9
21b4dfc
47918a8
8c8ec85
7ffe52e
e86d143
6296e87
d8788ea
cb7363e
e785c2f
83b42af
0fc7c04
7dc30dc
bfc634a
30cca54
bab1236
672895a
2e4c672
efb3680
ee3f7f9
d86f271
82f1e1e
2da8100
6358f66
f2e774e
c98114e
1e3d2ef
056b770
f4a5813
b7f334f
6ad1979
69f3958
0084104
2d2ade8
b4c4c9e
e347148
b1adc9f
93cf544
ca133d4
78603c9
54d5a23
ae02236
e2df58f
f1e051b
80b0ec3
f7c46ae
9b02da6
2109d0e
dc7e3aa
99a4901
b15d18e
0210333
d6f20fd
81a84a8
33b2605
ae38ff3
4f108d1
bab88e7
e1fe200
78c7c33
a96700a
022322f
f379b99
6dc67b5
0eae99a
b93a0f1
ed1a43f
299a565
641314f
5ac4a06
e3f163e
2cc764b
b7c49c8
0bb172e
0fbb82c
c452a58
4edb6e2
656cd3d
c996d7c
49413ca
af33f4e
6793253
57771fb
8ce46f6
601c500
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Gomding marked this conversation as resolved.
Show resolved
Hide resolved
Gomding marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,47 @@ | ||
| # java-janggi | ||
|
|
||
| 장기 미션 저장소 | ||
| # 구현 기능 | ||
|
|
||
| - 보드 초기화 | ||
| - 각 플레이어에게 장기판 차림 방법을 입력 받는다. | ||
| - 먼저 플레이하는 초나라부터 명령 번호를 입력하여 판차림 방법을 결정한다. | ||
| - 1, 2, 3, 4 이외의 숫자가 들어오는 경우 예외 처리한다. | ||
| - 선택할 수 있는 판차림은 다음과 같다. | ||
| 1. 왼상 차림 | ||
| 2. 오른상 차림 | ||
| 3. 안상 차림 | ||
| 4. 바깥상 차림 | ||
| - 입력한 장기판 차림을 기반으로 보드를 세팅한다. | ||
| - 보드 출력 | ||
| - 빈 공간은 온점 전각 문자를 활용한다. | ||
| - 팀 구분은 색상(빨강, 파랑)으로 구분한다. | ||
| - 기물은 각 기물에 대응하는 한나라의 한자로 표기한다. | ||
|
|
||
| - 기물 이동 구현 | ||
| - 움직일 기물의 좌표를 플레이어에게 입력받는다. | ||
| - 다음의 경우 다시 입력받는다. | ||
| - `row`, `column` 형식이 아닌 경우 | ||
| - 플레이어의 팀이 아닌 기물의 좌표를 입력한 경우 | ||
| - 입력한 기물이 이동 가능한 위치가 없는 경우 | ||
| - 입력받은 위치의 기물이 이동 가능한 위치를 출력한다. | ||
| - 이동 가능한 위치는 초록색으로 표현된다. | ||
| - 플레이어에게 이동 가능한 위치 중 하나를 입력받는다. | ||
| - 다음의 경우 다시 입력받는다. | ||
| - `row`, `column` 형식이 아닌 경우 | ||
| - 이동 가능한 위치가 아닌 경우 | ||
| - 입력받은 위치로 기물을 이동시킨다. | ||
| - 각 기물의 이동 규칙은 다음과 같다. | ||
| - 차 (車): 가로/세로 직선 이동가능, 거리 제한 없음, 중간에 기물이 있으면 못 지나감 | ||
| - 마 (馬): 1칸 직선 + 1칸 대각선, 가는 길이 막히면 이동 불가 | ||
| - 상 (象): 1칸 직선 + 2칸 대각선, 가는 길이 막히면 이동 불가 | ||
| - 사 (士): 모든 방향으로 1칸 이동 가능 | ||
| - 장 (將): 모든 방향으로 1칸 이동 가능 | ||
| - 포 (砲): 반드시 하나를 넘어 전후좌우로 이동이 가능하고 포는 서로 넘거나 잡을 수 없다. | ||
| - 졸 (卒): 앞, 왼쪽, 오른쪽 1칸씩 이동 가능 | ||
|
|
||
| - 게임 루프 로직 | ||
| - 처음엔 한나라(Red Team)로 시작한다. | ||
| - 각 플레이어가 움직일 기물의 위치를 입력한다. | ||
| - 기물 이동 구현의 규칙과 같다. | ||
| - 한나라 -> 초나라 -> 한나라... 반복된다. | ||
| - 종료 조건은 어느 한 팀의 장 기물이 잡히면 게임은 종료된다. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package janggi; | ||
|
|
||
| import janggi.controller.JanggiController; | ||
|
|
||
| public class Application { | ||
|
|
||
| public static void main(String[] args) { | ||
| final JanggiController janggiController = new JanggiController(); | ||
| janggiController.run(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| package janggi.controller; | ||
|
|
||
| import janggi.domain.Position; | ||
| import janggi.domain.board.Board; | ||
| import janggi.domain.board.BoardGenerator; | ||
| import janggi.domain.board.setup.ElephantFormation; | ||
| import janggi.domain.board.setup.SetupStrategy; | ||
| import janggi.domain.team.BlueTeam; | ||
| import janggi.domain.team.RedTeam; | ||
| import janggi.domain.team.Team; | ||
| import janggi.domain.team.TeamType; | ||
| import janggi.domain.team.TurnManager; | ||
| import janggi.dto.BoardDto; | ||
| import janggi.utils.RetryExecutor; | ||
| import janggi.view.InputView; | ||
| import janggi.view.OutputView; | ||
| import java.util.List; | ||
|
|
||
| public class JanggiController { | ||
|
|
||
| public JanggiController() { | ||
| } | ||
|
|
||
| public void run() { | ||
| Board board = BoardGenerator.generate(setupRedTeam(), setupBlueTeam()); | ||
| TurnManager turnManager = new TurnManager(); | ||
| while (board.hasGeneral(turnManager.currentTeamType())) { | ||
| playTurn(board, turnManager); | ||
| } | ||
| turnManager.changeTurn(); | ||
| OutputView.printGameOverMessage(turnManager.currentTeamTypeToString()); | ||
| } | ||
|
|
||
| private Team setupRedTeam() { | ||
| OutputView.printSetupGuide(TeamType.RED); | ||
| final SetupStrategy setupStrategyCommand = RetryExecutor.retry(this::readSetupCommand); | ||
| final ElephantFormation elephantFormation = setupStrategyCommand.toPolicy(); | ||
| return new RedTeam(elephantFormation); | ||
| } | ||
|
|
||
| private Team setupBlueTeam() { | ||
| OutputView.printSetupGuide(TeamType.BLUE); | ||
| final SetupStrategy setupStrategyCommand = RetryExecutor.retry(this::readSetupCommand); | ||
| final ElephantFormation elephantFormation = setupStrategyCommand.toPolicy(); | ||
| return new BlueTeam(elephantFormation); | ||
| } | ||
|
|
||
| private SetupStrategy readSetupCommand() { | ||
| int inputCommand = InputView.readSetupCommand(); | ||
| return SetupStrategy.from(inputCommand); | ||
| } | ||
|
|
||
| private void playTurn(Board board, TurnManager turnManager) { | ||
| OutputView.printBoard(BoardDto.from(board), turnManager.currentTeamTypeToString()); | ||
| Position from = findFromPosition(board, turnManager); | ||
| List<Position> movable = board.calculateMovablePositions(from); | ||
| OutputView.printBoardWithMovable(BoardDto.from(board), movable); | ||
| Position to = RetryExecutor.retry(() -> inputToPosition(movable)); | ||
| board.movePiece(from, to); | ||
| turnManager.changeTurn(); | ||
| } | ||
|
|
||
| private Position findFromPosition(Board board, TurnManager turnManager) { | ||
| while (true) { | ||
| Position from = RetryExecutor.retry(() -> inputFromPosition(board, turnManager)); | ||
| List<Position> movable = board.calculateMovablePositions(from); | ||
| if (!movable.isEmpty()) { | ||
| return from; | ||
| } | ||
| OutputView.printErrorMessage("이동 가능한 위치가 없습니다. 다른 기물을 선택하세요."); | ||
| } | ||
| } | ||
|
|
||
| private Position inputFromPosition(Board board, TurnManager turnManager) { | ||
| while (true) { | ||
| try { | ||
| OutputView.printInputFromPosition(); | ||
| Position from = InputView.readPosition(); | ||
| if (!board.isSameTeamType(from, turnManager.currentTeamType())) { | ||
| throw new IllegalArgumentException("자신의 기물을 선택하세요."); | ||
| } | ||
| return from; | ||
| } catch (IllegalArgumentException e) { | ||
| OutputView.printErrorMessage(e.getMessage()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private Position inputToPosition(List<Position> movable) { | ||
| while (true) { | ||
| try { | ||
| OutputView.printInputToPosition(); | ||
| Position to = InputView.readPosition(); | ||
| if (!movable.contains(to)) { | ||
| throw new IllegalArgumentException("이동 불가능한 위치입니다."); | ||
| } | ||
| return to; | ||
| } catch (IllegalArgumentException e) { | ||
| OutputView.printErrorMessage(e.getMessage()); | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| package janggi.domain; | ||
|
|
||
| import janggi.domain.movement.Direction; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
||
| public final class Position { | ||
|
|
||
| public static final int MAXIMUM_ROW = 10; | ||
| public static final int MAXIMUM_COLUMN = 9; | ||
| public static final int MINIMUM_ROW = 1; | ||
| public static final int MINIMUM_COLUMN = 1; | ||
| private static final int ROW_FLIP_VALUE = 11; | ||
| private static final Map<Integer, Map<Integer, Position>> CACHE; | ||
|
|
||
| static { | ||
| CACHE = new LinkedHashMap<>(); | ||
| for (int row = MINIMUM_ROW; row <= MAXIMUM_ROW; row++) { | ||
| CACHE.put(row, new LinkedHashMap<>()); | ||
| } | ||
| } | ||
|
|
||
| private final int row; | ||
| private final int column; | ||
|
|
||
| private Position(final int row, final int column) { | ||
| this.row = row; | ||
| this.column = column; | ||
| } | ||
|
|
||
| public static Position valueOf(final int row, final int column) { | ||
| validateRowRange(row); | ||
| validateColumnRange(column); | ||
| final Map<Integer, Position> secondaryMap = CACHE.get(row); | ||
| if (!secondaryMap.containsKey(column)) { | ||
| secondaryMap.put(column, new Position(row, column)); | ||
| } | ||
| return secondaryMap.get(column); | ||
| } | ||
|
|
||
| private static void validateRowRange(final int row) { | ||
| if (row < MINIMUM_ROW || row > MAXIMUM_ROW) { | ||
| throw new IllegalArgumentException(String.format("행 입력은 %d~%d을 입력해야 합니다.", MINIMUM_ROW, MAXIMUM_ROW)); | ||
| } | ||
| } | ||
|
|
||
| private static void validateColumnRange(final int column) { | ||
| if (column < MINIMUM_COLUMN || column > MAXIMUM_COLUMN) { | ||
| throw new IllegalArgumentException(String.format("열 입력은 %d~%d을 입력해야 합니다.", MINIMUM_COLUMN, MAXIMUM_COLUMN)); | ||
| } | ||
| } | ||
|
|
||
| public Position flipAroundMiddleRow() { | ||
| return Position.valueOf(ROW_FLIP_VALUE - row, column); | ||
| } | ||
|
|
||
| public boolean checkNextBound(final int distance, final Direction direction) { | ||
| final int nextRow = row + direction.getRowDirection() * distance; | ||
| final int nextColumn = column + direction.getColumnDirection() * distance; | ||
|
|
||
| return nextRow >= MINIMUM_ROW && nextRow <= MAXIMUM_ROW && nextColumn >= MINIMUM_COLUMN | ||
| && nextColumn <= MAXIMUM_COLUMN; | ||
| } | ||
|
|
||
| public Position calculateNext(final int distance, final Direction direction) { | ||
| final int nextRow = row + direction.getRowDirection() * distance; | ||
| final int nextColumn = column + direction.getColumnDirection() * distance; | ||
|
|
||
| return Position.valueOf(Math.clamp(nextRow, MINIMUM_ROW, MAXIMUM_ROW), | ||
| Math.clamp(nextColumn, MINIMUM_COLUMN, MAXIMUM_COLUMN)); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(final Object object) { | ||
| if (object == null || getClass() != object.getClass()) { | ||
| return false; | ||
| } | ||
| final Position position = (Position) object; | ||
| return row == position.row && column == position.column; | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hash(row, column); | ||
| } | ||
| } |
|
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.
3번 답변에 이어서 코멘트하겠습니다. 현재의 방식이 결국 캡슐화가 지켜지지 않는 방향이라고 답변을 주셔서 혼란이 왔습니다.. 캡슐화를 지키면서 기물에게 보드의 상태를 어떻게 제공할지 잘 모르겠습니다. 그렇다면 기물이 꼭 보드의 상태를 알아야만 움직일 수 있는가? 라는 질문을 던져보아도 계속 기물이 보드의 상태를 알아야만 움직임을 담당할 수 있을 것 같다고 생각이 듭니다. 우선 이번 리뷰에선 여기까지 정리하며 고민을 해보도록 하겠습니다! 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.
Piece 가 Board 를 상태로 가지는 설계를 생각하셨군요 🤔
++) 캡슐화가 붕괴된다 라고 하셨지만
+++) SRP 가 위배된다고 생각하신 이유도 궁금해요
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.
이 코멘트를 보고 어떤 코드가 문제의 시작일까 다시 생각을 해봤습니다. @Override
public Piece getPieceInPosition(final Position position) {
return findPieceByPosition(position);
}이 코드에서 Piece 객체를 주면 이걸 받은 movement나 Piece같은 다른 객체들이 다시 Piece의 상태를 getTeamType()으로 다시 확인하는 경우도 있었습니다. 이런 부분은 TeamType을 리턴해주거나 하는 방식으로 수정할 수 있을 것 같습니다. 솔직하게 얘기한다면 지금 가장 헷갈리는건 어디까지가 캡슐화고, 괜찮은 의존 관계인지를 모르겠습니다. 캡슐화는 내부 구현을 숨기고 필요한 행동만 외부에 노출하는 것입니다.이 관점에서 위의
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. 캡슐화란 무엇일까요?
Board 를 의존하게되면 SRP 관점에서 위배되는 부분은 어떤 점인가요?
이제 여기서 뭘 더 건드려야될지 모르겠습니다.. 😢
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. Movement쪽에선 Piece의 의존은 제거하였습니다. 하지만 아직 Controller에는 TDA가 지켜지지 않고 있습니다. 여전히 여기는 어떻게 해야할지 감이 잘 안잡힙니다! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package janggi.domain.board; | ||
|
|
||
| import janggi.domain.Position; | ||
| import janggi.domain.piece.Piece; | ||
| import janggi.domain.team.TeamType; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| public class Board implements BoardMediator { | ||
|
|
||
| private final Map<Position, Piece> positionPieceMap; | ||
|
|
||
| public Board(final Map<Position, Piece> positionPieceMap) { | ||
| this.positionPieceMap = new LinkedHashMap<>(positionPieceMap); | ||
| } | ||
|
|
||
| public void movePiece(final Position from, final Position to) { | ||
| positionPieceMap.put(to, positionPieceMap.remove(from)); | ||
| } | ||
|
|
||
| public List<Position> calculateMovablePositions(Position from) { | ||
| return findPieceByPosition(from).calculateMovablePositions(from, this); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean hasPieceAt(final Position position) { | ||
| return hasPieceIn(position); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean hasGeneral(TeamType teamType) { | ||
| return positionPieceMap.values().stream() | ||
| .anyMatch(piece -> piece.isGeneral() && piece.isSameTeamType(teamType)); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isCannon(Position position) { | ||
| return findPieceByPosition(position).isCannon(); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isSameTeamType(Position position, TeamType teamType) { | ||
| return findPieceByPosition(position).isSameTeamType(teamType); | ||
| } | ||
|
|
||
| public Map<Position, Piece> getPositionPieceMapForDTO() { | ||
| return Map.copyOf(positionPieceMap); | ||
| } | ||
|
|
||
| private boolean hasPieceIn(final Position position) { | ||
| return positionPieceMap.containsKey(position); | ||
| } | ||
|
|
||
| private Piece findPieceByPosition(final Position position) { | ||
| if (hasPieceIn(position)) { | ||
| return positionPieceMap.get(position); | ||
| } | ||
| throw new IllegalArgumentException("요청된 위치에는 기물이 존재하지 않습니다."); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package janggi.domain.board; | ||
|
|
||
| import janggi.domain.Position; | ||
| import janggi.domain.piece.Piece; | ||
| import janggi.domain.team.Team; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
|
|
||
| public final class BoardGenerator { | ||
|
|
||
| private BoardGenerator() { | ||
| } | ||
|
|
||
| public static Board generate(final Team redTeam, final Team blueTeam) { | ||
| final Map<Position, Piece> positionPieceMap = new LinkedHashMap<>(); | ||
| positionPieceMap.putAll(redTeam.generatePieces()); | ||
| positionPieceMap.putAll(blueTeam.generatePieces()); | ||
| return new Board(positionPieceMap); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package janggi.domain.board; | ||
|
|
||
| import janggi.domain.Position; | ||
| import janggi.domain.team.TeamType; | ||
|
|
||
| public interface BoardMediator { | ||
| boolean hasPieceAt(Position position); | ||
|
|
||
| boolean hasGeneral(TeamType teamType); | ||
|
|
||
| boolean isCannon(Position position); | ||
|
|
||
| boolean isSameTeamType(Position position, TeamType teamType); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package janggi.domain.board.setup; | ||
|
|
||
| import janggi.domain.Position; | ||
| import janggi.domain.piece.PieceType; | ||
| import java.util.Collections; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
|
|
||
| public abstract class ElephantFormation { | ||
| static final Map<Position, PieceType> COMMON_BOARD_MAP; | ||
|
|
||
| static { | ||
| Map<Position, PieceType> boardMap = new LinkedHashMap<>(); | ||
| boardMap.put(Position.valueOf(1, 1), PieceType.CHARIOT); | ||
| boardMap.put(Position.valueOf(1, 4), PieceType.GUARD); | ||
| boardMap.put(Position.valueOf(1, 6), PieceType.GUARD); | ||
| boardMap.put(Position.valueOf(1, 9), PieceType.CHARIOT); | ||
| boardMap.put(Position.valueOf(2, 5), PieceType.GENERAL); | ||
| boardMap.put(Position.valueOf(3, 2), PieceType.CANNON); | ||
| boardMap.put(Position.valueOf(3, 8), PieceType.CANNON); | ||
| boardMap.put(Position.valueOf(4, 1), PieceType.SOLDIER); | ||
| boardMap.put(Position.valueOf(4, 3), PieceType.SOLDIER); | ||
| boardMap.put(Position.valueOf(4, 5), PieceType.SOLDIER); | ||
| boardMap.put(Position.valueOf(4, 7), PieceType.SOLDIER); | ||
| boardMap.put(Position.valueOf(4, 9), PieceType.SOLDIER); | ||
| COMMON_BOARD_MAP = Collections.unmodifiableMap(boardMap); | ||
| } | ||
|
|
||
| public abstract Map<Position, PieceType> offerBoardMap(); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.