Skip to content
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

[3단계 - 체스] 테오(최우성) 미션 제출합니다. #536

Merged
merged 59 commits into from
Mar 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
8a20fd1
refactor: 추상메소드가 아닌 람다를 사용해 방향을 찾도록 변경
woosung1223 Mar 20, 2023
c0f2674
refactor: 불필요한 생성자에 대한 테스트는 제거
woosung1223 Mar 20, 2023
844e12b
test: 예외 케이스뿐 아니라, 정상 흐름에 대한 테스트도 생성
woosung1223 Mar 20, 2023
96d2a03
docs: 구현에 맞게 기능 목록 최신화
woosung1223 Mar 20, 2023
a675aba
docs: 3단계 기능 목록 추가
woosung1223 Mar 20, 2023
397db84
refactor: 메소드 네이밍 수정
woosung1223 Mar 20, 2023
ace2935
fix: 보드 이미지에서 캐싱 된 객체가 가변이기에 새로운 객체로복사해서 반환하도록 수정
woosung1223 Mar 20, 2023
56c6248
refactor: 불필요한 Boolean Wrapping 제거
woosung1223 Mar 20, 2023
c379d07
feat: 올바른 턴이 아니라면 기물의 움직임을 제한할 수 있다
woosung1223 Mar 20, 2023
47a7e0b
refactor: Turn에서 getter 삭제, 상태 검사 메소드를 도메인 친화적인 용어로 변경
woosung1223 Mar 20, 2023
adb7b9e
chore: 코드 포맷팅을 통한 가독성 개선
woosung1223 Mar 20, 2023
e72e2d7
fix: 게임 진행 의도와 다르게 턴이 행동하던 버그 수정
woosung1223 Mar 20, 2023
6ebb81b
fix: 턴에서 getter 삭제로 인한 테스트 변경
woosung1223 Mar 20, 2023
a82b2da
refactor: 칸들을 2중 리스트 형태가 아닌 Map으로 관리하도록 변경
woosung1223 Mar 20, 2023
50f7bb8
docs: 구현 변경에 따른 기능목록 변경
woosung1223 Mar 20, 2023
53b948c
refactor: 캐싱된 칸을 반환할 때 가변 객체이므로 clone해서 반환
woosung1223 Mar 21, 2023
afc0124
refactor: 도메인 내부 구현 변경에 따른 출력 구현방식 변경
woosung1223 Mar 21, 2023
f8f5a5e
feat: 기물은 다른 진영의 기물을 잡을 수 있다
woosung1223 Mar 21, 2023
f8eb8da
fix: 테스트 클래스가 도메인 클래스명과 다른 문제 해결
woosung1223 Mar 21, 2023
8142fff
feat: 킹이 잡히면 게임이 종료된다
woosung1223 Mar 21, 2023
7a2e345
docs: 완성된 기능 체크 및 고민사항 기재
woosung1223 Mar 21, 2023
85ecc60
refactor: 불필요한 추상화 단계 압축
woosung1223 Mar 21, 2023
a9b00ec
chore: 패키지 추가 분리
woosung1223 Mar 21, 2023
22407a2
refactor: 도메인 용어에 맞도록 Camp -> Color 네이밍 변경
woosung1223 Mar 21, 2023
fe990f7
chore: 상속가능성이 없는 클래스 final화
woosung1223 Mar 21, 2023
170367d
refactor: 첫번째 움직임인지는 기물에서 알도록 변경
woosung1223 Mar 21, 2023
98e8ba6
chore: 메소드 네이밍을 클라이언트의 기준에서 이해할 수 있도록 변경
woosung1223 Mar 21, 2023
d09048d
refactor: pawn의 초기 움직임을 상태가 아닌 객체로 표현
woosung1223 Mar 21, 2023
a5809eb
refactor: 칸은 불변객체가 되었기에 Cloneable 구현 삭제
woosung1223 Mar 21, 2023
aaee1bb
chore: 변수명 통일성 있게 변경, 코드 포맷팅
woosung1223 Mar 21, 2023
acf3684
refactor: 보드를 초기화하는 메소드명을 이해하기 쉽도록 변경
woosung1223 Mar 22, 2023
3d961cc
refactor: 보드 생성 책임을 이미지 객체에 전부 위임
woosung1223 Mar 22, 2023
1b425a5
feat: 폰의 대각 공격 기능 생성
woosung1223 Mar 22, 2023
8ff6b7e
refactor: 메소드명을 보다 이해하기 쉽도록 수정
woosung1223 Mar 22, 2023
ea1de33
fix: 폰이 연속적으로 두칸 갈 수 있던 버그 수정
woosung1223 Mar 22, 2023
5bec05f
feat: 각 진영의 최종 점수를 계산할 수 있다
woosung1223 Mar 22, 2023
f417116
feat: 게임 결과 출력을 위한 흐름제어 생성
woosung1223 Mar 22, 2023
5e6f178
refactor: 중복해서 검사하는 예외처리 기능 삭제
woosung1223 Mar 22, 2023
d9ba41a
refactor: 사용하지 않는 메소드 제거
woosung1223 Mar 22, 2023
1147a41
refactor: 검증 메소드 병합
woosung1223 Mar 22, 2023
b9c93f2
refactor: Square, Piece -> Piece 통합
woosung1223 Mar 22, 2023
400ce9a
refactor: Color에 대한 getter 삭제, 상태검사 메소드로 변경
woosung1223 Mar 22, 2023
92b8542
refactor: 메소드명을 보다 이해하기 쉽도록 변경
woosung1223 Mar 22, 2023
a385625
refactor: Pawn 움직임 체크에 대한 중복 코드 제거
woosung1223 Mar 22, 2023
37cce48
refactor: 부생성자가 주생성자를 이용하도록 변경
woosung1223 Mar 23, 2023
114b89b
refactor: Character.isDigit을 사용함으로써 가독성 개선
woosung1223 Mar 23, 2023
2bee1e3
refactor: 뷰에서 도메인 패키지 의존성 분리
woosung1223 Mar 23, 2023
6fd7258
refactor: 컨트롤러 단 패키지 분리
woosung1223 Mar 23, 2023
f226eb4
feat: 명령 파라미터를 객체로 분리
woosung1223 Mar 23, 2023
46a4228
refactor: status 입력 시 게임이 끝나지 않도록 수정
woosung1223 Mar 23, 2023
efe7f23
refactor: 출력 메세지 조정
woosung1223 Mar 23, 2023
98ebd72
feat: 공동 우승 기능 추가
woosung1223 Mar 23, 2023
f4be3bc
refactor: 메소드 추가 분리
woosung1223 Mar 23, 2023
707270b
fix: 킹을 잡고 게임이 끝나면 점수가 출력되지 않던 문제 해결
woosung1223 Mar 23, 2023
a8b9850
docs: 기능목록 및 다이어그램 최신화
woosung1223 Mar 23, 2023
01b786f
test: 컨트롤러 레이어 테스트 보충
woosung1223 Mar 23, 2023
12c7e7a
refactor: 테스트 코드 리팩토링 및 보완
woosung1223 Mar 23, 2023
6fac6f6
test: 폰 대각 공격 테스트케이스 추가
woosung1223 Mar 23, 2023
5e9f40f
fix: 폰이 같은 팀을 공격할 수 있던 버그 수정
woosung1223 Mar 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 61 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,49 @@
### 입력
- [x] 게임 시작 혹은 종료 명령을 입력한다
- [x] 게임 이동 명령을 입력한다
- [x] 게임 결과 생성 명령을 입력한다

