체비쇼프 거리
시야, AOE를 체비쇼프 거리라고 하네요.
시야를 구하는 함수
영역을 구해서 칸(배열)을 반환하는 것은 비효율적
- 시야 영역을 구해도, 어차피 이동/공격에서 시야 배열을 순회하면서 탐색해야 하니까…
시야의 유닛을 모두 반환하는게 좋을까?
- 순회하면서 유닛을 찾겠다는 의도
- 누가 찾아서 사용할건데?
- 누군가는 유닛의 목록을 알 고 있으면 유닛을 찾을 필요가 없는데?
SimulationManager?
유닛 목록을 누가 관리하는가?
시뮬레이션 매니저 코드는 직접 구현해야 하고 public 매소드들이 쭉 선언되어 있음
- 이것 만으로 정보는 부족하니까, 사용처를 확인해보자
3. 본인 컴퓨터에서 테스트하는 법 코드를 확인해보면
- 직접 유닛을 생성하고
spawn함수를 통해 등록하는 개념 - 비쥬얼라이저는
simulationManager.getUnits()로 얻은 유닛 목록을 이용해 시각화함
2.10 SimulationManager 클래스를 구현한다 명세를 확인해보면 SimulationManager는 유닛 목록을 관리해야함
- 생사 여부
따라서 유닛 목록을 알 고 있는 누군가는 SimulationManager로 확정!
- 사고를 전환
- 맵의 유닛을 찾음 X
- 유닛 목록을 전지적으로 다 알고 있음 O
시야를 구하는 함수의 시그니처 → 유닛을 볼 수 있는지 확인하는 함수의 시그니처
외부에서 SimulationManager가 유닛 목록을 관리하고 있으니, 함수의 매개변수에 유닛을 넣으면 매니저가 넣어주면 됨
어떤 유닛의 함수에 유닛을 넘겨서 유닛이 가진 구체적인 상태(스펙)에 따라 target.canSee(other)을 했을 때 “target”이 “other”을 볼 수 있는지 구할 수 있음
매개변수:
- 유닛 반환값:
- boolean
“유닛을 볼 수 있는지 구하는 함수”를 구현한다고 결론!
- 이는 공통 로직이니까 추상 클래스에 선언하면 되겠죠?
유닛을 볼 수 있는 지 판단하는 함수 구현 방법에는 크게 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;
}
}템플릿 메서드 패턴으로 선택
공격하는 함수 → 생각하는 함수
공격하는 함수 전 공격이라는 동작을 결정하는 “생각”이라는 기능이 필요함
- 2.10 SimulationManager 클래스를 구현한다 에서
update함수의 동작 참고- 마린은 생각할 수 있는 유닛 같아
- 공격/이동을 결정하니까
- 마린은 생각할 수 있는 유닛 같아
“생각”(행동 결정)과 “실행”은 분리되어 있음
- A.2 시뮬레이션 규칙 참고
- 각 유닛의 이동 순서가 달라져도 시뮬레이션 결과는 동일함
- 각 유닛의 공격 순서가 달려저도 시뮬레이션 결과는 동일함
- 분리해야 각 유닛이 동일한 상황(스냅샷)을 보고 실행할 동작이 결정나니까, 실행 간의 순서가 바뀌어도 문제가 없음
- 만약 분리되지 않아서 먼저 실행한 유닛에 따라 상황(스냅샷)이 바뀌진 않음
따라서 생각하는 함수 기능을 먼저 구현해
- 공격 생각
- 이동 생각
- 아무것도 안 함 생각
생각하는 함수
2.10 SimulationManager 클래스를 구현한다 를 보면 registerThinkable 함수가 있음
- 일단 접미사가 “~able” 이니 인터페이스로 구현해볼까?
- 어떤 유닛은 생각, 어떤 유닛은 움직일 수 있고, 등등
- 다중 상속 관점
- 006-016-add-wristwatch-and-multiple-inheritance
- 유닛의 능력이 상속 트리로 표현하기 어려움
- 다중 상속 관점
registerThinkable함수의 매개변수 타입을IThinkable로
- 어떤 유닛은 생각, 어떤 유닛은 움직일 수 있고, 등등
IThinkable 에 생각하는 함수의 시그니처를 작성해보자
반환값은 enum으로 하면 되겠지?
SimulationManager.update내부에서 사용하기 편하잖아?- 이것도 개체 스스로 자신의 상태를 책임지게 하려면 마린의 이동하는 함수에서 언급한 것 처럼 반환하지 않고, 멤버 변수로 상태를 기억
매개변수로는 유닛 목록을 받으면 판단할 수 있잖아?
- 시뮬레이터가 유닛 목록을 관리하니 넣어주면 되고
마린의 생각하는 함수
마린의 생각하는 함수에 어떤 식으로 구현할까?
- A.2 시뮬레이션 규칙
- A.2.9 에서 공격을 먼저 판단해야 함을 알 수있음
- 공격
- 시야에서 발견한 적을 공격
- 공격 위치 정해서 상태로 기록
- A.2 시뮬레이션 규칙의 A.2.13 참고
- 교전/이동 규칙의 각 단계를 실행한 후에도 적이 한 명으로 특정되지 않고 여럿이 남아있다면 남은 적들에 대해 다음 단계들을 실행
- A.2 시뮬레이션 규칙의 A.2.13 참고
- 이동
- 시야에서 발견했지만, 공격 구역이 아닐 때
- 이동 위치 정해서 상태로 기록
- A.2.13 규칙 그대로 적용
- 아무 행동 X
- 시야에 적이 없는 경우
마린의 공격 생각
필터링 기반으로 구현
공격 규칙은 아래와 같음
- 공격 가능 지역으로 필터링
- 자기 자신은 제외
- 볼 수 있어야함
- HP가 가장 낮은 유닛으로 필터링
- 시계 방향 점수로 필터링
- 공격 가능 지역은 하위 유닛에 “vector:점수” 룩업 테이블을 이용함
- 이 룩업 테이블은 “1.공격 가능 지역으로 필터링”에서도 재활용 해되 되고, 테이블 분리해도 되고
- 공격 가능 지역은 하위 유닛에 “vector:점수” 룩업 테이블을 이용함
공격 생각 구현에서 주의할 것
- 공격 함수는
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처럼 유닛 목록을 받으면 되지 않을까?
- “2.1 전반적인 규칙” 참고
매개변수는
- 진짜 움직일 때는 자신의 위치를 바꿔야겠음
- 유닛 목록이 필요없으면 받아서 안 쓰면 되고..
- 유닛 목록이 필요없네요, 생각할 때만 유닛 목록이 필요하고
- 이거 아님 마린의 이동하는 함수에서 수정
마린의 이동 생각
A.2.6
- 바로 인접한 타일
- 동서남북 중 한 방향
- 한 프레임에 한 칸만
- A.2 시뮬레이션 규칙 참고
공격처럼 A.2.13 그대로 적용
- 스냅샷 기반 이동 위치 정하기
이동 생각 알고리듬은 다음과 같음
- 볼 수 있는 유닛 확인
- [[pocu-note/COMP2500/901-assignmnet/901-003-assignment-3/spec#A.3.1 해병(Marine, 마린)|#A.3.1 해병(Marine, 마린)]] 참고, 해병은 볼 수 있는 유닛이 없으면 움직이지 않음
Unit에서 상태vision값을 기반으로canSee를 이미 구현했음
- 거리로 필터링
- HP가 가장 낮은 유닛으로 필터링
- 시계 방향 점수로 필터링
- 방법이 여러 가지
- vision 값에 따라 동일한 멘하튼 거리의 diff vector 만들기
- “멘하튼 거리: vector 목록”
- 4번째 필터까지 왔다는 것은, 멘하튼 거리가 동일한 것들에서 필터링
- 다이아 몬드 모양으로 움직이는 동선을 인덱스화
- 이 방법을 채택하고 구체적으로 이해해보면 멘하튼 거리가 동일한 집합에서 순서 정하기
- 다이아 몬드 모양으로 움직이는 동선을 인덱스화
- vision 값에 따라 동일한 멘하튼 거리의 diff vector 만들기
- 방법이 여러 가지
이동 생각도 마린의 공격 생각에서 본 것 처럼 마지막 규칙에서 여러 명이 특정되도 이동 지점은 하나로 특정됨
- 그래서 마지막 규칙 적용하는 함수는
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씩 증가
- 이를 일반화해 변마다 어떤 좌표가 걸음당 1씩 단조 변화하는지만 찾기
E→S 변을 걸어보면 d=2일 때
- 이 변 위에서는 걸음 수 = dy
- 그리고 지금까지 누적 걸음을 더해줘서 앞의 변보다 이 변에서 걸음이 뒤에 있도록
이를 일반화 하면 아래 표처럼 됨
| 변 | 걸음마다 | 변 위에서 걸음 수 | 변 시작까지 누적 걸음 | index |
|---|---|---|---|---|
| N→E | dx +1, dy +1 | dx | 0 | dx |
| E→S | dx −1, dy +1 | dy | d | d + dy |
| S→W | dx −1, dy −1 | −dx | 2d | 2d − dx |
| W→N | dx +1, dy −1 | −dy | 3d | 3d − 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축을 따라 이동합니다.”
이동은 직접 위치를 바꾸면 되죠?
- 적절한 위치인지도 마린 자체가 판단하는게
- 공격 규칙에서 적절한 위치인지 판단해서, 그 결과에 따라 이동으로 넘어갈지 안 넘어갈지가 영향을 받음
- 이동 규칙에서 절절한 위치인지 판단해서, 그 결과에 따라 아무것도 안 할지 영향을 받음
- 근데 마린은 유닛을 따라가니까 적절한 위치인지 판단할 필요가 없음
- 애초에 유효한 위치의 유닛목록만 프레임에서 다룬다고 가정하면 되요
어쨌건 적절한 위치로 이동하는 것은 각 유닛이 판단하는게 맞음
Move는 ActionType에 따른 가드가 필요함
Move를 어떻게 호출할 것인가?Think에 따라 이번 프레임에 이동이 결정난 것들만 이동해야함Move내부에서 여기에 대한 필터링을 수행하는게 좋음- 이렇게 안 하면
SimulationManager에서Movable목록에서 진짜 움직여야 하는 애들을 움직이거나..- 이것도 괜찮은게
Think에서 어떤 행동을 해야하는지 반환하고 이걸 기반으로 사용하면 되잖아? - 개체 상태를 스스로 책임지는 관점에서는 반환하지 않고, 자기가 상태를 가지는게 좋아보이긴 함..?
- 이것도 괜찮은게
- 이렇게 안 하면
참고로 Unit의 attack이 결국 AttackIntent로 시뮬레이터가 무엇인가 할 수 있게 반환하지만 null 도 반환할 수 있음
- 결국 공격할지 말지는 개체 스스로가 결정하는 개념이야
- 이 결정이
think - “공격할 기회”만 받을 뿐
- 이동도 유사하게 “이동할 기회”만 받을 뿐
think로 결정된 상태에 따라 이동하겠지- 그래서 가드가 필요함
- 이 결정이
마린의 공격하는 함수
Unit 클래스에 attack()이 있음
- 시그니처 바꿀 수 있나?
- 2.1.1 에서 불가능하다고 나와요
- 추상 메서드로 변경은 시그니처 변경에 해당되는가?
- 아님
- 보통 시그니처는 함수명 그리고 매개변수 목록, 반환형 까지
상태로 저장한 targetPosition위치에 공격함
AttackIntent 에 다음 정보를 포함하면?
- 위치
- AOE 계산 때문에
- 피해치
- 공격자
- 자기위치 때리더라도 자신은 피해입으면 안 됨
- 이건 여기가 아니라
OnAttacked에서 처리하는? - 그래서 공격자를 포함하는 구나
- AOE 값
사실상 정보 반환이 전부네
피해량 계산은 OnAttakced에서 처리 이러면 추상 메서드로 구현할 이유가 없네?
- 반환하는 기능을 부모에 구현하면 됨
마린의 이동하는 함수에서 봤듯이 호출자(시뮬레이터)는 마린의 상태를 모름
- 자신이 공격 가능한 상태인지 자신이 책임지기
think로 결정된 상태에 따라 가드
마린의 피해를 입히는 함수
2.10 참고
update()내부에서 피해를 입히는 함수를 호출함Unit이라는 상위 클래스에onAttacked의 시그니처를 유지하면서 구현- 부모 클래스의 시그니처를 유지해야 late binding으로 호출 할 수 있음
- 여기서도 개체들에 내리는 동일한 명령 참고
- 이 때문에 스펙에서 애초에 가이드 라인 겸 stub을 준거지
- 부모 클래스의 시그니처를 유지해야 late binding으로 호출 할 수 있음
마린은 피해 입을 때 특별한 규칙 없음
onAttacked시그니처를 보니까, 데미지 계산같은건 시뮬레이션 매니저에서 끝내고 damage를 넘김- AOE 계산을 시뮬레이션 매니저에서 수행하고
다른 유닛의 피해를 입을 때 특별한 규칙이 있는가?
- 전차는 공성 모드일 때 피해 2배
- 망령은 특수 방어막
- 특수 방어막 상태는 자기가 관리하면 되겠지? 시뮬레이션 매니저는 방어막 여부는 관계없이
onAttakced호출하고attack,move에서 자신의 상태를 관리하는 것과 유사- 개체지향
- 특수 방어막 상태는 자기가 관리하면 되겠지? 시뮬레이션 매니저는 방어막 여부는 관계없이
- 포탑도 없고
- 지뢰도 없고
- 스마트 지뢰도 없고
- 파괴자는 있네
마린의 공격하는 함수에서 본 attack의 구현과 다르게 이번에는 추상 메서드화 하고 세부 구현을 해도 되고?
Unit에 구체적인 매소드를 그대로 두되, 오버라이딩 해도 되고
마린은 특별한 규칙이 없으니 Unit을 그대로 사용하자
- 오버라이딩은 선택사항 참고
시뮬레이터 구현
getUnits 는 살아있는 유닛 목록 반환하면 됨
- A.1 “HP가 0이 된 유닛은 죽습니다” 참고
- 시뮬레이터는 살았는지 죽었는지 확인하는 함수를 호출해야하니 만들어주죠
Unit에 구현- 근데 이미
getHp가 stub으로 있네?- hp 값으로 판단해보자
우선 유닛을 어떻게 상태로 저장할 것인가?
- 유닛을 어떻게 추가하지?
- 본인 컴퓨터에서 테스트하는 법 을 보면
spwan에 그 기능이 있네? - 내부에 변할 수 있는 리스트 두죠
- 본인 컴퓨터에서 테스트하는 법 을 보면
생각해보니 반환할 때 필터링도 필요없네
- 2.10 보면
update내부에서 죽은 유닛들을 모두 게임에서 제거하니까- 유닛 목록 자체가 살아있는 유닛만 가짐
- 반환할 때 새로 리스트 만들어야하나??
- 깊은복사 할 필요가 없는데?
- 2.1.11 “1. 유닛을 고유하게 식별하는 기준은 그 개체의 참조(레퍼런스)입니다. 따라서 유닛을 깊게 복사하지 마세요.”
- 깊은복사 할 필요가 없는데?
spawn도 논리가 단순하네. 그냥 유닛 추가하면 끝임
registerThinkable, registerMovable 같은 등록 류는?
- 별도로 목록을 관리해야지
- 왜?
- update 내부에서 생각할 수 있는 유닛에게만 지시, 움직일 수 있는 유닛에게만 지시, 충돌할 수 있는 유닛만 지시..
- 별도로 관리하는 만큼, 유닛 추가/제거 시 어떻게 하지?
- 시뮬레이터는 타입을 모르고, 개체는 안다?
- 2.2 “
onSpawn(): 유닛이 월드에 추가될 때(spawn)SimulationManager가 이 메서드를 호출해야 합니다.” - 개체가 스폰할 때 onSpawn을 호출하고, 여기서 등록하는 함수를 호출하는거구나
- 2.2 “
- 시뮬레이터는 타입을 모르고, 개체는 안다?
- 그리고 매개변수의 타입도 변경해야함
- “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)
- (2) 모드 전환 필요성 확인
- 적이 없으면
- 모드 전환 필요성 확인
- 모드 전환해야하면 탱크모드하고 다음 턴
- 이미 탱크모드면 이동 (findMovePosition)
- 모드 전환 필요성 확인
전환하는 도중에 맞으면 어떻게 되지?
- 2.10 보면
- “이동할 기회를 줌” ⇒ “공격할 기회를 줌”
- 이동 전에 모드 전환이 결정나니까, 모드가 바뀌고나서 피해를 받는게 맞는 것 같은데
- 뭔가 다른 방법은 애매한데?
- 이번 턴에 시즈모드로 전환하면, 전환한 시즈모드 기준 피해량 받는거지
-
- 본인 컴퓨터에서 테스트하는 법 참고하면
- Frame 0에서 탱크랑, 마린이랑 겹처서 생성됨
- “3-4” 확인
- 탱크랑 마린 겹치면 탱크는 자기 위치 공격 불가능, 시야 안에서 적을 찾음
- 그러면 탱크의 초기 모드는 무엇인가?
- FRAME 1에서 탱크는 공격을 받음, 12가 줄었네?
- 6 * 2? 마린이 자기 위치 공격했고
- 그러면 초기는 시즈모드여?
- 만약에 탱크가 시즈모드 였다면, 시즈모드 그대로 유지
- 시야에 적이 있으니까
- 만약에 탱크가 탱크모드 였다면, 시즈모드로 변환
- 시야에 적이 있으니까
- Frame 0 ⇒ Frame1에서 탱크한태 공격받은 놈이 있는지 볼까?
- u1(0,5), u3(2,4)니까 공격 가능하거든?
- 어 마린 피가 안 닳았네?
- 탱크는 (0,5) 공격하면, u1이 (0,4)로 이동하고도 AOE로 데미지 받아야하는데?
- 아 그러면 시작은 탱크모드로 시작하고, 전환한다고 안 때림
- u1(0,5), u3(2,4)니까 공격 가능하거든?
- FRAME 1에서 탱크는 공격을 받음, 12가 줄었네?
- 그러면 탱크의 초기 모드는 무엇인가?
- 결론은 탱크는 시작이 탱크모드, 전환하면 전환 후 상태로 데미지 받음
- Frame 1 ⇒ Frame 2에서 마린이 4데미지 받았는데
- 탱크가 (0,4)때렸구 마린이 (1,4)로 이동해서
- 8 * (1 - 1 / (1 + 1)) = 4
- 검증 완료
- 탱크가 (0,4)때렸구 마린이 (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인 유닛은 현재 자기 위치만 볼 수 있습니다.”
- A.1 참고
- 체비쇼프 거리
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”?
- https://docs.google.com/document/d/1HvQj4orCIcL6hug-C9n7c2631SiWOKRVawnKi4FkyI8/edit?tab=t.0
- 개체의 상태가 올바르지 않을 때 나오는 메시지?
올바른 결과:
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이 한 프레임 일찍 나옴)