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

[1, 2, 3단계 - 체스] 기론(김규철) 미션 제출합니다. #295

Merged
merged 53 commits into from
Apr 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
6c8ca60
docs: 1단계 기능 요구 사항 정리
Gyuchool Mar 22, 2022
5fbb082
feat: 체스판을 만들고 체스 말들을 초기화 시킨다.
Gyuchool Mar 22, 2022
325f6ee
feat: start와 end가 아닌 입력이 들어오면 예외가 발생한다.
Gyuchool Mar 22, 2022
110c301
feat: 게임 시작과 종료 기능 구현
Gyuchool Mar 22, 2022
742ff00
feat: 체스판과 말들을 출력한다.
Gyuchool Mar 22, 2022
9f5689e
refactor: rank 순서 변경
Gyuchool Mar 22, 2022
10e5026
docs: 2단계 기능 요구 사항 정리
Gyuchool Mar 23, 2022
23e2261
feat: position끼리 수평에 위치하는지 비교하는 기능 구현
Gyuchool Mar 23, 2022
27341e6
feat: position끼리 수직에 위치하는지 확인하는 기능 구현
Gyuchool Mar 23, 2022
4a094b2
test: 룩의 진행방향이 다르면 예외가 발생하는 테스트 추가
Gyuchool Mar 23, 2022
84c3914
refactor: piece의 추상메서드 수정
Gyuchool Mar 23, 2022
649f8db
feat: position이 대각선에 있는지 비교하는 기능 구현
Gyuchool Mar 23, 2022
78e60ce
feat: 퀸의 진행방향이 옳은지 확인하는 기능 구현
Gyuchool Mar 23, 2022
0447917
feat: 킹의 진행방향과 거리가 옳은지 확인하는 기능 구현
Gyuchool Mar 23, 2022
32e0cf8
feat: 나이트의 진행방향과 거리가 옳은지 확인하는 기능 구현
Gyuchool Mar 23, 2022
62d79c0
feat: 폰의 진행방향과 거리가 옳은지 확인하는 기능 구현
Gyuchool Mar 23, 2022
cc548fe
refactor: piece들이 각각 이름을 갖도록 수정
Gyuchool Mar 24, 2022
0f6128e
feat: 체스판의 빈곳을 표현하는 말 추가
Gyuchool Mar 24, 2022
4baa7d6
refactor: compareTo 메서드에서 depth 2 제거
Gyuchool Mar 24, 2022
95abedd
feat: command 구조 상태패턴으로 변경
Gyuchool Mar 24, 2022
d44b7f2
feat: 룩이 상하좌우로 칸수 제한없이 움직이도록 기능 구현
Gyuchool Mar 25, 2022
e2b867a
feat: 킹이 상하좌우, 대각선 한 칸만 움직일수 있다.
Gyuchool Mar 25, 2022
e9a9253
feat: 비숍은 대각선으로만 칸수 제한없이 움직인다.
Gyuchool Mar 25, 2022
61b3ce2
feat: 나이트의 이동 규칙에 맞게 기능 구현
Gyuchool Mar 25, 2022
754ff24
feat: 퀸이 상하좌우, 대각선 칸수 제한없이 움직인다.
Gyuchool Mar 25, 2022
708d657
feat: 폰의 이동규칙에 맞게 기능 추가
Gyuchool Mar 26, 2022
aadcce9
feat: 폰이 상대 말을 잡을때의 기능 구현
Gyuchool Mar 26, 2022
7bb3409
test: 체스판 범위를 벗어나는지 검증 테스트
Gyuchool Mar 26, 2022
c235483
refactor: OutputView에서 게임 시작 메세지를 출력한다.
Gyuchool Mar 26, 2022
7f92619
docs: 3단계 기능 요구사항 정리
Gyuchool Mar 26, 2022
a267622
feat: 킹이 죽어도 게임이 끝난다.
Gyuchool Mar 26, 2022
0a4fead
feat: 남아있는 말에 대한 점수를 계산한다.
Gyuchool Mar 26, 2022
2cd9b8b
feat: 게임이 끝나고 status를 입력하면 결과를 보여주고 우승 팀을 알려준다.
Gyuchool Mar 26, 2022
daa63dc
feat: 턴을 추가한다.
Gyuchool Mar 28, 2022
58212d7
refactor: 안쓰는 import문 제거 및 코드 포멧팅
Gyuchool Mar 28, 2022
1283e48
refactor: final class로 수정
Gyuchool Mar 28, 2022
5c6e0a8
refactor: killstrategy 제거
Gyuchool Mar 28, 2022
afbbb7a
refactor: stream을 이용하여 변경
Gyuchool Mar 28, 2022
354e7bd
feat: docker-compose 설치
Gyuchool Mar 29, 2022
8e2d464
refactor: error메시지 수정
Gyuchool Mar 29, 2022
5a60cb1
refactor: 예외가 발생할 때 return 을 던짐으로써 validate메서드 분리
Gyuchool Mar 29, 2022
7f111ef
refactor: 불필요한 주석 제거
Gyuchool Mar 29, 2022
40b2a1f
refactor: 인터페이스 이름 범용적이도록 수정
Gyuchool Mar 29, 2022
0e074b5
refactor: 정적 펙터리 메서드 네이밍 수정
Gyuchool Mar 29, 2022
4f377c2
refactor: 빈칸 초기화하는 메서드 네이밍 수정
Gyuchool Mar 29, 2022
c0c53e8
refactor: view에서 도메인 의존성 제거
Gyuchool Mar 29, 2022
3190f12
test: pieces 테스트 추가
Gyuchool Mar 29, 2022
b7b3972
test: command관련 테스트 추가
Gyuchool Mar 30, 2022
ad6e2bb
refactor: 변수 이름 의미 명확하도록 수정
Gyuchool Mar 30, 2022
42df1b1
refactor: 조건문 메서드로 분리
Gyuchool Mar 30, 2022
8235b85
refactor: 게임이 바로 종료되지 않도록 수정
Gyuchool Mar 30, 2022
6c770d0
refactor: 명령어 입력 형식 잘못되어도 다시 입력받도록 수정
Gyuchool Mar 30, 2022
6896212
refactor: position에 캐싱 적용
Gyuchool Mar 31, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,8 @@ out/