### 출력
- [x] 체스판을 출력한다
- [x] 체스판의 각 행을 출력한다
- [x] 게임 결과를 출력한다
- [x] 각 진영의 최종 점수를 출력한다
- [x] 어떤 진영이 승리했는지를 출력한다

### 도메인

- 체스판
- [x] 랭크를 초기화할 수 있다
- [x] 랭크들을 알고 있다
- [x] 말이 움직일지 말지 결정할 수 있다
- [x] 말이 움직일 수 없는 경우를 판단할 수 있다
- [x] 특정 위치로부터 다른 위치까지의 경로에 말이 존재하는지 확인한다
- [x] 움직일 수 있는 경우, 해당 위치로 말을 이동시킨다
- [x] 특정 좌표까지의 경로를 반환한다

- 랭크
- [x] 칸을 초기화할 수 있다
- 체스 게임
- [x] 보드를 알고 있다
- [x] 기물이 움직일지 말지 결정할 수 있다
- [x] 기물이 움직일 수 없는 경우를 판단할 수 있다
- [x] 특정 위치로부터 다른 위치까지의 경로에 기물이 존재하는지 확인한다
- [x] 움직일 수 있는 경우, 해당 위치로 기물을 이동시킨다
- [x] 기물은 다른 진영의 기물을 잡을 수 있다
- [x] 킹이 잡히면 게임이 종료된다
- [x] 올바른 턴이 아니라면 기물의 움직임을 제한할 수 있다
- [x] 턴마다 해당 진영의 기물만 움직일 수 있다
- [x] 각 진영의 최종 점수를 계산할 수 있다
- [x] 생존한 기물들의 점수의 총합으로 계산된다
- [x] 폰의 경우, 일직선 상에 같은 진영의 폰이 이미 존재한다면 특별하게 계산한다
- [x] 점수를 통해 우승자를 판단할 수 있다
- [x] 공동 우승자인 경우도 판단할 수 있다

