각종 후기/우아한테크코스

[우아한 테크코스 3기] LEVEL 1 회고 (45일차)

제이온 (Jayon) 2021. 3. 18.

안녕하세요? 제이온입니다.

 

오늘도 하루 종일 페어 프로그래밍에 모든 노력을 쏟아 부었습니다.

 

 

데일리 미팅

제가 아침에 배탈이 나는 바람에 데일리 미팅을 늦게 참석하였습니다. 파피는 마지막 글자로 끝나는 단어를 말하는 게임을 진행해 주었습니다. 6명과 7명 조로 나누어서 게임을 진행하였고, 저는 중간에 와서 어리바리를 좀 타다가 뒤늦게 적응해서 순조롭게 대답을 하였습니다. 하지만, 제가 오기 전까지 우리 조는 지고 있던 것 같았고, 결국 제가 속한 조에서 아직까지 데일리 미팅 진행 안한 사람 중 내일 진행자를 정하기로 하였습니다.

 

그런데, 삭정이 바로 하겠다고 하여서 별 다른 게임 없이 내일 데일리 미팅 진행자는 삭정으로 결정되었습니다.

 

 

페어 프로그래밍 - 1

어제까지는 중간 경로를 이용하여 각 체스말의 이동 조건을 확인하는 기능을 구현하였습니다. 오늘은 이 기능의 구조를 전반적으로 리팩토링 하였고, 룩, 비숍, 퀸에 대해 다음과 같은 코드를 추가하였습니다.

 

 

public class Rook extends Piece {
    private static final List<Direction> POSSIBLE_DIRECTIONS = Arrays.asList(Direction.EAST, Direction.WEST, Direction.SOUTH,
            Direction.NORTH);
    private static final String INITIAL_NAME = "R";

    public Rook(final boolean isBlack) {
        super(isBlack, INITIAL_NAME);
    }

    @Override
    public boolean canMove(final Position source, final Position target, final Piece piece) {
        return isPossibleDirection(source, target) && (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)));
    }
}

 

 

공교롭게도 위 코드가 폰과 빈 공간을 제외한 나머지 말들에 대해 동일하였습니다. 그래서 이들의 상위 클래스인 GeneralPiece를 만들어서 한 번 더 추상화하였습니다.

 

 

public abstract class GeneralPiece extends Piece {
    private final List<Direction> possibleDirections;

    public GeneralPiece(final boolean isBlack, final String initialName) {
        super(isBlack, initialName);
        this.possibleDirections = createPossibleDirections();
    }

    @Override
    public boolean canMove(final Position source, final Position target, final Piece piece) {
        return isPossibleDirection(source, target) && (isOpponent(piece) || piece.equals(new Blank()));
    }

    private boolean isPossibleDirection(final Position source, final Position target) {
        return possibleDirections.stream()
                .anyMatch(possibleDirection -> possibleDirection.isSameDirection(target.subtract(source)));
    }

    protected abstract List<Direction> createPossibleDirections();
}

 

 

다음과 같이 중복 코드를 해결하였습니다.

 

 

public class Rook extends GeneralPiece {
    private static final String INITIAL_NAME = "R";

    public Rook(final boolean isBlack) {
        super(isBlack, INITIAL_NAME);
    }
    
    protected List<Direction> createPossibleDirections() {
        return Arrays.asList(Direction.EAST, Direction.WEST, Direction.SOUTH, Direction.NORTH);
    }
}

 

 

그리고 폰과 빈 공간을 제외한 모든 말들이 위와 유사한 구조로 바뀌었습니다. 훨씬 깔끔해지지 않았나요? 아론과 저는 서로 감탄하면서 구조가 이뻐지는 것에 희열을 느꼈습니다.

 

오전까지는 체스말 이동에 관한 리팩토링 및 2단계 미션 입, 출력 기능을 구현해 주었습니다.

 

 

페어 프로그래밍 - 2

테코톡이 끝난 이후부터는 3단계 미션을 진행하였습니다. 3단계 미션은 점수를 출력하고 승자를 정하는 것이 관건이었습니다.

 

 

 

 

여기서 "status" 명령을 언제 실행하는지가 헷갈렸는데, 저와 아론은 게임이 끝났을 때 사용자에게 "status" 명령 실행 여부를 물어보기로 하였습니다. 여기서 게임이 끝나는 조건은 "플레이어가 end 명령 입력" 또는 "king 체스말이 죽음"이라고 볼 수 있습니다.

 

그리고 각 체스말에 따라 점수가 달랐는데, 폰만 세로줄에 같은 색의 폰이 있을 경우 1점이 아니라 개당 0.5점으로 바뀌게 됩니다. 결국, 이놈의 폰 때문에 다형성을 깨뜨리는 코드를 작성할 수 밖에 없었습니다.

 

 

    public double calculateScore(final boolean isBlack) {
        double total = 0;
        for (final Horizontal column : Horizontal.values()) {
            total += getColumnTotalScore(isBlack, column.getValue());
        }
        return total;
    }

    private double getColumnTotalScore(final boolean isBlack, final int column) {
        final List<Piece> pieces = chessBoard.keySet().stream()
                .filter(position -> position.getHorizontal().getValue() == column)
                .map(chessBoard::get)
                .filter(piece -> piece.isSameTeam(isBlack))
                .collect(Collectors.toList());

        return pieces.stream()
                .mapToDouble(Piece::getScore)
                .reduce(0, Double::sum) - getPawnDiscountScore(pieces);
    }

    private double getPawnDiscountScore(final List<Piece> pieces) {
        long count = pieces.stream()
                .filter(piece -> piece instanceof Pawn)
                .count();

        if (count >= 2) {
            return count * Pawn.EXTRA_SCORE;
        }
        return 0;
    }

 

 

우선, 특정 세로 줄에서 화이트 또는 블랙에 해당하는 말들을 추출합니다. 그리고 그 말들의 점수를 일단 다 더합니다. 이때, 폰이 2개 이상이라면 그 개수만큼 0.5씩 추가로 뺍니다. 이 과정에서 특정 체스 말이 폰인지 확인해야하는 instanceof 코드가 발생하였고, 폰의 0.5점 상수도 직접 접근해야하는 문제가 발생하였습니다. 이 부분은 내일 중점적으로 리팩토링할 예정입니다.

 

 

다음으로, 왕이 죽었는지 판단하는 것은 어렵지 않았으나 각자 턴을 번갈아 가면서 로직을 짜는 것이 어려웠습니다. 예를 들어, 처음 화이트가 시작하였으면 그 다음에는 블랙, 그 다음에는 화이트, ... 와 같은 방식입니다. 저는 화이트나 블랙을 단순히 boolean 타입으로 작성하였기 때문에 어쩔 수 없이 컨트롤러의 필드에 static 변수를 만들 수 밖에 없었습니다. 그리고 턴이 끝날 때마다 그 boolean 변수를 반전시켜주었습니다. 이 부분은 enum 클래스를 통해서 리팩토링할 예정입니다.

 

 

정리

후반에는 집중력이 떨어졌지만, 어떻게든 모든 기능을 구현한 것에 만족합니다. 아론과 상당히 잘 맞아서 다행이었고, 협업하는 내내 힘들었지만 뿌듯했습니다. 내일은 마지막 스퍼트로 열심히 리팩토링해서 서로가 만족하는 구조가 되었으면 좋겠습니다.

댓글

추천 글