체비쇼프 거리

시야, AOE를 체비쇼프 거리라고 하네요.

시야를 구하는 함수

영역을 구해서 칸(배열)을 반환하는 것은 비효율적

  • 시야 영역을 구해도, 어차피 이동/공격에서 시야 배열을 순회하면서 탐색해야 하니까…

시야의 유닛을 모두 반환하는게 좋을까?

  • 순회하면서 유닛을 찾겠다는 의도
    • 누가 찾아서 사용할건데?
    • 누군가는 유닛의 목록을 알 고 있으면 유닛을 찾을 필요가 없는데?
      • SimulationManager?

유닛 목록을 누가 관리하는가?

시뮬레이션 매니저 코드는 직접 구현해야 하고 public 매소드들이 쭉 선언되어 있음

  • 이것 만으로 정보는 부족하니까, 사용처를 확인해보자

3. 본인 컴퓨터에서 테스트하는 법 코드를 확인해보면

  • 직접 유닛을 생성하고 spawn함수를 통해 등록하는 개념
  • 비쥬얼라이저는 simulationManager.getUnits()로 얻은 유닛 목록을 이용해 시각화함

2.10 SimulationManager 클래스를 구현한다 명세를 확인해보면 SimulationManager는 유닛 목록을 관리해야함

  • 생사 여부

따라서 유닛 목록을 알 고 있는 누군가는 SimulationManager로 확정!

  • 사고를 전환
    • 맵의 유닛을 찾음 X
    • 유닛 목록을 전지적으로 다 알고 있음 O

시야를 구하는 함수의 시그니처 유닛을 볼 수 있는지 확인하는 함수의 시그니처

외부에서 SimulationManager가 유닛 목록을 관리하고 있으니, 함수의 매개변수에 유닛을 넣으면 매니저가 넣어주면 됨

어떤 유닛의 함수에 유닛을 넘겨서 유닛이 가진 구체적인 상태(스펙)에 따라 target.canSee(other)을 했을 때 “target”이 “other”을 볼 수 있는지 구할 수 있음

매개변수:

  • 유닛 반환값:
  • boolean

“유닛을 볼 수 있는지 구하는 함수”를 구현한다고 결론!

  • 이는 공통 로직이니까 추상 클래스에 선언하면 되겠죠?

유닛을 볼 수 있는 지 판단하는 함수 구현 방법에는 크게 2가지

  1. 부모(추상) 클래스에 상태를 정의하고, 이를 생성자로 값을 넘기는 방법
  2. 템플릿 메서드 패턴

유닛을 볼 수 있는지 확인하는 함수의 시그니처 구현

1. 추상 클래스에 상태를 정의하는 구현

부모(추상) 클래스에 정의된 상태

  • 시야
    • 범위 로직에 값을 사용함
  • 공중을 볼 수 있는가?
  • 지상을 볼 수 있는가?

위 데이터값을 자식 클래스 생성자의 매개변수로 받아, 상태로 가짐

부모 클래스에 시야의 적 목록을 반환하는 함수를 구현하고, 자식 클래스 생성자로 초기화된 값을 이용해 판별에 활용 할 수 있음

// UnitType.java - 최상위 파일로! (규칙 8: 내포 열거형 금지)
public enum UnitType {
    GROUND, AIR
}
 
// Unit.java
public abstract class Unit {
    private final int vision;
    private final UnitType unitType;      // 나는 지상인가 공중인가
    private final boolean canSeeGround;   // 내가 볼 수 있는 것
    private final boolean canSeeAir;
    private final boolean isVisible;      // 남이 나를 볼 수 있는가 (지뢰만 false)
 
    protected Unit(IntVector2D position, int hp, int vision,
                   UnitType unitType, boolean canSeeGround, boolean canSeeAir,
                   boolean isVisible) { ... }
 
    protected final boolean canSee(Unit other) {
        if (!other.isVisible) {
            return false;
        }
        boolean typeOk = (other.unitType == UnitType.GROUND && this.canSeeGround)
                || (other.unitType == UnitType.AIR && this.canSeeAir);
        if (!typeOk) {
            return false;
        }
        
        // 체비쇼프 거리 구하는 공식
        int dx = Math.abs(...);
        int dy = Math.abs(...);
        return Math.max(dx, dy) <= this.vision;
    }
}

2. 템플릿 메서드 패턴

public abstract class Unit {
    protected abstract boolean canSeeGround();
    protected abstract boolean canSeeAir();
 
    protected final boolean canSee(Unit other) {  // 부모의 공통 로직
        boolean typeOk =
                (other.unitType == UnitType.GROUND && this.canSeeGround())
                || (other.unitType == UnitType.AIR && this.canSeeAir());
        // ... 거리 판정
    }
}
 
public class Tank extends Unit {
    @Override
    protected boolean canSeeGround() {
        return true;
    }
 
    @Override
    protected boolean canSeeAir() {
        return false;
    }
}

템플릿 메서드 패턴으로 선택

공격하는 함수 생각하는 함수

공격하는 함수 전 공격이라는 동작을 결정하는 “생각”이라는 기능이 필요함

“생각”(행동 결정)과 “실행”은 분리되어 있음

  • A.2 시뮬레이션 규칙 참고
    • 각 유닛의 이동 순서가 달라져도 시뮬레이션 결과는 동일함
    • 각 유닛의 공격 순서가 달려저도 시뮬레이션 결과는 동일함
  • 분리해야 각 유닛이 동일한 상황(스냅샷)을 보고 실행할 동작이 결정나니까, 실행 간의 순서가 바뀌어도 문제가 없음
  • 만약 분리되지 않아서 먼저 실행한 유닛에 따라 상황(스냅샷)이 바뀌진 않음

따라서 생각하는 함수 기능을 먼저 구현해

  • 공격 생각
  • 이동 생각
  • 아무것도 안 함 생각

생각하는 함수

2.10 SimulationManager 클래스를 구현한다 를 보면 registerThinkable 함수가 있음

  • 일단 접미사가 “~able” 이니 인터페이스로 구현해볼까?
    • 어떤 유닛은 생각, 어떤 유닛은 움직일 수 있고, 등등
    • registerThinkable 함수의 매개변수 타입을 IThinkable