- 보드
- [x] 칸들을 알고 있다
- [x] 특정 위치에 있는 말을 찾을 수 있다
- [x] 특정 위치에 있는 말을 바꿔줄 수 있다
- [x] 말이 특정 좌표로 움직일 수 있는지 여부를 묻는다

- 말
- [x] 칸을 찾을 수 있다
- [x] 특정 칸에 있는 기물을 찾을 수 있다
- [x] 특정 칸에 있는 기물을 교체할 수 있다
- [x] 명시된 칸에 있는 기물이 움직일 수 있는지 물을 수 있다
- [x] 진영 별 점수를 계산할 수 있다
- [x] 모든 킹이 살아있는지 여부를 알고 있다

- 기물
- [x] 자신의 점수를 알고 있다
- [x] 자신의 진영을 알고 있다
- [x] 기물 종류를 알고 있다
- [x] 특정 좌표로 움직일 수 있는지 판단한다
- [x] 서로 다른 이동 규칙을 가진다
- [x] 이동 규칙을 알고 있다
- [x] 폰의 경우, 대치 상황에서 적팀인지 아닌지에 따라 이동 규칙이 바뀐다
- [x] 특정 좌표로 움직일 수 있는지 판단할 수 있다
---

### 이동 규칙
Expand All @@ -57,18 +71,18 @@

```mermaid
classDiagram
ChessController --> Command
ChessController --> InputView
ChessController --> OutputView
ChessController --> Board
Board --> Rank
ChessGame --> Board
ChessGame --> Turn
Board --> BoardInitialImage
Board --> DirectionVector
Rank --> BoardInitialImage
Rank --> Square
Square --> Piece
Square --> Camp
Piece --> PieceType
PieceType --> Coordinate
Board --> Coordinate
Board --> Piece
Board --> Situation
Piece --> Color
Piece --> Direction
Piece --> Inclination
Piece --> Coordinate
Piece --> Situation
```


Expand All @@ -83,3 +97,20 @@ classDiagram
비즈니스 로직을 수행하기도 한다. 이 객체가 정체성이 모호하지는 않나?
- 부정문을 사용하고 싶지 않은데(`!`를 붙이는 것), 코드를 재사용하기 위해서는 부정문이 사용되어야 한다. 어쩔 수 없는 부분일까?
- 일급 컬렉션에 대한 테스트가 필요할까? 특별한 로직이 있는 것이 아니라면 컬렉션에 대한 테스트나 다름없지 않을까?

