조합으로 체스 말 이동 범위 검사하기!

요약

체스 말 객체들이 해당 위치로 이동할 수 있는지 검증하는 로직이 중복된 경우가 있었다.
이를 따로 분리해서 중복을 제거하고자 했다.

도입 배경

체스 말 종류 구현하기

체스 말 객체(Piece)는 자신의 위치와 이동할 위치를 전달받아서 이동할 위치로 이동할 수 있는지 확인한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class Piece {

private final Color color;
private MovableStrategy strategy;

public Piece(final Color color, final MovableStrategy strategy) {
this.color = color;
this.strategy = strategy;
}

public abstract double getPointValue();

public boolean movable(Square source, Square target, MoveType moveType) {
if (source.equals(target)) {
throw new IllegalArgumentException("같은 위치로는 이동 불가능합니다.");
}
return this.strategy.movable(source, target, moveType);
}

//...생략
}

체스 말 객체는 여러가지 종류가 있다. (퀸, 킹, 폰, 비숍, 룩, 나이트)
Piece를 상속받아서 다양한 종류를 구현한다.

1
public final class King extends Piece

체스 말 종류에 따라 달라지는 이동 범위

체스 말 객체는 종류에 따라 이동 범위가 다르다.

문제는 각 클래스마다 이동 검증 로직이 중복되는 게 많다는 점이다!

중복되는 코드

퀸 기물의 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class Queen extends Piece {

private static final List<Direction> QUEEN_DIRECTION
= List.of(EAST, WEST, SOUTH, NORTH, SOUTHEAST, NORTHEAST, SOUTHWEST, NORTHWEST);

public Queen(Color color) {
super(color);
}

@Override
public boolean movable(Square source, Square target, MoveType moveType) {
Direction direction = source.findDirection(target);
return QUEEN_DIRECTION.contains(direction);
}
}

반면 이건 룩 기물의 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class Bishop extends Piece{

public static final List<Direction> BISHOP_DIRECTION = List.of(SOUTHEAST, NORTHEAST, SOUTHWEST, NORTHWEST);

public Bishop(Color color) {
super(color);
}

@Override
public boolean movable(Square source, Square target, MoveType moveType) {
Direction direction = source.findDirection(target);
return BISHOP_DIRECTION.contains(direction);
}
}

여기서 잘 보면 movable 메서드 안에 로직이 중복됨을 알 수 있다. 이를 해결해보자.

조합으로 중복 제거하기

이때 체스 말 객체를 두가지로 나눠서 생각한다.

이동하는 거리가 제한 없는 경우 VS 이동하는 거리가 제한 있는 경우

전자는 퀸, 비숍, 룩이 해당한다.
후자는 폰, 킹, 나이트가 해당한다.

그렇다면 거리 제한 없이 이동하는 전략과 거리 제한이 있는 전략을 구분해서 추상화할 수 있지 않을까?

인터페이스로 전략을 추상화해서 만들기

모든 종류의 체스 말들이 자신의 이동여부를 확인할 수 있도록하는 인터페이스를 만들어보자.

1
2
3
4
public interface MovableStrategy {
boolean movable(Square source, Square target, MoveType moveType);
}

자신의 위치, 목표 위치, 움직임이 공격인지를 전달받아 해당 목표로 갈 수 있는지 판단한다.

이동하는 거리가 제한 없는 경우

인터페이스를 구현해서 먼저 이동하는 거리가 제한이 없는 경우의 전략을 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UnlimitedMovableStrategy implements MovableStrategy {

private final List<Direction> movableDirections;

public UnlimitedMovableStrategy(List<Direction> movableDirections) {
this.movableDirections = movableDirections;
}

public boolean movable(Square source, Square target, MoveType moveType) {
try {
Direction direction = source.findDirection(target);
return movableDirections.contains(direction);
} catch (IllegalArgumentException e) {
return false;
}
}
}

이동하는 거리가 제한이 없는 전략은 가려는 목표지점의 방향이 내가 갈 수 있는 방향인지만 검사하면 된다.

즉 룩의 경우 상하좌우로 거리제한없이 이동할 수 있다.
룩이 가려는 목표 지점이 상하좌우 방향 중 하나에 있다면, 룩은 그 목표로 갈 수 있다.
(이동 경로에 다른 누군가 있는 경우나 목표 지점이 같은 편이 있는 경우는 다른 곳에서 검증한다.)

이동하는 거리가 제한된 경우

이동하는 거리가 제한된 경우의 전략을 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LimitedMovableStrategy implements MovableStrategy {
private final List<Direction> movableDirections;
private final int moveLimit;

public LimitedMovableStrategy(List<Direction> movableDirections, int moveLimit) {
this.movableDirections = movableDirections;
this.moveLimit = moveLimit;
}

@Override
public boolean movable(Square source, Square target, MoveType moveType) {
try {
Distance distance = source.getDistance(target);
Direction direction = source.findDirection(target);
return movableDirections.contains(direction) && distance.isInRange(moveLimit);
} catch (IllegalArgumentException e) {
return false;
}
}
}

이동하려는 거리 제한이 있으면 두 위치간에 거리를 추가로 확인해주면 된다.

폰의 이동 전략

폰의 이동 전략은 꽤 까다롭다.

첫 위치에서 움직이는 경우 전진을 두칸까지 할 수 있다.
공격은 반드시 전진 방향 대각선으로 할 수 있다.

이를 위해서는 MoveType에 따라 다른 전략을 만들어서 실행해줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class PawnMovableStrategy implements MovableStrategy {
private static final int FIRST_MOVE_LIMIT = 2;
private static final int NOT_FIRST_MOVE_LIMIT = 1;
private static final int ATTACK_LIMIT = 1;

private final List<Direction> nonAttackDirection;
private final List<Direction> attackDirection;
private final Rank startRank;

public PawnMovableStrategy(List<Direction> nonAttackDirection, List<Direction> attackDirection, Rank startRank) {
this.nonAttackDirection = nonAttackDirection;
this.attackDirection = attackDirection;
this.startRank = startRank;
}

@Override
public boolean movable(Square source, Square target, MoveType moveType) {
if (moveType.isAttack()) {
return new LimitedMovableStrategy(attackDirection, ATTACK_LIMIT).movable(source, target, moveType);
}
return nonAttackMovable(source, target);
}

private boolean nonAttackMovable(Square source, Square target) {
if (source.isSameRank(startRank)) {
return new LimitedMovableStrategy(nonAttackDirection, FIRST_MOVE_LIMIT).movable(source, target,
MoveType.MOVE);
}
return new LimitedMovableStrategy(nonAttackDirection, NOT_FIRST_MOVE_LIMIT).movable(source, target,
MoveType.MOVE);
}
}

폰의 이동전략을 수행하기 위해서는 자신의 첫 위치 시점(starkRank), 공격가능 방향(attckDirection), 이동 가능 방향(nonAttackDirection)을 주입받는다.

현재 이동하려는 것이 공격이면 공격에 맞는 방향과 거리로 검증한다.
반면 그냥 이동하려는 경우는 이동에 맞는 방향과 거리로 검증한다.

Share