IThinkable 에 생각하는 함수의 시그니처를 작성해보자

반환값은 enum으로 하면 되겠지?

  • SimulationManager.update 내부에서 사용하기 편하잖아?
    • 이것도 개체 스스로 자신의 상태를 책임지게 하려면 마린의 이동하는 함수에서 언급한 것 처럼 반환하지 않고, 멤버 변수로 상태를 기억

매개변수로는 유닛 목록을 받으면 판단할 수 있잖아?

  • 시뮬레이터가 유닛 목록을 관리하니 넣어주면 되고

마린의 생각하는 함수

마린의 생각하는 함수에 어떤 식으로 구현할까?

  • A.2 시뮬레이션 규칙
    • A.2.9 에서 공격을 먼저 판단해야 함을 알 수있음
  • 공격
    • 시야에서 발견한 적을 공격
    • 공격 위치 정해서 상태로 기록
      • A.2 시뮬레이션 규칙의 A.2.13 참고
        • 교전/이동 규칙의 각 단계를 실행한 후에도 적이 한 명으로 특정되지 않고 여럿이 남아있다면 남은 적들에 대해 다음 단계들을 실행
  • 이동
    • 시야에서 발견했지만, 공격 구역이 아닐 때
    • 이동 위치 정해서 상태로 기록
      • A.2.13 규칙 그대로 적용
  • 아무 행동 X
    • 시야에 적이 없는 경우

마린의 공격 생각

필터링 기반으로 구현

공격 규칙은 아래와 같음

  1. 공격 가능 지역으로 필터링
    • 자기 자신은 제외
    • 볼 수 있어야함
  2. HP가 가장 낮은 유닛으로 필터링
  3. 시계 방향 점수로 필터링
    • 공격 가능 지역은 하위 유닛에 “vector:점수” 룩업 테이블을 이용함
      • 이 룩업 테이블은 “1.공격 가능 지역으로 필터링”에서도 재활용 해되 되고, 테이블 분리해도 되고

공격 생각 구현에서 주의할 것

  • 공격 함수는 Unit에 정의되어 있으니, 공격할 위치만 특정하면 됨
  • 공격 구역에 적을 발견하면, 반드시 공격해야함
    • 공격 구역에 적이 있으면 이동으로 넘어가지 않음
  • 공격 생각에서 각 규칙에서 한 명이 특정되면 그 규칙으로 공격 위치 선정
    • 마지막 규칙에서는 여러 명이 특정되도 괜찮음
    • 여러 명이 특정되도 공격 지점은 하나로 정해짐
      • 스펙이 그렇게 되어있음

