[우아한 테크코스 3기] LEVEL 1 회고 (44일차)
안녕하세요? 제이온입니다.
오늘은 온라인으로 우테코를 진행하였지만, 크루인 아론과 만나서 페어 프로그래밍을 진행하였습니다.
데일리 미팅
오늘 데일리 미팅 진행자는 제리였습니다. 제리도 게임을 가져왔고, 각 플레이어는 제리에게 배정 받은 미션을 비밀스럽게 수행해야하는 것이 규칙이었습니다. 저는 성대모사를 한 후 남들에게 비슷하다는 소리를 듣는 것이 미션이었는데, 성대모사 할 줄 아는 것이 없어서... 게임에 제대로 참여하지는 못했습니다. 그래서 저 이외의 미션을 실패한 크루들끼리 모여서 채팅으로 눈치 게임을 진행하였습니다.
총 4명이서 눈치 게임을 하였는데, 저 빼고 3명이 동시에 "2"라고 채팅을 쳐서 다행히 내일 데일리 미팅은 진행하지 않게 되었습니다 ㅎㅎ 대신 2차 눈치게임에서 파피가 걸렸습니다. 요새 다들 게임을 가져오는 것 같은데.. 웬만해서는 안 걸리는 것이 좋겠습니다.
페어 프로그래밍 - 1
아론과 오전 11시 20분 쯤에 만나서 같이 돈까스를 먹고 인근 카페에 갔습니다. 어제까지 1단계 미션을 끝냈기 때문에 나름 첫 단추를 잘 끼운 기분이었고, 그러한 긍정적인 흐름에 힘입어 쭉쭉 개발을 이어나갔습니다.
오늘은 시작점과 끝점을 입력받아서 특정 체스말이 이동가능한지 체크하는 것이 관건이었습니다. 처음에는 폰부터 시작하였고, 폰은 아래 3가지 이동 전략이 있었습니다.
1. 북쪽으로 1칸 이동한다.
2. 북서쪽 또는 북동쪽으로 1칸 이동한다.
3. 폰을 처음 움직이는 경우, 2칸 이동할 수 있다.
폰이 이동할 수 있는 방향은 총 4가지가 있기 때문에 이 방향들을 리스트에 모아 두었습니다. 그리고 플레이어가 입력한 두 위치를 빼서 어느 방향에 속하는지 판단하였습니다. 그리고 각각의 방향에 대해 이동 여부를 확인하도록 설계하였습니다.
1. 북쪽으로 한 칸 이동할 때, 빈 공간이어야 한다.
2. 북서쪽 또는 북동쪽에 상대 체스말이 있을 경우, 그 방향으로 1칸 이동한다.
3. 체스말이 처음 위치에 있다면, 2칸 이동할 수도 있다.
위 조건에 만족하도록 소스 코드를 작성하였습니다.
private boolean checkPossible(final Direction direction, final Piece piece, final Vertical vertical) {
if (direction == Direction.NORTH) {
return piece.equals(new Blank());
}
if (direction == Direction.NORTHEAST || direction == Direction.NORTHWEST) {
return piece.isOpponent(this);
}
if (direction == Direction.INITIAL_PAWN_NORTH) {
return (vertical.getValue() == 2 || vertical.getValue() == 7) && piece.equals(new Blank());
}
return false;
}
여기서 중요한 점은 폰은 뒤로 갈 수 없으므로 블랙 팀 폰과 화이트 팀 폰을 구분해야 합니다. 가령, 화이트 팀 폰이 앞으로 가는 명령은 "move a2 a3"이므로 차이가 (0, 1)이고, 블랙 팀 폰이 앞으로 가는 명령은 "move a7 a6" 이므로 차이가 (0, -1)이 됩니다. 따라서, 해당 폰이 블랙인지 화이트인지에 따라서 빼는 방향을 다르게 해 주는 메소드를 추가하였습니다.
private List<Integer> subtractByTeam(Position source, Position target) {
if (isBlack) {
return source.subtract(target);
}
return target.subtract(source);
}
이렇게 이동 규칙이 까다로웠던 폰은 구현하였습니다.
룩, 비숍, 퀸, 폰을 제외한 나머지 말들은 단순히 갈 수 있는 방향을 정해놓고, 끝점이 상대말이거나 빈 공간인지 확인하면 돼서 쉬웠습니다. 예를 들어, 나이트 말의 이동 판단은 아래와 같이 소스 코드를 작성할 수 있습니다.
public class Knight extends Piece {
private static final List<Direction> POSSIBLE_DIRECTIONS = Arrays.asList(Direction.NNE, Direction.NNW, Direction.SSE,
Direction.SSW, Direction.WWN, Direction.WWS, Direction.EEN, Direction.EES);
private static final String INITIAL_NAME = "N";
public Knight(final boolean isBlack) {
super(isBlack, INITIAL_NAME);
}
@Override
public boolean canMove(final Position source, final Position target, final Piece piece) {
if (!isPossibleDirection(source, target)) {
throw new IllegalArgumentException("해당 위치로 이동할 수 없습니다.");
}
return isOpponent(piece) || piece.equals(new Blank());
}
private boolean isPossibleDirection(final Position source, final Position target) {
return POSSIBLE_DIRECTIONS.stream()
.anyMatch(possibleDirection -> possibleDirection.isSameDirection(target.subtract(source)));
}
}
플레이어가 입력한 시작점과 끝점의 차이를 구한 뒤, 방향 리스트에서 그 방향이 있는지 확인한 후, 끝점에 상대말이 있거나 빈 공간인지 체크하는 것이죠.
그래서 여기까지는 페어 프로그래밍도 화목하였고, 시간도 오래 걸리지 않았습니다. 하지만.. 아래부터 기술할 내용이 굉장히 빡셌습니다.
페어 프로그래밍 - 2
지금까지 한 내용은 폰, 킹, 나이트에 대한 이동 판단입니다. 아직 퀸, 룩, 비숍에 대한 이동 판단은 구현하지 않았습니다. 왜냐하면 이것들은 각 방향에 대해 무제한으로 이동할 수 있어서 구현하기 어려운 면이 있었죠.
처음에는 재귀 함수를 통해서 시작점에서 끝점까지 갈 수 있는지 검사하려고 하였으나, 재귀 함수 자체를 구현하기 어려웠을 뿐만 아니라 다형성을 만족하기가 힘들었습니다. 여기서 폰, 킹, 나이트는 굳이 끝점까지 갈 수 있는지 한 칸씩 확인할 필요가 없어서 부득이하게 instanceof를 통해 타입 체크해야하였기 때문이죠.
그래서 퀸, 룩, 비숍의 특징을 깊게 고민해 보았습니다. 거기서 저는 한 가지 특징들을 캐치해 냈습니다. 룩은 가로와 세로를 무제한으로 이동할 수 있으므로 각각 시작점과 끝점의 차이 좌표 (x, y) 중 하나라도 0이 있다면 가능한 입력값이라는 것입니다. 예를 들어, 시작점과 끝점의 차이 좌표가 (3, 0)이라면 (1, 0) 방향이되, 길이는 3인 것입니다. 하지만, 시작점과 끝점의 차이 좌표가 (1, 2)라면 직선 방향이 아니므로 가능한 입력값이 아닙니다.
비슷한 맥락으로, 비숍은 대각선을 무제한으로 이동할 수 있으므로 시작점과 끝점의 차이 좌표 (x, y)의 절댓값이 같아야 합니다. 그리고 퀸은 룩과 비숍의 특징을 둘 다 가지고 있습니다.
여기까지 각 말들의 규칙을 찾았으므로 구현만 하면 되겠다는 생각이 들었습니다. 하지만, 아론이 우리가 여태까지 간과하였던 부분을 지적해 주었습니다. 바로, 룩이 (a, 1)에서 (a, 5)까지 갈 수는 있지만, 중간 경로인 (a, 4)가 빈 공간이 아니라면 이동할 수 없다는 것이죠.
그래서 이때가 오후 2시 30분 정도였는데, 7시 20분 집 갈 때까지 중간 경로를 구현하기 위해서 온갖 노력을 하였습니다. 퀸, 룩, 비숍만 특정 방향으로 무제한 이동이므로 이 객체들의 상위 클래스를 따로 뺄까도 생각하였지만, 구조만 복잡해지고 원하는 결과는 나오지 않았습니다.
그렇게 시간이 계속 흐르다가 서로 다른 아이디어가 생겨서 각자 생각한 부분을 코딩해 보기로 하였습니다. 저와 아론은 중간 경로를 구하는 방식이 달랐지만, 운 좋게도 서로의 방식을 합치면 정답에 가까운 설계가 되었습니다! 저는 '어떠한 경우 중간 경로인가?'를 판단하려고 노력하였고, 아론은 중간 경로 리스트를 만들었다고 가정한 다음 그것들이 모두 빈 공간이지 확인하는 로직을 세우려고 노력하였습니다.
private boolean checkPath(Position source, Position target) {
List<Position> paths = new ArrayList<>();
if (source.hasMiddlePath(target)) {
paths = updatePosition(source, target);
} // 여기서 NOTHING 일 때 false 반환하도록 수정해야 함.
if (paths.isEmpty() || chessBoard.get(source) instanceof Pawn) {
return chessBoard.get(source).canMove(source, target, chessBoard.get(target));
}
if (paths.stream().allMatch(path -> chessBoard.get(path).equals(new Blank()))) {
return chessBoard.get(source).canMove(paths.get(paths.size() - 1), target, chessBoard.get(target));
}
return false;
}
public List<Position> updatePosition(Position source, Position target) {
List<Position> paths = new ArrayList<>();
final Direction direction = source.decideDirection(target);
Position nextPosition = source.next(direction);
while (!nextPosition.equals(target) || direction == Direction.NOTHING) {
paths.add(nextPosition);
nextPosition = nextPosition.next(direction);
}
return paths;
}
먼저, Position 클래스의 hasMiddlePath() 메소드를 통해 플레이어가 입력한 시작점과 끝점이 '중간 경로'가 존재하는지 확인하였습니다. 중간 경로는 단어만 들으면 온갖 경로가 다 될 것 같지만, 시작점과 끝점을 이은 선이 직선이어야 합니다. 즉, 가로와 세로 방향 또는 대각선 방향일 때만 중간 경로가 가능한 것이죠. 예를 들어, 나이트는 직선 방향이 아니므로 중간 경로가 존재하지 않습니다.
그리고 updatePosition() 메소드를 통해 실제 중간 경로를 담습니다. Position 클래스의 decideDirection() 메소드를 통해 플레이어가 입력한 시작점과 끝점이 무슨 방향인지 확인합니다. 그리고 그 방향으로 한 칸씩 계속 이동하다가 끝점에 도달한다면 메소드를 종료합니다.
만약, 중간 경로 리스트가 비어있다면 시작점을 그대로 체스말의 canMove() 메소드 인자로 넘겨줍니다. 여기서 해당 객체가 폰인지도 확인하였는데, 이것은 아래 조건문부터 말씀드리고 설명하겠습니다.
아래 조건문은 중간 경로이 모두 빈 공간인지 확인하는 것입니다. 만약, 참이라면 canMove()의 인자로 시작점을 그대로 넘기지 않고 끝점 바로 전 좌표로 넘깁니다. 가령, 시작점이 (1, 1)이고 끝점이 (5, 5)라면 canMove()의 인자로 넘길 때 시작점은 (4, 4)가 되는 것입니다. 이렇게 한 이유는 Direction enum 클래스에는 '단위' 방향만 정의해 두었기 때문입니다. 그래서 퀸, 룩, 비숍은 무조건 1칸 짜리 이동만 검사할 수 있게 되므로 설계가 단순해진다는 장점이 있습니다.
그런데 여기서 폰에 대해 예외 처리를 하지 않는다면, 폰의 3번째 이동 방식에 문제가 생깁니다. 폰은 처음 이동하는 경우에 2칸 이동할 수 있으므로 중간 경로 리스트에 위치 하나가 들어갑니다. 그래서 이 리스트가 비었다고 판단하지 않아서 폰의 canMove()의 인자로 시작점이 중간 경로의 위치로 바뀌게 됩니다.
따라서, 폰을 위와 같이 예외 처리를 하였는데 생각해 보니 부족한 점이 있습니다. 왜냐하면, 폰이 처음 이동하는 시점에 1칸 앞의 체스말이 존재할 수도 있기 때문이죠. 이것을 넘고 이동은 불가능하므로 결국 폰도 중간 경로를 확인해 주어야 합니다. 이 부분은 기억해 두었다가 아론과 이야기해 보아야겠습니다.
public Position next(final Direction direction) {
String str = this.horizontal.getSymbol();
char c = (char) (str.charAt(0) + direction.getHorizontalDegree());
String str2 = this.vertical.getSymbol();
char c2 = (char) (str2.charAt(0) + direction.getVerticalDegree());
return new Position(String.valueOf(c), String.valueOf(c2));
}
그리고 둘다 멘탈이 바스락나고 있었기에 기능 구현을 일단 1순위로 두었습니다. 그래서 위와 같이 특정 방향에 따라 다음 위치를 정해주는 코드의 구조가 개판이 나 있는 것을 알 수 있습니다. 이 부분도 내일 리팩토링할 예정입니다.
정리
아론이 오늘 정말 고생을 많이 하였습니다. 그래도 체스말의 핵심 이동 전략 로직을 대부분 구현하였다는 생각이 듭니다. 내일도 힘내서 2단계 미션을 끝내고, 도메인 구조를 추상화하는 데까지는 완료했으면 좋겠습니다. 물론.. 시간이 된다면 3단계까지도 어느 정도 구현해 보고 싶군요.
'각종 후기 > 우아한테크코스' 카테고리의 다른 글
[우아한 테크코스 3기] LEVEL 1 회고 (46일차) (2) | 2021.03.19 |
---|---|
[우아한 테크코스 3기] LEVEL 1 회고 (45일차) (0) | 2021.03.18 |
[우아한 테크코스 3기] LEVEL 1 회고 (43일차) (0) | 2021.03.16 |
[우아한 테크코스 3기] LEVEL 1 회고 - 블랙잭 2단계 미션의 2차 피드백을 받아보다 (42일차) (0) | 2021.03.15 |
[우아한 테크코스 3기] LEVEL 1 회고 - 블랙잭 2단계 미션의 1차 피드백을 받아보다 (41일차) (2) | 2021.03.14 |
댓글