### 3단계 미션 고민사항
- [x] `BoardInitialImage` 객체가 `Square` 객체를 반환하는데, `Square` 객체가 가변이기 때문에 한 테스트가 다른 테스트에 영향을 준다.
일단은 `BoardInitialImage`에서 `Square` 반환 시 복사해서 반환하도록 조치해두었는데, 좋은 방법이 있을까? (static 사용 시 가변 객체를 다루면 조심하자!)
이건 캐싱을 하는 의미가 없지 않나?
> Square 객체를 불변으로 만듬으로써 해결 완료
- [x] Turn의 개념을 도입할 때, Square에서 `hasSameCampWith` 등의 메소드를 도입해서 Camp를 비교해 처리할 수 있다. 하지만 이게 좋은걸까? 상태를 외부에서
추측할 수 있다는 점에서, getter 사용과 다른 점이 뭘까? 저번에도 이런 문제가 발생해서 기물의 타입을 비교하는 메소드를 getter로 변경했었는데, 그냥 getter를 쓸까?
즉, getter와 동일한 역할을 하는 메소드를 도메인 용어를 사용해 그럴듯하게 포장할 것이냐, 아니면 getter로 이름을 만들고 명확하게 할 것이냐.
> 관련된 아티클 하나 써보면서 공부해보기
- [x] 같은 팀인지 판단하기 위해서 Camp끼리 비교해야 하는 상황이 왔다. 하지만 이것을 `Square`에 둘 것인가? `EmptySquare`는 사용하지 않는 속성인데.. 하지만 반대로
생각해보면 `EmptySquare`는 null Object Pattern을 사용한 것이기에 다형성을 사용한 것과는 거리가 멀지 않을까?
> null object pattern은 다형성보다는 어떤 null을 격리시키기 위한 전략에 가깝다.
- [x] 일급 컬렉션에서 체스 게임과 관련된 비즈니스 로직을 수행해야 하는가?(InitPawn -> Pawn 변환 등)
일급 컬렉션은 단순히 컬렉션에 대한 명령 - 쿼리의 책임만으로 두고, 비즈니스 로직은 `ChessGame`에 두면 디미터의 법칙을 위반하지 않나?
- [x] 오버로딩은 나쁜걸까? 퍼블릭 인터페이스가 여러개가 되니까 나쁜 게 아닐까?
- [x] 책임을 여기서 더 나눌 수 있을까? 해당 상태를 다룬다면 해당 객체에 책임이 존재하는게 맞지 않나?
99 changes: 69 additions & 30 deletions src/main/java/controller/ChessController.java
Original file line number Diff line number Diff line change
@@ -1,67 +1,106 @@
package controller;

import controller.adapter.inward.Command;
import controller.adapter.inward.CommandArguments;
import controller.adapter.inward.CoordinateAdapter;
import controller.adapter.outward.RenderingAdapter;
import domain.board.ChessGame;
import domain.piece.Coordinate;
import domain.piece.move.Coordinate;
import view.InputView;
import view.OutputView;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;