타일에 유닛이 겹쳐 있을 수 있음!!

  • [[pocu-note/COMP2500/901-assignmnet/901-003-assignment-3/spec#A.3.1 해병(Marine, 마린)|#A.3.1 해병(Marine, 마린)]] 에서 해병은 자신 위치의 유닛을 공격한다고 명시
  • c3. 본인 컴퓨터에서 테스트하는 법 에서 “한 타일 안에 여러 유닛이 있을 경우”가 명시되어 있음
  • A.2 시뮬레이션 규칙에서 A.2.13 “어떤 단계를 실행 후, 적이 하나로 특정되었다면”에서 “하나로 특정”은 필터링 후 유닛이 하나 남는게 아니라, 타일(위치)가 하나 남는 것으로 해석
    • A.3.1 해병에서 해병은 “타일”을 공격한다고 명시
    • A.2.10 시뮬레이션 규칙 “유닛이 어떤 타일을 공격하면, 그 타일 안에 있는 다른 모든 유닛이 공격을 받습니다.”

이동하는 함수

2.10 SimulationManager 클래스를 구현한다 를 보면 registerMovable 함수가 있음

  • IThinkable 처럼 움직이는 함수를 구현하자

반환값은 Int2DVector로 위치 반환하면 될 것 같은데?

  • 아니다 반환값이 없어도 되려나?
    • 없어도됨
  • 어디로 갈 지 판단하는 함수와 진짜 움직이는 함수를 분리되어 있으니
    • 진짜 움직일 때는 자신의 위치를 바꿔야겠음
      • Unit에 위치 설정하는 함수를 추가해도 되겠지?
        • “2.1 전반적인 규칙” 참고 매개변수는 Think처럼 유닛 목록을 받으면 되지 않을까?
  • 유닛 목록이 필요없으면 받아서 안 쓰면 되고..
  • 유닛 목록이 필요없네요, 생각할 때만 유닛 목록이 필요하고

마린의 이동 생각

A.2.6

공격처럼 A.2.13 그대로 적용

  • 스냅샷 기반 이동 위치 정하기

이동 생각 알고리듬은 다음과 같음

  1. 볼 수 있는 유닛 확인
    • [[pocu-note/COMP2500/901-assignmnet/901-003-assignment-3/spec#A.3.1 해병(Marine, 마린)|#A.3.1 해병(Marine, 마린)]] 참고, 해병은 볼 수 있는 유닛이 없으면 움직이지 않음
    • Unit에서 상태 vision 값을 기반으로 canSee를 이미 구현했음
  2. 거리로 필터링
  3. HP가 가장 낮은 유닛으로 필터링
  4. 시계 방향 점수로 필터링
    • 방법이 여러 가지
      • vision 값에 따라 동일한 멘하튼 거리의 diff vector 만들기
        • “멘하튼 거리: vector 목록”
      • 4번째 필터까지 왔다는 것은, 멘하튼 거리가 동일한 것들에서 필터링

이동 생각도 마린의 공격 생각에서 본 것 처럼 마지막 규칙에서 여러 명이 특정되도 이동 지점은 하나로 특정됨

  • 그래서 마지막 규칙 적용하는 함수는 findxxxPosition 처럼 포지션을 찾는 다는 함수 이름을 사용하자

멘하튼 거리가 동일한 집합에서 순서 정하기

1. 링의 모양부터 결정 맨해튼 거리가 d로 같은 점들의 집합 {(dx,dy) : |dx|+|dy| = d}은 45° 돌아간 정사각형(다이아몬드)이

  • 꼭짓점이 N(0,-d), E(d,0), S(0,d), W(-d,0) 네 개,
  • 변이 네 개
  • 격자점은 총 4d개

2. “북쪽부터 시계방향 검색”을 기하로 번역 북쪽 꼭짓점에서 출발해 둘레를 시계방향으로 한 칸씩 걸을 때 몇 걸음째에 그 점을 만나는가

  • 그 걸음 수가 곧 인덱스
    • 참고로 곡선을 시작점부터의 이동 거리로 매개화하는 건 호길이 매개화(arc-length parametrization)라는 표준 기법

3. 각 변 위에서 걸음 수를 좌표로 표현 N→E 변을 걸어보면 d=2일 때 (0,-2) → (1,-1) → (2,0). 한 걸음마다 dx가 정확히 1씩 증가

  • 이 변 위에서는 걸음 수 = dx
    • 이를 일반화해 변마다 어떤 좌표가 걸음당 1씩 단조 변화하는지만 찾기 E→S 변을 걸어보면 d=2일 때 (2,0) -> (1,1) -> (0,2). 한 걸음마다 dy가 정확히 1씩 증가
  • 이 변 위에서는 걸음 수 = dy
  • 그리고 지금까지 누적 걸음을 더해줘서 앞의 변보다 이 변에서 걸음이 뒤에 있도록

이를 일반화 하면 아래 표처럼 됨

걸음마다변 위에서 걸음 수변 시작까지 누적 걸음index
N→Edx +1, dy +1dx0dx
E→Sdx −1, dy +1dydd + dy
S→Wdx −1, dy −1−dx2d2d − dx
W→Ndx +1, dy −1−dy3d3d − dy

NORTH & EAST 변

  • 상대가 나보다 오른쪽에 있으니 dx > 0
    • “상대 위치 - 내 위치”로 변위를 구하잖아
  • 상대가 나보다 위에 있으니 dy < 0
  • 북쪽 꼭지점을 포함하니 dx = 0

NORTH & WEST 변

  • 상대가 나보다 왼쪽에 있으니 dx < 0
  • 상대가 나보다 위쪽에 있으니 dy < 0
  • 서쪽 꼭지점을 포함하니 dy = 0

4. 꼭짓점 소속

  • 꼭짓점은 두 변에 걸쳐 있으니 한 변에만 소속시켜야 함
  • 각 꼭짓점을 자기가 시작하는 변에 배정
    • 끝나는 변에 해도 상관없음
    • 반열린 구간 [시작, 끝) 처리
    • 이렇게 해야 네 조건이 서로 배타적이면서 링 전체를 덮을 수 있음
    • off-by-one 함정에 속함

참고자료

  • 택시 기하학(Taxicab geometry) — “맨해튼 원 = 다이아몬드”라는 출발점. Wikipedia 항목이 잘 정리돼 있어요.
  • 각도 정렬(polar/angular sort) — 이 공식의 일반형. 북쪽 기준 시계방향 각도 atan2(dx, -dy)로 정렬해도 같은 순서가 나와요. 제 공식은 그 각도 정렬을 다이아몬드 링에 특화해서 부동소수점 없이 정수로 만든 버전이라고 보면 정확해요.
  • 나선형 행렬 순회(spiral matrix traversal) — 정사각(체비쇼프) 링의 둘레를 인덱싱하는 문제로, 완전히 같은 “변별로 나눠서 누적 오프셋 + 단조 좌표” 유도를 써요. LeetCode 54번 같은 문제 풀이에서 이 사고방식을 볼 수 있어요.

IntVector2D의 초기값?

멤버 변수에 final을 사용해 참조를 고정하고, 초기값을 유효하지 않은 값(x,y)를 넣을까?

  • 과제 명세에 setter가 존재하니…
  • 값을 대입할 때는 매번 새로운 개체를 생성하는게 아니라, 기존 참조의 x,y,를 바꿈
    • 내가 소유한 객체는 내가 바꿀 수 있음
  • 값을 반환할 때는 기존의 참조를 지키기 위해 복사해서 반환
    • 내가 소유하지 않을 것이라 새로 생성해서 반환

IReadOnlyIntVector2D를 만들고, 여기에 setter 빼고 정의함 IntVector2D는 setter을 가지고 있음

  • 과제 명세에도 위배되지 않음

소유권을 가지는 곳에서는 그냥, 소유권이 없는 곳은 ReadOnly로 참조형에 대해서 좋은 패턴이다

  • 깊게 복사될 수 있는 여지가 있는 것들은 이렇게 제한하자.
    • 방어적 복사랑 연관됨
    • 자바는 읽기전용 리스트 만드는 깔끔한 방법이 없넹..?

요약하자면 매개변수로 받을 때는 읽기 전용으로 받고 함수 내부에서 소유권 관리 반환할 때는 소유권을 넘기니까 읽기 전용으로 반환할 필요 없음

  • 빌림으로 받고, 소유로 반환 (Rust 모델)

마린의 이동하는 함수

이동할 위치는 정해졌고

A.3.1 보면 “이동할 때는 언제나 y축을 따라 다 이동한 뒤 x축을 따라 이동합니다.”

이동은 직접 위치를 바꾸면 되죠?

  • 적절한 위치인지도 마린 자체가 판단하는게
    • 공격 규칙에서 적절한 위치인지 판단해서, 그 결과에 따라 이동으로 넘어갈지 안 넘어갈지가 영향을 받음
    • 이동 규칙에서 절절한 위치인지 판단해서, 그 결과에 따라 아무것도 안 할지 영향을 받음
      • 근데 마린은 유닛을 따라가니까 적절한 위치인지 판단할 필요가 없음
      • 애초에 유효한 위치의 유닛목록만 프레임에서 다룬다고 가정하면 되요

어쨌건 적절한 위치로 이동하는 것은 각 유닛이 판단하는게 맞음

MoveActionType에 따른 가드가 필요함

  • Move를 어떻게 호출할 것인가?
    • Think에 따라 이번 프레임에 이동이 결정난 것들만 이동해야함
    • Move 내부에서 여기에 대한 필터링을 수행하는게 좋음
      • 이렇게 안 하면 SimulationManager에서 Movable 목록에서 진짜 움직여야 하는 애들을 움직이거나..
        • 이것도 괜찮은게 Think에서 어떤 행동을 해야하는지 반환하고 이걸 기반으로 사용하면 되잖아?
        • 개체 상태를 스스로 책임지는 관점에서는 반환하지 않고, 자기가 상태를 가지는게 좋아보이긴 함..?

참고로 Unitattack이 결국 AttackIntent로 시뮬레이터가 무엇인가 할 수 있게 반환하지만 null 도 반환할 수 있음

  • 결국 공격할지 말지는 개체 스스로가 결정하는 개념이야
    • 이 결정이 think
    • “공격할 기회”만 받을 뿐
    • 이동도 유사하게 “이동할 기회”만 받을 뿐 think로 결정된 상태에 따라 이동하겠지
      • 그래서 가드가 필요함

마린의 공격하는 함수

Unit 클래스에 attack()이 있음

  • 시그니처 바꿀 수 있나?
    • 2.1.1 에서 불가능하다고 나와요
  • 추상 메서드로 변경은 시그니처 변경에 해당되는가?
    • 아님
    • 보통 시그니처는 함수명 그리고 매개변수 목록, 반환형 까지

상태로 저장한 targetPosition위치에 공격함 AttackIntent 에 다음 정보를 포함하면?

  • 위치
    • AOE 계산 때문에
  • 피해치
  • 공격자
    • 자기위치 때리더라도 자신은 피해입으면 안 됨
    • 이건 여기가 아니라 OnAttacked에서 처리하는?
    • 그래서 공격자를 포함하는 구나
  • AOE 값

사실상 정보 반환이 전부네

피해량 계산은 OnAttakced에서 처리 이러면 추상 메서드로 구현할 이유가 없네?

  • 반환하는 기능을 부모에 구현하면 됨

마린의 이동하는 함수에서 봤듯이 호출자(시뮬레이터)는 마린의 상태를 모름

  • 자신이 공격 가능한 상태인지 자신이 책임지기
  • think로 결정된 상태에 따라 가드

마린의 피해를 입히는 함수

2.10 참고

  • update() 내부에서 피해를 입히는 함수를 호출함
  • Unit이라는 상위 클래스에 onAttacked의 시그니처를 유지하면서 구현
    • 부모 클래스의 시그니처를 유지해야 late binding으로 호출 할 수 있음

마린은 피해 입을 때 특별한 규칙 없음

  • onAttacked시그니처를 보니까, 데미지 계산같은건 시뮬레이션 매니저에서 끝내고 damage를 넘김
    • AOE 계산을 시뮬레이션 매니저에서 수행하고

다른 유닛의 피해를 입을 때 특별한 규칙이 있는가?

  • 전차는 공성 모드일 때 피해 2배
  • 망령은 특수 방어막
    • 특수 방어막 상태는 자기가 관리하면 되겠지? 시뮬레이션 매니저는 방어막 여부는 관계없이 onAttakced호출하고
      • attack, move에서 자신의 상태를 관리하는 것과 유사
        • 개체지향
  • 포탑도 없고
  • 지뢰도 없고
  • 스마트 지뢰도 없고
  • 파괴자는 있네

마린의 공격하는 함수에서 본 attack의 구현과 다르게 이번에는 추상 메서드화 하고 세부 구현을 해도 되고?

  • Unit 에 구체적인 매소드를 그대로 두되, 오버라이딩 해도 되고

마린은 특별한 규칙이 없으니 Unit을 그대로 사용하자

시뮬레이터 구현

getUnits 는 살아있는 유닛 목록 반환하면 됨

  • A.1 “HP가 0이 된 유닛은 죽습니다” 참고
  • 시뮬레이터는 살았는지 죽었는지 확인하는 함수를 호출해야하니 만들어주죠
    • Unit에 구현
    • 근데 이미 getHp가 stub으로 있네?
      • hp 값으로 판단해보자

우선 유닛을 어떻게 상태로 저장할 것인가?

생각해보니 반환할 때 필터링도 필요없네

  • 2.10 보면 update내부에서 죽은 유닛들을 모두 게임에서 제거하니까
    • 유닛 목록 자체가 살아있는 유닛만 가짐
  • 반환할 때 새로 리스트 만들어야하나??
    • 깊은복사 할 필요가 없는데?
      • 2.1.11 “1. 유닛을 고유하게 식별하는 기준은 그 개체의 참조(레퍼런스)입니다. 따라서 유닛을 깊게 복사하지 마세요.”

spawn도 논리가 단순하네. 그냥 유닛 추가하면 끝임

registerThinkable, registerMovable 같은 등록 류는?

  • 별도로 목록을 관리해야지
    • 왜?
    • update 내부에서 생각할 수 있는 유닛에게만 지시, 움직일 수 있는 유닛에게만 지시, 충돌할 수 있는 유닛만 지시..
  • 별도로 관리하는 만큼, 유닛 추가/제거 시 어떻게 하지?
    • 시뮬레이터는 타입을 모르고, 개체는 안다?
      • 2.2 “onSpawn(): 유닛이 월드에 추가될 때(spawn) SimulationManager가 이 메서드를 호출해야 합니다.”
      • 개체가 스폰할 때 onSpawn을 호출하고, 여기서 등록하는 함수를 호출하는거구나
  • 그리고 매개변수의 타입도 변경해야함
    • “1. 프로젝트를 준비한다”에서 주어진 stub에서 매개변수의 타입은 Unit
    • 2.1 “단, 다음 메서드들의 매개변수 자료형(param type)은 바꿔도 됩니다.”에서 힌트를 얻을 수 있음

SimulationManager에서 spawn할 때 onSpawn을 강제하는 방법은 없을까?

없음 함수 body까지 강제할 방법은 없음

  • 다형성은 함수 시그니처만..

시뮬레이터 update 구현

AttackIntent 상태가 유효하지 않은 것을 표현해야 함

  • 주어진 Unit.java 의 attack 시그니처는 고정
  • null 반환하기는 싫음
    • null 반환하면 이름을 attackOrNull 이렇게 변경해야 하는데 시그니처 변경이 불가능하니
    • 꼼수로 유효하지 않은 AttackIntent를 표현하도록 매개변수 없는 생성자를 만듦

유닛 제거할 때 foreach 문에서 루프 중에 바로 제거하면 안 됨

  • Concurrent Modification Exception

모든 AttackIntent에 대해서 어떤 유닛에 줄 수 있는 데미지 총합을 구해서 유닛의 onAttack을 한 번만 호출했는데

  • 근데 onAttack을 공격에 대해서 한 번씩 호출하는 것이 개념적으로 맞지?
    • 총 데미지를 구해서 한 번 때리는게 아니라
    • 각각이 공격이니까 각각 공격에 대해서 onAttack 호출하는거지
    • 우연히 망령의 경우 한 프레임동안 특수 방어막이 발동되는 개념이니까 누적해도 OK
    • 다른 방어 기능 없는 유닛도 마찬가지
    • 디스트로이어는 1 * 공격횟수 만큼 한 프레임에 데미지 입어야겠죠..?

개념적으로 누적 데미지 방식이 실수하기 좋군

  • AttackIntent damage 계산 onAttack 호출
    • 중간에 damage 계산에서 0이면 onAttack 호출하지 않기
    • 0데미지 짜리 onAttack은 실수 유발, 괜히 레이스 방어막만 깨진다.

마린 기능 테스트하기

case 1 A(0,0) B(2,2) 에 마린 배치하기

  • frame1
    • A(0,1) 이동
    • B(2,1) 이동
  • frame 2
    • A(1,1)이동
    • B(1,1)이동
  • frame 3
    • 둘다 6씩 피 감소
  • frame 9
    • 둘다 죽음

case 2 0(4,4) 1(4,2) 2(6,4)

  • frame1
    • 0(4,3) 1을 찾아서 북쪽으로 이동
    • 1(4,3) 2를 찾아서 남쪽으로 이동
    • 2(5,4) 0를 찾아서 서쪽으로 이동
  • frame2
    • 0,1 서로 전투
    • 2 같은 위치에 있는 0,1 찾아서 북쪽으로 이동
  • frame3
    • 0 자기 위치 공격
    • 1 자기 위치 공격
    • 2 (4,3) 공격
      • 0,1가 12씩 깎여야함
  • frame(5)
    • 0,1 동시 사망
Frame: 5
 
0(M)XX 1(M)XX 2(M)35 
 
  0123456789ABCDEF
 +----------------+
0|                |
1|                |
2|                |
3|     2          |
4|                |
5|                |
6|                |
7|                |
 +----------------+

탱크 생각

A.3.2 “차는 모드를 바꿀 때 1 프레임을 소모합니다.”

  • UnitAction에 “전환”이 필요하겠는데?

“전차가 시야 안에서 적을 찾으면 공성 모드로 변환하여 공격할 준비를 합니다.”

  • 시야에 적이 있으면, 공성 모드로 전환해서 기다리는 개념이네요
    • UnitAction에 “대기”가 필요하겠는데?

시야에 적이 있는지 확인

  • (1) 적이 있으면
    • (2) 모드 전환 필요성 확인
      • 모드 전환해야하면 시즈모드하고 다음 턴
      • 이미 시즈모드면 공격 위치 찾기 (findAttackPosition)
  • 적이 없으면
    • 모드 전환 필요성 확인
      • 모드 전환해야하면 탱크모드하고 다음 턴
      • 이미 탱크모드면 이동 (findMovePosition)

전환하는 도중에 맞으면 어떻게 되지?

  • 2.10 보면
    • “이동할 기회를 줌” “공격할 기회를 줌”
    • 이동 전에 모드 전환이 결정나니까, 모드가 바뀌고나서 피해를 받는게 맞는 것 같은데
      • 뭔가 다른 방법은 애매한데?
      • 이번 턴에 시즈모드로 전환하면, 전환한 시즈모드 기준 피해량 받는거지
        1. 본인 컴퓨터에서 테스트하는 법 참고하면
        • Frame 0에서 탱크랑, 마린이랑 겹처서 생성됨
          • “3-4” 확인
          • 탱크랑 마린 겹치면 탱크는 자기 위치 공격 불가능, 시야 안에서 적을 찾음
            • 그러면 탱크의 초기 모드는 무엇인가?
              • FRAME 1에서 탱크는 공격을 받음, 12가 줄었네?
                • 6 * 2? 마린이 자기 위치 공격했고
                • 그러면 초기는 시즈모드여?
              • 만약에 탱크가 시즈모드 였다면, 시즈모드 그대로 유지
                • 시야에 적이 있으니까
              • 만약에 탱크가 탱크모드 였다면, 시즈모드로 변환
                • 시야에 적이 있으니까
              • Frame 0 Frame1에서 탱크한태 공격받은 놈이 있는지 볼까?
                • u1(0,5), u3(2,4)니까 공격 가능하거든?
                  • 어 마린 피가 안 닳았네?
                  • 탱크는 (0,5) 공격하면, u1이 (0,4)로 이동하고도 AOE로 데미지 받아야하는데?
                    • 아 그러면 시작은 탱크모드로 시작하고, 전환한다고 안 때림
    • 결론은 탱크는 시작이 탱크모드, 전환하면 전환 후 상태로 데미지 받음
    • Frame 1 Frame 2에서 마린이 4데미지 받았는데
      • 탱크가 (0,4)때렸구 마린이 (1,4)로 이동해서
        • 8 * (1 - 1 / (1 + 1)) = 4
        • 검증 완료

마린과 탱크의 공격 로직에서 공통점 뽑기

마린

  • 공격 지역에 적이 있는지 필터링
    • 자신 제외
  • 공격 지역에 있는 적 중에서 가장 약한 유닛 필터링
  • 가장 약한 유닛 중에서 시계 방향으로 탐색
    • 시계 방향 로직은 마린에 종속

탱크

  • 시야에 적이 있는지 확인
    • 자신 제외
  • 공성모드로 변환
    • 여기까지 하면 마린과 똑같은 상태
  • 공격 지역에 적이 있는지 필터링
  • 공격 지역에 있는 적 중에서 가장 약한 유닛 필터링
  • 가장 약한 유닛 중에서 시계 방향으로 탐색
    • 시계 방향 로직은 탱크에 종속

이거 일반화 가능하겠는데?

  • 시야에 적이 있는지 필터링하는 함수
    • 이 때 자신 제외
    • Unit으로 올릴 수 있음
      • 이 때 시야에 있으면 공격 필터 타고, 없으면 NONE으로 아무 행동 X
  • 공격 지역에 적이 있는지 필터링 하는 함수
    • 템플릿 메소드 패턴
    • Unit으로 올릴 수 있음
      • 공격 지역을 반환하는 함수는 자식 클래스에서 구현
  • 공격 지역에 있는 적 중에서 가장 약한 유닛 필터링
    • Unit으로 올릴 수 있음
  • 가장 약한 유닛 중에서 시계 방향으로 탐색

탱크, 마린 움직이는 로직에서 공통점 뽑기

공통로직으로 묶기 애매합니다

  • IMovable은 구체 클래스에서 로직이 종속되는게 편하겠다.

탱크의 움직이는 함수

오른쪽, 왼쪽 방향으로 이동하는지 boolean 플래그로 상태

탱크 테스트

탱크 (0,4)

  • 좌우 이동 테스트
  • 프레임 15에 x=15 도달
  • 프레임 16에 반전(x=14)
  • 프레임 30에 x=0 도달
  • 프레임 31에 반전(x=1)

탱크 (0,4) 마린 (4,3)

  • 프레임1
    • 탱크 (1,4)로 이동
    • 마린 제자리
  • 프레임2
    • 탱크 모드 전환
    • 마린 제자리
  • 이후 교착상태

탱크 (0,4) 마린 (2,4)

  • 프레임1
    • 탱크 시즈모드
    • 마린 (1,4)로 이동
  • 프레임2
    • 마린만 때림, 탱크는 시즈모드라 12씩 피해 받음
  • 프레임9
    • 탱크 사망

탱크 (0,0) 마린 (3,0) 마린 (3,1)

  • 프레임 1
    • 탱크 시즈모드
    • 마린 서로 때림
  • 프레임 2
    • 탱크 아무것도 안함
    • 마린 서로 때림
  • 반복
  • 프레임 6
    • 마린 둘 다 사망
  • 프레임 7
    • 탱크는 시야에 적이 없으니 탱크모드로 전환
  • 프레임 8
    • 탱크 (1,0)로 이동
    • 순찰 시작

레이스이동 규칙

처음 자신의 위치 상태로 기억

마린과 공통점이 있네?

  • 적을 추격하는 개념
    • 가장 가까운 적으로 이동
    • 가장 약한 유닛 쪽으로 이동
    • 시계 방향 검색 추상 클래스로 묶어

레이스 테스트

레이스 (0,0) 마린 (2,2)

프레임 1

  • 레이스 (0,1)이동
  • 마린 (2,1)이동

프레임 2

  • 레이스 (1,1) 이동
  • 마린 (1,1) 이동

프레임 3

  • 레이스 피해 무효화
  • 마린 6피해 입음 …

프레임 8

  • 레이스 피 50 남음
  • 마린 죽음

프레임 9

  • 레이스 (1,0) 이동

프레임 10

  • 레이스 (0,0) 이동

프레임 11

  • 레이스 아무것도 안함

볼 수 있는 유닛과 공격 대상

마린

  • 지상, 공중 볼 수 있음
  • 지상, 공중 공격 가능

탱크

  • 지상 볼 수 있음
  • 지상 공격 가능

망령

  • 지상, 공중 볼 수 있음
  • 지상 공중 공격 가능

터렛

  • 공중 볼 수 있음
  • 공중 공격 가능

지뢰

  • 볼 수 없음
  • 지상 공격 가능

스마트 지뢰

  • 볼 수 없음
  • 지상 공격 가능

canSeeAir, canAttackAir 분리하는게 맞음

지뢰

지뢰는 think, move 둘다 안 함

새로운 인터페이스 CollisionEventListener

시야 0이면, “시야가 0인 유닛은 현재 자기 위치만 볼 수 있습니다.”

Unit 추상 클래스에서 filterEnemyInVision를 그대로 사용 할 수 있음

생각으로 공격 위치를 정하는 개념이 아님

  • 2.10 참고
  • “이동 후 충돌 처리”에서 일정 횟수 밟는 것을 상태로 가지고, 0이 되면 UnitAction이 공격으로

충돌은 어떻게?

  • 이번 프레임에 UnitAction이 Move이면서, 지뢰 위치로 온 녀석을 카운트

밟는 횟수는?

  • 2.7 참고
  • 생성자의 인자로 받네

인터페이스에 충돌 함수 시그니처 정의

  • 유닛을 인자로 받고, 반환값은 필요없겠지?
    • 이게 아니라 유닛 목록을 받아야지, think랑 똑같이
  • UnitAction이 Move인 것 필터링하는 것도 여기서 하면 되넹

2.10 참고

  • 충돌 처리 후
  • 각 유닛에게 공격할 기회 줌
    • 따라서 충돌 함수에서는 충돌 횟수만 까주면 됨
    • 그리고 생각을 하지 않기 때문에 충돌 처리에서 UnitAction도 공격으로 변경하는 것 필요함

스마트 지뢰

Mine 상속

  • ” 최소 몇 명의 적을 감지해야 터지는지를 나타내는 int” 상태 필요

마인이랑 공격 구역은 동일하고

충돌 로직이 달라지네

  • 밟는거도 확인
  • 지상 유닛 감지해서
    • 지뢰는 감지 못하고
    • 공중 유닛 감지 X
    • 기존에 filterEnemyInVision 그대로 사용 가능
      • 시야값 1

디스트로이어

AOE 값을 매우 크게하면

  • 거의 데미지 감소가 없음

디스트로이어의 피해치를 매우 크게하면

  • 사실상 전 범위
  • SimulationManager에서 calculateDamageByDistance 에서 커버

targetPosition을 자기 위치로, 공격력 매우 높게, AOE 매우 높게 하면 점범위 쓸어버림

  • 오버플로우 조심
  • ” 피해치(x, y) = (공격 지점에서의 피해치) * (1 - 공격 지점으로부터의 거리 / (공격의 AoE 값 + 1))”
    • 피해치는 여유롭게 크게
    • 공격 지점으로부트의 거리의 최대값은 15임
    • AOE도 여유롭게 크게

filterEnemyInVision 체인 탈 필요 없음

  • 시야값 0으로 해도 괜찮음

filterEnemyInAttackArea 체인 탈 필요 없음

  • getAttackOffset도 사용 안 해서 빈 리스트 반환

canAttackAir, canAttackGround는 true해야함

  • 시뮬레이션 매니저에서 canHit로 때릴 수 있는지 판정할 때 필요함

IThinkable 이어야지 공격을 선택할 수 있음

  • 2.10
    • “1. 각 유닛들이 이번 프레임에서 할 행동(선택지: 공격, 이동, 아무것도 안 함)을 결정”

실패 케이스

K00_SimulationTest0

여러 유닛을 월드에 추가한 뒤, 실행하는 테스트

  • 시뮬레이션을 시작할 때(프레임 0)의 유닛 배치 및 상태
  • 결과가 틀려진 프레임에서의 유닛 배치 및 상태
  • 결과가 틀려진 프레임에서의 올바른 유닛 배치 및 상태
  • 시뮬레이션에 지뢰나 스마트 지뢰가 참여하고 있다면 프레임 0 맵의 제일 아래에 다음과 같은 추가 정보를 볼 수 있습니다.
[1] # steps: 4, # enemies: 3
[8] # steps: 4
[9] # steps: 1
[B] # steps: 4
[F] # steps: 3, # enemies: 3
  • ’[]’ 안에 있는 숫자는 유닛 번호입니다.
  • ’# steps’는 지뢰가 최소 몇 번을 밟혀야 터지는지를 나타냅니다.
  • ’# enemies’는 스마트 지뢰가 몇 명의 적을 감지해야 터지는지를 나타냅니다.
Frame: 0

0(M)35 1(U)99 2(A)01 3(N)01 
4(N)01 5(U)99 6(N)01 7(A)01 
8(U)99 9(U)99 A(U)99 B(T)85 
C(A)01 D(M)35 E(W)80 F(W)80 


  0123456789ABCDEF
 +----------------+
0|   7      86    |
1|                |
2|              A |
3|       3     9  |
4|       1        |
5|  2             |
6| 5          0 B |
7|       4E      F|
 +----------------+

8-C
0-D

[2] # steps: 2, # enemies: 2
[3] # steps: 2
[4] # steps: 4
[6] # steps: 4
[7] # steps: 2, # enemies: 1
[C] # steps: 1, # enemies: 3
object state invalid: Mine 6, Turret 8, SmartMine C

Frame 0에서 “object state invalid”?

올바른 결과:
Frame: 1

0(M)29 1(U)99 2(A)01 3(N)01 
4(N)01 5(U)99 6(N)XX 7(A)01 
8(U)84 9(U)99 A(U)99 B(T)85 
C(A)XX D(M)29 E(W)80 F(W)80 


  0123456789ABCDEF
 +----------------+
0|   7      8     |
1|                |
2|              A |
3|       3     9  |
4|       1        |
5|  2             |
6| 5      E   0 BF|
7|       4        |
 +----------------+

0-D
실제 결과:
Frame: 1

0(M)29 1(U)99 2(A)01 3(N)01 
4(N)01 5(U)99 6(N)01 7(A)01 
8(U)99 9(U)99 A(U)99 B(T)85 
C(A)01 D(M)29 E(W)80 F(W)80 


  0123456789ABCDEF
 +----------------+
0|   7      86    |
1|                |
2|              A |
3|       3     9  |
4|       1        |
5|  2             |
6| 5      E   0 BF|
7|       4        |
 +----------------+

8-C
0-D

6번 마인 - 위치 (B,0) 8번 터렛 - 위치 (A,0) C번 스마트 마인 (A,0)

이 3개의 상태가 문제

정상적인 상태라면 Frame 1에서 스마트 마인 자폭

  • 동일한 위치인 터렛은 15 피해받음
  • x축 1칸 옆의 마인은 7 피해 받아서 죽음

A.3.5 “밟으면”의 의미를 이번 턴에 착지(유닛 상태가 움직이면서, 해당 위치)가 아니라 그냥 그 자리에 있으면 카운트

K01_SimulationTest1

Frame: 0

0(T)85 1(T)85 2(N)01 3(N)01 
4(N)01 5(N)01 6(N)01 7(N)01 
8(N)01 9(N)01 A(N)01 B(N)01 
C(N)01 D(N)01 E(N)01 F(N)01 


  0123456789ABCDEF
 +----------------+
0|       4        |
1|         B  9   |
2|0              E|
3|A  35 7       8 |
4| 6              |
5|D               |
6|1 F             |
7|         2      |
 +----------------+

7-C

[2] # steps: 2
[3] # steps: 1
[4] # steps: 4
[5] # steps: 4
[6] # steps: 4
[7] # steps: 4
[8] # steps: 2
[9] # steps: 1
[A] # steps: 2
[B] # steps: 4
[C] # steps: 3
[D] # steps: 3
[E] # steps: 3
[F] # steps: 2
object state invalid: Mine 7, Mine C

7번 마인 - 위치(6,3) C번 마인 - 위치(6,3)

실제 결과:

Frame: 3

0(T)85 1(T)85 2(N)01 3(N)01 
4(N)01 5(N)01 6(N)01 7(N)01 
8(N)01 9(N)01 A(N)01 B(N)01 
C(N)01 D(N)01 E(N)01 F(N)01 


  0123456789ABCDEF
 +----------------+
0|       4        |
1|         B  9   |
2|   0           E|
3|A  35 7       8 |
4| 6              |
5|D               |
6|  F1            |
7|         2      |
 +----------------+

7-C

Unit stats mismatch: SimulationManager

나는 겹친 마인이 살아있음

올바른 결과:

Frame: 3

0(T)85 1(T)85 2(N)01 3(N)01 
4(N)01 5(N)01 6(N)01 7(N)XX 
8(N)01 9(N)01 A(N)01 B(N)01 
C(N)XX D(N)01 E(N)01 F(N)01 


  0123456789ABCDEF
 +----------------+
0|       4        |
1|         B  9   |
2|   0           E|
3|A  35         8 |
4| 6              |
5|D               |
6|  F1            |
7|         2      |
 +----------------+

올바른 결과에서 두 마인 죽음

7번 마인은 4번 C번 마인은 3번 밟으면 터짐

3프레임 동안 서로 서로 밟는(같은 위치)라서 C가 터졌고 7번은 데미지 받아서 죽음

지뢰는 안 보이는 유닛과 겹쳐있어도 밟은 횟 수 카운트

K02_SimulationTest2

Frame: 0

0(U)99 1(W)80 2(W)80 3(M)35 
4(T)85 5(A)01 6(T)85 7(M)35 
8(M)35 9(A)01 A(T)85 B(M)35 
C(N)01 D(W)80 E(W)80 F(A)01 


  0123456789ABCDEF
 +----------------+
0|26 B 50         |
1| A              |
2|F7   1          |
3|   38           |
4|                |
5|                |
6|                |
7|                |
 +----------------+

6-E
B-D
5-9
0-4
3-C

[5] # steps: 4, # enemies: 1
[9] # steps: 1, # enemies: 3
[C] # steps: 3
[F] # steps: 2, # enemies: 2
object state invalid: Turret 0, Tank 4
올바른 결과:

Frame: 1

0(U)85 1(W)80 2(W)80 3(M)29 
4(T)57 5(A)XX 6(T)73 7(M)28 
8(M)29 9(A)XX A(T)59 B(M)29 
C(N)XX D(W)80 E(W)80 F(A)XX 


  0123456789ABCDEF
 +----------------+
0|26 B  0         |
1| A   1          |
2| 7              |
3|   38           |
4|                |
5|                |
6|                |
7|                |
 +----------------+

6-E
B-D
0-4
실제 결과:

Frame: 1

0(U)92 1(W)80 2(W)80 3(M)29 
4(T)71 5(A)XX 6(T)73 7(M)28 
8(M)29 9(A)XX A(T)59 B(M)29 
C(N)XX D(W)80 E(W)80 F(A)XX 


  0123456789ABCDEF
 +----------------+
0|26 B  0         |
1| A   1          |
2| 7              |
3|   38           |
4|                |
5|                |
6|                |
7|                |
 +----------------+

6-E
B-D
0-4

Unit stats mismatch: SimulationManager

0번 터렛 - 위치(6,0) 4번 탱크 - 위치(6,0)

올바른 결과에서

  • 탱크는 28데미지 받음
  • 터렛은 14데미지 받음

실제 결과에서

  • 탱크는 14데미지 받음
  • 터렛은 7데미지 받음

5번 스마트 지뢰 - 위치(5,0)의 데미지만 들어감

  • 15 * (1 - 1 / 2 ) 소수점 버림 7 9번 스마트 지뢰 - 위치(5,0)의 데미지도 들어가야함
  • 5번 스마트 지뢰가 9번 스마트 지뢰를 죽이는게 아님
  • 9번 스마트 지뢰는 1번 밟으면 터지니 겹쳐있으면 자폭 데미지 넣고 죽어야함
  • K01_SimulationTest1 과 동일한 원인

K00_SimulationTest0 (2회차)

Frame: 0

0(N)01 1(M)35 2(A)01 3(M)35 
4(A)01 5(A)01 6(U)99 7(A)01 
8(W)80 9(A)01 A(W)80 B(U)99 
C(A)01 D(U)99 E(N)01 F(T)85 


  0123456789ABCDEF
 +----------------+
0|       E    7   |
1|                |
2|     2          |
3|C        4 B    |
4|         D    0 |
5| 3              |
6|           589  |
7|     A 6 1   F  |
 +----------------+


[0] # steps: 1
[2] # steps: 4, # enemies: 2
[4] # steps: 1, # enemies: 1
[5] # steps: 3, # enemies: 1
[7] # steps: 3, # enemies: 3
[9] # steps: 1, # enemies: 3
[C] # steps: 2, # enemies: 1
[E] # steps: 1
object state invalid: SmartMine 5, Tank F

스마트 지뢰의 “감지”는 충돌이 아님

  • “감지”를 충돌로 간주하면, 이동 후 결과에 따라 감지
  • “감지”를 생각으로 간주하면, 이동 전 상황에서 감지
  • 2.10 참고

탱크 - 위치(13,7) 스마트 마인 - 위치 (11, 6)

탱크(적 없음)가 동쪽 순찰 후 반전해 지뢰의 대각 시야로 걸어 들어옴
프레임 1~4: 탱크 (14,7)-(15,7)-(14,7)-(13,7), 지뢰와 거리 2 이상
프레임 5: 탱크 (12,7) 도착 - 이동 후에는 거리 1이지만, 프레임 시작엔 (13,7)이었으므로 미감지. 둘 다 무사
프레임 6: 프레임 시작 위치 (12,7)이 거리 1 - 감지 폭발(XX). 탱크는 (11,7)로 이동한 뒤 -7(78) (범위 효과로 15에서 7)

  • (이동 후 시점에 감지하는 구현은 프레임 5에 조기 폭발해 탱크 78이 한 프레임 일찍 나옴)