### VS Code ###
.vscode/

### Docker ###
docker/db/mysql/data/
docker/db/mysql/init/*
!docker/db/mysql/init/init.sql
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,73 @@
## 우아한테크코스 코드리뷰

- [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md)

## 1단계

### 입력

- [x] 게임 시작할때 start를 입력받는다.
- [x] 게임 종료할때 end를 입력받는다.
- [x] start와 end가 아닌 입력이 들어오면 예외가 발생한다.

### 게임

- [x] 체스판을 만든다.
- [x] 체스 말들을 체스판 위에 초기화시킨다. 말이 없으면 `.`으로 초기화 시킨다.

### 출력

- [x] 체스판을 출력한다.
- [x] 말이 있는 위치는 알파벳으로 표현한다.

## 2단계

### 입력

- [x] 말을 움직일때는 move`(source위치 target위치)`를 입력받는다.
- [x] 체스판을 벗어나는 위치가 입력되면 예외가 발생한다.
- [x] move 입력 형식과 다른 경우 예외가 발생한다.
- [x] start를 입력한 후에 move가 입력되어야 한다.

### 게임

- [x] 말이 움직이면 source위치는 `.`이 되고 target위치에 말이 표시된다.
- [x] `나이트`를 제외한 말들은 앞에 말들이 있으면 뛰어넘을 수 없다.
- [x] 규칙에 벗어난 말들의 움직임은 예외가 발생한다.

### 폰

- [x] 폰은 처음 시작할 때, 한칸 또는 두 칸 움직일수 있다.
- [x] 폰은 상대 말을 잡을 때, 대각선 한칸만 움직여서 잡을수 있다.
- [x] 처음 움직임 이후로는 한 칸만 움직일 수 있다.
- [x] 뒤로 움직일 수 없다.

### 룩

- [x] 상하좌우로 칸수 제한없이 움직인다.

### 나이트

- [x] 앞으로 두칸 전진하고 좌 또는 우로 한칸 움직인다.
- [x] 앞에 말들이 있어도 뛰어넘을수 있다.

### 비숍

- [x] 대각선으로만 칸수 제한없이 움직인다.

### 퀸

- [x] 상하좌우, 대각선 칸수 제한없이 움직인다.

### 킹

- [x] 상하좌우, 대각선 한 칸만 움직일수 있다.

## 3단계

### 게임

- [x] 킹이 죽거나 `end`를 입력하면 게임이 끝난다.
- [x] 남아있는 말에 대해서 점수를 계산한다.
- [x] 게임이 끝나고 `status`를 입력하면 결과를 보여주고 우승 팀을 알려준다.
- [x] 게임이 끝나고 아무키나 입력하면 그냥 종료된다.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {
implementation 'com.sparkjava:spark-core:2.9.3'
implementation 'com.sparkjava:spark-template-handlebars:2.7.1'
implementation 'ch.qos.logback:logback-classic:1.2.10'
runtimeOnly 'mysql:mysql-connector-java:8.0.28'

Choose a reason for hiding this comment

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

DB가 붙는군요! 설레네요(?)

여기서 한가지 퀴즈~!

runtimeOnly와 implementation 그리고 testImplementation는 각각 어떤 의미로 build.gradle에서 사용될까요? 댓글로 남겨주세요!

Copy link
Author

Choose a reason for hiding this comment

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

오 퀴즈는 언제나 설레네요 ㅎㅎㅎ
퀴즈 공부하면서 grdle의 의존성 옵션에 대해서 알아볼 수 있었습니다!
runtimeOnly

  • runtimeClassPath에만 추가된다.
  • 해당 클래스에서 코드 변경이 발생해도 런타임에만 불러오기 때문에 컴파일을 다시 할 필요가 없다는 장점이 있습니다.

implementation

  • A->B->C 모듈 순서로 의존성이 있을 때, A에서 B를 implementation으로 설정하면 B에 대한 모듈만 가져오고 C에 대해서는 가져오지 않습니다!
  • 따라서 C가 수정되었더라도 A를 다시 컴파일할 필요가 없습니다.

testImplementation

  • implementation이 테스트코드를 수행할 때만 적용한다.

와 같이 정리했습니다!
정리글

Choose a reason for hiding this comment

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

정리 잘 해주셨네요! 좋습니당 블로그 글도 확인했어요 기론은 글도 잘 작성하시는군요 👍

testImplementation 'org.assertj:assertj-core:3.22.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2'
}
Expand Down
15 changes: 15 additions & 0 deletions docker/db/mysql/init/init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE member
(
id varchar(10) not null,
name varchar(20) not null,
primary key (id)
);

CREATE TABLE role
(
member_id varchar(10) not null,
role varchar(10) not null,
primary key (member_id),
foreign key (member_id) references member (id)
);

18 changes: 18 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: "3.9"

Choose a reason for hiding this comment

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

docker-compose는 어떤 의도에서 추가하셨는지 궁금해요~!

Copy link
Author

Choose a reason for hiding this comment

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

아 docker-compose는 오늘 수업 중에 실습 했던 내용입니다!
깃에 추가하고 싶은 사람은 추가해도 된다고 해서 추가했었습니다!

Choose a reason for hiding this comment

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

오호 그런 배경이 있었군요!

해당 기술을 잘 익혀두시면 추후에 프로젝트할 때나 현업에 와서도 잘 활용하실 수 있을거에요!

services:
db:
image: mysql:8.0.28
platform: linux/x86_64
restart: always
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: chess
MYSQL_USER: user
MYSQL_PASSWORD: password
TZ: Asia/Seoul
volumes:
- ./db/mysql/data:/var/lib/mysql
- ./db/mysql/config:/etc/mysql/conf.d
- ./db/mysql/init:/docker-entrypoint-initdb.d
104 changes: 104 additions & 0 deletions src/main/java/chess/Board.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package chess;

import chess.piece.Empty;
import chess.piece.Piece;
import chess.piece.Pieces;
import chess.piece.position.Position;

import java.util.List;

public final class Board {

private final Pieces pieces;

Choose a reason for hiding this comment

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

체스판을 구현하는 여러가지 방법이 있을텐데요. 기론은 자신의 위치에 해당하는 정보를 들고 있는 각 기물들의 모음을 체스판으로 봤어요.

다른 방식으로 구현한다면 8X8의 체스판 자체를 이중 리스트나 맵 형식으로 구조화할 수도 있겠죠!

기론이 기론의 방식으로 구현하며 느낀 장점이나 단점이 있을까요? 다른 방식으로 구현한다면 어떤 장단점이 있을지도 상상해보면 재미있겠네요!

Copy link
Author

@Gyuchool Gyuchool Mar 30, 2022

Choose a reason for hiding this comment

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

처음엔 Map<Position, Piece>를 갖는 board를 만들었습니다. 그런데 구현을 진행하면서 페어와 board가 가지고 있는 상태에 대해서 이야기를 했는데, board에서 위치와 말을 모두 알아야하나? 라는 생각을 먼저 했습니다.

boardpiece를 움직여서 게임하는 것이므로 piece들만 관리하는 게 더 맞지 않을까 라고 생각하였고 boardpiece만 알고 piece가 자신의 위치를 가지고 있도록 구현하였습니다.

또한 boardpieceposition모두를 관리하지 않고 piece들만 관리하면 되기 때문에 결합도가 더 낮아지지않을까? 라고 생각했습니다.

장점으로는 boardpieces를 가지면서 board클래스 내에서 하는 일이 많이 줄어들었습니다. 대부분 piece들이 움직이므로 책임의 대부분이 pieces 또는 piece로 갔어요!
단점으로는 체스판을 출력해줄 때, piece의 위치를 매번 확인해서 마지막 file에 있는 자리인지 확인해야 한다는 단점이 있었습니다.

아마 기존의 Map방식으로 사용한다면
Map의 자료구조 특성상 요소의 추가 삭제가 List보다 성능이 나을 때가 많고 검색 성능은 더 좋아서 성능상에선 더 좋을 것 같네요🤔
대신 제 생각에는 객체의 행위에 집중하면 boardposition까지 알 필요가 있나 라고 생각되어서 이게 단점인 것 같습니다.

생각하면 생각할수록 어렵네요🤔🤔 각각 트레이드오프가 있는 것 같습니다.
토니였다면 어떤 선택을 하셨을까요?

또한 토니는 제 생각에 대해서 어떻게 생각하시나요?

객체의 행위에 집중하면 boardposition까지 알 필요가 있나?

boardpiece들을 가지고 게임하므로 position은 알 필요가 없다고 생각해서 나온 이 생각이 정말 객체의 행위에 집중한 게 맞을까요?
오브젝트 책의 내용에 나온 행동을 바탕으로 설계하는 것을 적용해보려고 하는데 읽는 것과 실제 적용하기에는 너무 어렵네요😂😂

Choose a reason for hiding this comment

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

크... 많은 고민을 하셨군요 기론! 상세하게 적어주셔서 이해하는 데에 도움이 많이 되었어요 감사합니다! 페어와 함께 고민하셨다는 부분 너무 좋네요.

항상 개발을 하다보면 트레이드 오프가 있죠! 체스 게임이 엄청 큰 서비스가 아니기에 성능적인 고려보다는 코드를 이해하기 좋은 방식으로 구성할거 같아요. 지금 기론의 방식은 좋은 접근이라 생각해요. 각 piece를 자신의 위치도 관리하는 주체적인 객체라고 정의한다!


private Board(Pieces pieces) {
this.pieces = pieces;
}

public static Board create(Pieces pieces) {
return new Board(pieces);
}

public Pieces getPieces() {
return pieces;
}

public void move(List<String> commandPosition, Turn thisTurn) {
Position sourcePosition = getSource(commandPosition);
Position targetPosition = getTarget(commandPosition);

Piece sourcePiece = pieces.findByPosition(sourcePosition);
Piece targetPiece = pieces.findByPosition(targetPosition);
validateTurn(thisTurn, sourcePiece);
validateMovable(targetPosition, sourcePiece, targetPiece);
move(sourcePosition, targetPosition, sourcePiece, targetPiece);
}

private void validateTurn(Turn thisTurn, Piece sourcePiece) {
if (!sourcePiece.isCurrentTurn(thisTurn)) {
throw new IllegalArgumentException("[ERROR] 현재 차례가 아닙니다.");
}
}

private void validateMovable(Position targetPosition, Piece sourcePiece, Piece targetPiece) {
if (isNotMovable(targetPosition, sourcePiece, targetPiece)) {
throw new IllegalArgumentException("[ERROR] 움직일수 없습니다.");
}
}

private boolean isNotMovable(Position targetPosition, Piece sourcePiece, Piece targetPiece) {
return (!sourcePiece.isMovableRange(targetPosition) || hasBlock(sourcePiece, targetPiece)) &&
!sourcePiece.isKill(targetPiece);
}

private void move(Position sourcePosition, Position targetPosition, Piece sourcePiece, Piece targetPiece) {
sourcePiece.moveTo(targetPosition);
pieces.remove(targetPiece);
pieces.add(new Empty(sourcePosition));
}

private boolean hasBlock(Piece sourcePiece, Piece targetPiece) {
if (sourcePiece.isSameTeam(targetPiece)) {
return true;
}
List<Position> positions = sourcePiece.getIntervalPosition(targetPiece);
return positions.stream()
.anyMatch(position -> !pieces.findByPosition(position).equals(new Empty(position)));
}

private Position getSource(List<String> commandPosition) {
return Position.of(
commandPosition.get(0).charAt(0),
commandPosition.get(0).charAt(1));
}

private Position getTarget(List<String> commandPosition) {
return Position.of(
commandPosition.get(1).charAt(0),
commandPosition.get(1).charAt(1)
);
}

public boolean isDeadKing() {
return pieces.countOfKing() == 1;
}

public double getWhiteTeamScore() {
return pieces.getTotalScore(Team.WHITE);
}

public double getBlackTeamScore() {
return pieces.getTotalScore(Team.BLACK);
}

public Team getWinTeam() {
if (getWhiteTeamScore() > getBlackTeamScore()) {
return Team.WHITE;
}
if (getBlackTeamScore() > getWhiteTeamScore()) {
return Team.BLACK;
}
return Team.NONE;
}
}

10 changes: 10 additions & 0 deletions src/main/java/chess/ConsoleApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package chess;

import chess.controller.ChessController;

public class ConsoleApplication {
public static void main(String[] args) {
ChessController chessController = new ChessController();

Choose a reason for hiding this comment

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

현재의 구조에서는 게임을 진행하다가 실수할 경우에 체스 게임이 바로 종료되어요 😂

요구사항에 명시되지 않았지만 프러덕트를 개발하는 개발자로서 사용자가 실수하더라도 적절한 메시지를 노출한 후에 이어서 진행할 수 있도록 수정해보는건 어떨까요?

image

Copy link
Author

Choose a reason for hiding this comment

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

명짤(?)이네요.. 저장해둬야겠습니다😁수정했습니다!

chessController.run();
}
}
18 changes: 18 additions & 0 deletions src/main/java/chess/Team.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package chess;

public enum Team {
BLACK(-1),
WHITE(1),
NONE(0),
;

private final int forwardDirection;

Team(int forwardDirection) {
this.forwardDirection = forwardDirection;
}

public int getForwardDirection() {
return this.forwardDirection;
}
}
43 changes: 43 additions & 0 deletions src/main/java/chess/Turn.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package chess;

import java.util.Objects;

public final class Turn {
private final Team team;

public Turn(Team team) {
this.team = team;
}

public static Turn init() {
return new Turn(Team.WHITE);
}

public boolean isCurrentTeam(Team team) {
return this.team.equals(team);
}

public Turn change() {
if (team.equals(Team.BLACK)) {
return new Turn(Team.WHITE);
}
return new Turn(Team.BLACK);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Turn turn = (Turn) o;
return team == turn.team;
}

@Override
public int hashCode() {
return Objects.hash(team);
}
}
44 changes: 44 additions & 0 deletions src/main/java/chess/command/Command.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package chess.command;

import java.util.List;

public abstract class Command implements State {

protected final String input;

public Command(String input) {
validateCommand(input);
this.input = input;
}

private void validateCommand(String input) {
if (!input.contains("move") && !"start".equals(input) && !"end".equals(input) && !"status".equals(input)) {
throw new IllegalArgumentException("[ERROR] 명령어는 start, move, end, status 중 하나여야합니다.");
}
}

@Override
public Command turnFinalState(String input) {
return new End(input);
}

@Override
public boolean isEnd() {
return false;
}

@Override
public boolean isMove() {
return false;
}

@Override
public boolean isStatus() {
return false;
}

@Override
public List<String> getCommandPosition() {
throw new IllegalStateException("[ERROR] 명령어에서 위치를 얻을수 없습니다.");
}
}
26 changes: 26 additions & 0 deletions src/main/java/chess/command/End.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package chess.command;

public final class End extends Command {

public End(String input) {
super(input);
}

@Override
public Command turnState(String input) {
return this;
}

@Override
public Command turnFinalState(String input) {
if ("status".equals(input)) {
return new Status(input);
}
return new End(input);
}

@Override
public boolean isEnd() {
return true;
}
}