public final class ChessController {
public class ChessController {

public static final int START_COORDINATE_INDEX = 1;
public static final int END_COORDINATE_INDEX = 2;

private final InputView inputView;
private final OutputView outputView;

public ChessController(final InputView inputView, final OutputView outputView) {
private final Map<Command, BiConsumer<ChessGame, CommandArguments>> commander;

public ChessController(
final InputView inputView,
final OutputView outputView
) {
this.inputView = inputView;
this.outputView = outputView;
this.commander = new HashMap<>();
initializeCommander();
}

private void initializeCommander() {
commander.put(Command.START, this::start);
commander.put(Command.END, this::end);
commander.put(Command.MOVE, this::move);
commander.put(Command.STATUS, this::status);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분을 독특하게 작성해주셨네요!
commander 클래스를 별도로 만들어줘야 한다는 점에서 조금 걸리긴 하나,
구조 개선에 있어서는 역할을 분명히 한다는 생각도 듭니다. 일단 유지해 봅시다 👍

}

public void run() {
try {
startChessGame();
} catch (IllegalArgumentException e) {
outputView.printExceptionMessage(e.getMessage());
}
ChessGame chessGame = setupGame();
repeat(() -> interact(chessGame));
printGameResult(chessGame);
}

private void startChessGame() {
outputView.printGameStartMessage();
private ChessGame setupGame() {
ChessGame chessGame = new ChessGame();
Command command = Command.of(inputView.readCommand());
if (command.isStart()) {
startInteractionLoop(chessGame);
}
outputView.printGameEndMessage();
outputView.printGameStartMessage();
return chessGame;
}

private void startInteractionLoop(final ChessGame chessGame) {
private void repeat(Runnable target) {
try {
doOneInteraction(chessGame);
target.run();
} catch (IllegalArgumentException e) {
outputView.printExceptionMessage(e.getMessage());
startInteractionLoop(chessGame);
repeat(target);
}
}

private void doOneInteraction(final ChessGame chessGame) {
private void printGameResult(final ChessGame chessGame) {
String gameResultMessage = RenderingAdapter.unpackGameResult(chessGame.collectPoint());
String winningColorMessage = RenderingAdapter.convertWinningColor(chessGame.getWinningColor());
outputView.printGameResult(gameResultMessage);
outputView.printWinner(winningColorMessage);
}

private void interact(final ChessGame chessGame) {
Command command;
do {
outputView.printBoard(chessGame);
List<String> frontCommand = inputView.readCommand();
command = Command.of(frontCommand);
moveByCommand(chessGame, command, frontCommand);
} while (command.isNotEnd());
List<String> pureArguments = inputView.readCommand();
command = Command.of(pureArguments);
CommandArguments commandArguments = CommandArguments.of(pureArguments);
commander.get(command).accept(chessGame, commandArguments);
} while (command.isNotEnd() && chessGame.isGameNotOver());
}

private void moveByCommand(final ChessGame chessGame, final Command command, final List<String> frontCommand) {
if (command.isMove()) {
Coordinate startCoordinate = CoordinateAdapter.convert(frontCommand.get(START_COORDINATE_INDEX));
Coordinate endCoordinate = CoordinateAdapter.convert(frontCommand.get(END_COORDINATE_INDEX));
chessGame.move(startCoordinate, endCoordinate);
}
private void start(final ChessGame chessGame, final CommandArguments ignored) {
printBoard(chessGame);
}

private void end(final ChessGame chessGame, final CommandArguments ignored) {
printGameResult(chessGame);
}

private void move(final ChessGame chessGame, final CommandArguments arguments) {
Coordinate startCoordinate = CoordinateAdapter.convert(arguments.getArgumentOf(START_COORDINATE_INDEX));
Coordinate endCoordinate = CoordinateAdapter.convert(arguments.getArgumentOf(END_COORDINATE_INDEX));
Comment on lines +91 to +92

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arguments를 별도의 일급 컬렉션으로 뺀 부분이 매우 좋았습니다.
다만 그럼에도 참조하는 번지수는 Controller에서 상수로 지정해주는 부분이 아쉽네요.
ComamndArguments 내에 getStartCoordinateInfo, getEndCoordinateInfo 와 같은 메소드가 있어도 괜찮지 않을까요?
이 경우 아예 getArgumentOf를 삭제하고, 위에 적어둔 메소드로만 접근하게 해서 외부 참조 동작을 통제할 수 있다는 생각이 들어서요.

물론 이부분은 이견이 있을 수 있으므로, 자유롭게 의견 적어주시고 반영 여부 결정해주세요 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동의합니다 👍👍

좌표계 변환 자체를 완전히 Arguments의 책임으로 변경해볼게요!

chessGame.move(startCoordinate, endCoordinate);
printBoard(chessGame);
}

private void status(final ChessGame chessGame, final CommandArguments ignored) {
outputView.printGameStatus(RenderingAdapter.unpackGameResult(chessGame.collectPoint()));
printBoard(chessGame);
}

private void printBoard(final ChessGame chessGame) {
String boardMessage = RenderingAdapter.unpackBoard(chessGame.getBoard());
outputView.printBoard(boardMessage);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package controller;
package controller.adapter.inward;

import java.util.Arrays;
import java.util.List;
Expand All @@ -7,7 +7,9 @@ public enum Command {

START("start", 1),
END("end", 1),
MOVE("move", 3);
STATUS("status", 1),
MOVE("move", 3),
;

private final String message;
private final int commandCount;
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/controller/adapter/inward/CommandArguments.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package controller.adapter.inward;

import java.util.List;

public class CommandArguments {

private final List<String> arguments;

private CommandArguments(final List<String> arguments) {
this.arguments = arguments;
}

public static CommandArguments of(final List<String> arguments) {
return new CommandArguments(arguments);
}

public String getArgumentOf(final int index) {
if (index < 0 || index >= arguments.size()) {
throw new IllegalArgumentException("[ERROR] 명령 인자가 존재하지 않습니다.");
}
return arguments.get(index);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package controller;
package controller.adapter.inward;

import domain.piece.Coordinate;
import domain.piece.move.Coordinate;

public class CoordinateAdapter {
public final class CoordinateAdapter {

private static final char ASCII_ALPHABET_A = 'a';
public static final int COMMAND_SIZE = 2;
Expand All @@ -22,17 +22,16 @@ private static void validateSize(final String frontCoordinate) {

private static int convertToRow(final String frontCoordinate) {
char pureRow = frontCoordinate.charAt(1);
if (pureRow >='0' && pureRow <= '9') {
if (Character.isDigit(pureRow)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 👍 👍

return Character.getNumericValue(pureRow) - 1;
}
throw new IllegalArgumentException("[ERROR] Y축 좌표는 숫자여야 합니다.");
}

private static int convertToCol(final String frontCoordinate) {
char pureCol = frontCoordinate.charAt(0);
if (Character.isAlphabetic(pureCol) &&
Character.isLowerCase(pureCol)) {
return (int) pureCol - ASCII_ALPHABET_A;
if (Character.isAlphabetic(pureCol) && Character.isLowerCase(pureCol)) {
return pureCol - ASCII_ALPHABET_A;
}
throw new IllegalArgumentException("[ERROR] X축 좌표는 알파벳 소문자여야 합니다.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package view;

import domain.square.Camp;
import domain.square.Square;
import domain.piece.Bishop;
import domain.piece.King;
import domain.piece.Knight;
import domain.piece.Pawn;
package controller.adapter.outward;

import domain.piece.Color;
import domain.piece.Piece;
import domain.piece.Queen;
import domain.piece.Rook;
import domain.piece.sliding.Bishop;
import domain.piece.nonsliding.King;
import domain.piece.nonsliding.Knight;
import domain.piece.pawn.Pawn;
import domain.piece.sliding.Queen;
import domain.piece.sliding.Rook;

import java.util.HashMap;

Expand All @@ -27,17 +26,17 @@ public final class PieceTypeMapper {
mapper.put(Pawn.class, "p");
}

public static String getTarget(final Square square) {
public static String getTarget(final Piece piece) {
String message = mapper.keySet().stream()
.filter(pieceType -> pieceType.isInstance(square.getPieceType()))
.filter(pieceType -> pieceType.isInstance(piece))
.map(mapper::get)
.findAny()
.orElseGet(() -> ".");
return makeUpperCaseIfCampIsBlack(square, message);
.orElse(".");
return makeUpperCaseIfCampIsBlack(piece, message);
}

private static String makeUpperCaseIfCampIsBlack(final Square square, final String message) {
if (square.getCamp() != null && square.getCamp().equals(Camp.BLACK)) {
private static String makeUpperCaseIfCampIsBlack(final Piece piece, final String message) {
if (piece.hasSameColorWith(Color.BLACK)) {
return message.toUpperCase();
}
return message;
Expand Down
Loading