자물쇠와 열쇠로 길 찾기?


22

저는 자물쇠와 열쇠 퍼즐 과 유사한지도로 게임을하고 있습니다 . AI는 잠긴 빨간 문 뒤에있을 수있는 목표로 이동해야하지만 빨간 열쇠는 잠긴 파란색 문 뒤에있을 수 있습니다.

이 퍼즐은 다음과 같이 젤다 스타일의 던전과 비슷합니다.

젤다 던전

목표에 도달하려면 보스를 물리 쳐야합니다. 보스를 넘겨야하며 깃털을 모으고 키를 모아야합니다

젤다 던전은 선형 인 경향이 있습니다. 그러나 일반적인 경우에는 문제를 해결해야합니다. 그래서:

  • 목표는 일련의 키 중 하나를 요구할 수 있습니다. 따라서 빨간색 키 또는 파란색 키를 가져와야 할 수도 있습니다. 아니면 먼 길을 열어 놓은 문이있을 수 있습니다!
  • 여러 개의 문과 열쇠가있을 수 있습니다. 예를 들어지도에 여러 개의 빨간색 키가있을 수 있으며 하나를 수집하면 모든 빨간색 문에 대한 액세스 권한이 부여됩니다.
  • 오른쪽 키는 잠긴 문 뒤에 있기 때문에 목표에 접근 할 수 없습니다

그러한지도에서 길 찾기를 어떻게 수행합니까? 검색 그래프는 어떤 모양입니까?

참고 : 액세스 할 수없는 목표를 감지하는 데 대한 마지막 사항이 중요합니다. 예를 들어 목표에 접근 할 수없는 경우 A *는 매우 비효율적입니다. 나는 이것을 효율적으로 다루고 싶다.

AI가 모든 것이 어디에 있는지 알고 있다고 가정하자.


4
AI가 잠금을 해제 한 후에 만 ​​물건을 알고 발견합니까? 예를 들어 깃털이 잠긴 문 뒤에 있다는 것을 알고 있습니까? AI는 "그게 자물쇠 야 열쇠가 필요해"와 같은 개념을 이해합니까? "나의 길을 막고있는 무언가가 있으므로 찾은 모든 것을 시도해보십시오. 열쇠는 요? "
Tim Holt

1
질문 에서 앞뒤로 길 찾기 에 대해이 문제에 대한 이전의 토론이 있었으며 , 이는 당신에게 유용 할 수 있습니다.
DMGregory

1
따라서 플레이어를 시뮬레이션하지 않고 최적화 된 던전 런을 만들려고합니까? 내 대답은 분명히 선수 행동을 시뮬레이션하는 것에 관한 것이었다.
Tim Holt

4
불행히도 접근 할 수없는 목표를 감지하는 것은 매우 어렵습니다. 목표에 도달 할 수있는 방법이 없다는 것을 확신 할 수있는 유일한 방법 은 도달 가능한 공간 전체를 탐색하여 목표를 포함하지 않는지 확인하는 것입니다. A *의 목표는 목표가 접근 할 수 없습니다. 공간을 덜 검색하는 알고리즘은 경로가 검색을 건너 뛴 공간의 일부에 숨겨져 있기 때문에 목표에 사용 가능한 경로가 누락 될 위험이 있습니다. 모든 타일 또는 navmesh 다각형 대신 룸 연결 그래프를 검색하여 더 높은 수준으로 작업하여이를 가속화 할 수 있습니다.
DMGregory

답변:


22

표준 길 찾기는 충분 합니다.주는 현재 위치 + 현재 인벤토리입니다. "이동"은 방을 변경하거나 재고를 변경하는 것입니다. 이 답변에서 다루지 않았지만 너무 많은 노력을 기울이지 않으면 서 A *에 대해 좋은 휴리스틱을 작성하고 있습니다. 멀리 떨어져서 물건을 집어 올리는 것을 선호하고 대상 근처의 문을 여는 것을 선호하여 검색 속도를 높일 수 있습니다 먼 길을 찾는 등

이 답변은 처음 나온 이후 많은 찬사를 받았으며 데모가 있지만 훨씬 최적화되고 전문화 된 솔루션의 경우 "거꾸로하는 것이 훨씬 빠릅니다"라는 답변도 읽어보십시오. /gamedev/ / a / 150155 / 2624


아래에서 완전히 작동하는 Javascript 개념 증명. 코드 덤프로서의 답변에 대해 죄송합니다. 실제로 좋은 답변이라고 확신하기 전에 실제로 구현했지만 실제로는 유연 해 보입니다.

경로 찾기에 대해 생각할 때 시작하려면 간단한 경로 찾기 알고리즘의 계층 구조는 다음과 같습니다.

  • 너비 우선 검색은 최대한 간단합니다.
  • Djikstra의 알고리즘은 너비 우선 탐색과 비슷하지만 주마다 "거리"가 다양합니다.
  • A *는 지크 스트라 (Jikstras)로, 휴리스틱으로 사용할 수있는 '올바른 방향의 일반적인 감각'이 있습니다.

우리의 경우, "상태"를 "위치 + 재고"로 "거리"를 "이동 또는 아이템 사용"으로 인코딩하면 Djikstra 또는 A *를 사용하여 문제를 해결할 수 있습니다.

다음은 예제 수준을 보여주는 실제 코드입니다. 첫 번째 스 니펫은 비교를위한 것입니다. 최종 솔루션을 보려면 두 번째 부분으로 이동하십시오. 올바른 경로를 찾는 Djikstra의 구현부터 시작하지만 모든 장애물과 키를 무시했습니다. (시도해보십시오. 방 0-> 2-> 3-> 4-> 6-> 5에서 마감 처리를위한 딱정벌레를 볼 수 있습니다)

function Transition(cost, state) { this.cost = cost, this.state = state; }
// given a current room, return a room of next rooms we can go to. it costs 
// 1 action to move to another room.
function next(n) {
    var moves = []
    // simulate moving to a room
    var move = room => new Transition(1, room)
    if (n == 0) moves.push(move(2))
    else if ( n == 1) moves.push(move(2))
    else if ( n == 2) moves.push(move(0), move(1), move(3))
    else if ( n == 3) moves.push(move(2), move(4), move(6))
    else if ( n == 4) moves.push(move(3))
    else if ( n == 5) moves.push(move(6))
    else if ( n == 6) moves.push(move(5), move(3))
    return moves
}

// Standard Djikstra's algorithm. keep a list of visited and unvisited nodes
// and iteratively find the "cheapest" next node to visit.
function calc_Djikstra(cost, goal, history, nextStates, visited) {

    if (!nextStates.length) return ['did not find goal', history]

    var action = nextStates.pop()
    cost += action.cost
    var cur = action.state

    if (cur == goal) return ['found!', history.concat([cur])]
    if (history.length > 15) return ['we got lost', history]

    var notVisited = (visit) => {
        return visited.filter(v => JSON.stringify(v) == JSON.stringify(visit.state)).length === 0;
    };
    nextStates = nextStates.concat(next(cur).filter(notVisited))
    nextStates.sort()

    visited.push(cur)
    return calc_Djikstra(cost, goal, history.concat([cur]), nextStates, visited)
}

console.log(calc_Djikstra(0, 5, [], [new Transition(0, 0)], []))

그렇다면이 코드에 항목과 키를 어떻게 추가합니까? 단순한! 모든 "상태"대신 방 번호 만 시작하면 이제 방의 튜플이되고 재고 상태가됩니다.

 // Now, each state is a [room, haskey, hasfeather, killedboss] tuple
function State(room, k, f, b) { this.room = room; this.k = k; this.f = f; this.b = b }

이제 전환이 (비용, 룸) 튜플에서 (비용, 상태) 튜플로 변경되므로 "다른 룸으로 이동"과 "항목 선택"을 모두 인코딩 할 수 있습니다.

// move(3) keeps inventory but sets the room to 3
var move = room => new Transition(1, new State(room, cur.k, cur.f, cur.b))
// pickup("k") keeps room number but increments the key count
var pickup = (cost, item) => {
    var n = Object.assign({}, cur)
    n[item]++;
    return new Transition(cost, new State(cur.room, n.k, n.f, n.b));
};

마지막으로, Djikstra 함수에 대한 사소한 유형 관련 변경을 수행합니다 (예를 들어, 여전히 전체 상태가 아닌 목표 방 번호와 일치하는 경우). 인쇄 된 결과는 먼저 4 번 방으로 가서 열쇠를 줍고 1 번 방으로 가서 깃털을 줍고 6 번 방으로 가서 보스를 죽인 후 5 번 방으로갑니다.

// Now, each state is a [room, haskey, hasfeather, killedboss] tuple
function State(room, k, f, b) { this.room = room; this.k = k; this.f = f; this.b = b }
function Transition(cost, state, msg) { this.cost = cost, this.state = state; this.msg = msg; }

function next(cur) {
var moves = []
// simulate moving to a room
var n = cur.room
var move = room => new Transition(1, new State(room, cur.k, cur.f, cur.b), "move to " + room)
var pickup = (cost, item) => {
	var n = Object.assign({}, cur)
	n[item]++;
	return new Transition(cost, new State(cur.room, n.k, n.f, n.b), {
		"k": "pick up key",
		"f": "pick up feather",
		"b": "SLAY BOSS!!!!"}[item]);
};

if (n == 0) moves.push(move(2))
else if ( n == 1) { }
else if ( n == 2) moves.push(move(0), move(3))
else if ( n == 3) moves.push(move(2), move(4))
else if ( n == 4) moves.push(move(3))
else if ( n == 5) { }
else if ( n == 6) { }

// if we have a key, then we can move between rooms 1 and 2
if (cur.k && n == 1) moves.push(move(2));
if (cur.k && n == 2) moves.push(move(1));

// if we have a feather, then we can move between rooms 3 and 6
if (cur.f && n == 3) moves.push(move(6));
if (cur.f && n == 6) moves.push(move(3));

// if killed the boss, then we can move between rooms 5 and 6
if (cur.b && n == 5) moves.push(move(6));
if (cur.b && n == 6) moves.push(move(5));

if (n == 4 && !cur.k) moves.push(pickup(0, 'k'))
if (n == 1 && !cur.f) moves.push(pickup(0, 'f'))
if (n == 6 && !cur.b) moves.push(pickup(100, 'b'))	
return moves
}

var notVisited = (visitedList) => (visit) => {
return visitedList.filter(v => JSON.stringify(v) == JSON.stringify(visit.state)).length === 0;
};

// Standard Djikstra's algorithm. keep a list of visited and unvisited nodes
// and iteratively find the "cheapest" next node to visit.
function calc_Djikstra(cost, goal, history, nextStates, visited) {

if (!nextStates.length) return ['No path exists', history]

var action = nextStates.pop()
cost += action.cost
var cur = action.state

if (cur.room == goal) return history.concat([action.msg])
if (history.length > 15) return ['we got lost', history]

nextStates = nextStates.concat(next(cur).filter(notVisited(visited)))
nextStates.sort()

visited.push(cur)
return calc_Djikstra(cost, goal, history.concat([action.msg]), nextStates, visited)
o}

console.log(calc_Djikstra(0, 5, [], [new Transition(0, new State(0, 0, 0, 0), 'start')], []))

이론적으로 이것은 BFS에서도 작동하며 Djikstra의 비용 기능은 필요하지 않지만 비용을 가짐으로써 "키를 집어 올리는 것은 쉽지 않지만 보스와의 싸움은 정말 어렵고 오히려 역 추적입니다." "우리가 선택을했다면 보스와 싸우기보다는 100 걸음":

if (n == 4 && !cur.k) moves.push(pickup(0, 'k'))
if (n == 1 && !cur.f) moves.push(pickup(0, 'f'))
if (n == 6 && !cur.b) moves.push(pickup(100, 'b'))

예, 검색 그래프에 인벤토리 / 키 상태를 포함시키는 것이 하나의 솔루션입니다. 증가 된 공간 요구 사항에 대해 걱정하고 있습니다. 4 개의 키가있는 맵에는 키가없는 그래프 공간의 16 배가 필요합니다.
congusbongus

8
@congusbongus는 NP 완료 여행 세일즈맨 문제에 오신 것을 환영합니다. 다항식 시간에이를 해결할 수있는 일반적인 솔루션은 없습니다.
ratchet freak

1
@congusbongus 나는 일반적으로 검색 그래프가 그렇게 많은 오버 헤드가 될 것이라고 생각하지 않지만 공간이 염려되면 데이터를 포장하십시오. 게이트로 사용하는 데 관심이있는 항목 (최대 8 개의 고유 항목)에 대해 각각 조금씩) 당신이 공상을 얻고 싶은 경우에, 당신은 더 작은 비트로 항목을 포장하는 종속성을 사용할 수있는, 즉 "키"와 간접 전이 dpendency가 이후 "상사"에 대해 동일한 비트를 사용
지미

@Jimmy은 개인이 아니다하더라도, 나는 :) 내 대답의 언급에 감사
Jibb 스마트

13

거꾸로 A *는 트릭을 할 것입니다

정방향 및 역방향 경로 찾기에 대한 질문에 대한 이 답변에서 논의 된 바와 같이 , 역방향 경로 찾기는이 문제에 대한 비교적 간단한 솔루션입니다. 이것은 목표 지향적 인 궁금를 최소화하면서 효율적인 솔루션을 계획하는 GOAP (Goal Oriented Action Planning)와 매우 유사합니다.

이 답변의 맨 아래에는 귀하가 제시 한 예를 어떻게 처리하는지에 대한 세부 정보가 있습니다.

상세히

대상에서 시작까지의 경로를 찾습니다. 길 찾기에서 잠긴 문을 발견 한 경우 문을 잠금 해제 한 것처럼 계속해서 길 찾기를하는 새 지점이 있으며 주 지점은 계속해서 다른 길을 찾습니다. 잠금 해제 된 것처럼 문을 통과하는 지점은 더 이상 AI 에이전트를 찾지 않습니다. 이제 문을 통과하는 데 사용할 수있는 키를 찾고 있습니다. A *의 새로운 휴리스틱은 AI 에이전트와의 거리 대신 키와의 거리 + AI 에이전트와의 거리입니다.

잠금 해제 된 도어 지점에서 키를 찾으면 AI 에이전트를 계속 찾습니다.

사용 가능한 실행 가능한 키가 여러 개인 경우이 솔루션이 조금 더 복잡해 지지만 그에 따라 분기 할 수 있습니다. 분기에는 고정 된 목적지가 있기 때문에 휴리스틱을 사용하여 경로 찾기 (A *)를 최적화 할 수 있으며 잠긴 문 주위에 방법이없는 경우 불가능한 경로는 빨리 차단 될 수 있습니다. 문을 통과하지 못하면 옵션이 빨리 없어지고 문을 통과하고 열쇠를 찾는 지점은 자체적으로 계속됩니다.

물론 다양한 실행 가능한 옵션 (여러 키, 문을 우회하는 다른 항목, 문 주위의 긴 경로)이있는 경우 많은 분기가 유지되어 성능에 영향을줍니다. 그러나 가장 빠른 옵션을 찾아서 사용할 수 있습니다.


실제로

구체적인 예에서 목표에서 시작까지의 경로 찾기 :

  1. 우리는 신속하게 보스 문을 만납니다. 지점 A는 문을 통해 계속되고, 이제 싸우는 보스를 찾고 있습니다. 지점 B는 방에 갇혀 있고 탈출구가 없으면 곧 만료됩니다.

  2. 지점 A가 보스를 찾아 시작을 찾고 있지만 구덩이를 만납니다.

  3. 지점 A는 구덩이를 계속 이어가지 만 이제는 깃털을 찾고 있으며 그에 따라 깃털쪽으로 벌선을 만듭니다. 지점 C는 구덩이 주위의 길을 찾으려고하지만 할 수 없으면 곧 만료됩니다. A * 휴리스틱이 지점 A가 여전히 가장 유망한 것으로 판단되면 잠시 동안 무시됩니다.

  4. 분기 A는 잠긴 문을 만나 잠금이 해제 된 것처럼 잠긴 문을 계속 통과하지만 이제는 열쇠를 찾고 있습니다. 분기 D도 잠긴 문을 통해 계속 깃털을 찾고 있지만 열쇠를 찾습니다. 열쇠 나 깃털을 먼저 찾아야하는지 알 수 없기 때문에 길 찾기와 관련하여 시작은이 문 반대편에있을 수 있습니다. 지점 E는 잠긴 문 주위에서 길을 찾으려고하지만 실패합니다.

  5. 지점 D는 깃털을 빠르게 찾고 열쇠를 계속 찾습니다. 여전히 열쇠를 찾고 있기 때문에 잠긴 문을 다시 통과 할 수 있습니다 (그리고 시간이 지남에 따라 작동합니다). 그러나 일단 열쇠가 있으면 열쇠를 찾기 전에는 잠긴 문을 통과 할 수 없으므로 열쇠를 통과 할 수 없습니다.

  6. 지점 A와 D는 계속 경쟁하지만 지점 A가 열쇠에 도달하면 깃털을 찾고 있으며 잠긴 문을 다시 통과해야하기 때문에 깃털에 도달하지 못합니다. 반면 지점 D는 키에 도달하면 시작 부분에주의를 기울여 합병증없이 찾아냅니다.

  7. 지점 D가 이깁니다. 반대 방향을 찾았습니다. 마지막 경로는 시작-> 키-> 깃털-> 보스-> 목표입니다.


6

편집 : 목표를 탐색하고 발견하기 위해 AI의 관점에서 작성되었으며 키, 잠금 또는 대상의 위치를 ​​미리 알지 못합니다.

먼저 AI에 어떤 종류의 목표가 있다고 가정하십시오. 예를 들어 "보스 찾기"를 예로들 수 있습니다. 그러나 당신은 그것을 이길 원하지만 실제로 그것을 찾는 것입니다. 목표를 달성하는 방법을 전혀 모른다고 가정하십시오. 그리고 그것을 찾으면 알게 될 것입니다. 목표가 달성되면 AI는 문제 해결을 위해 작동을 멈출 수 있습니다.

또한 틈과 깃털 일지라도 "lock"과 "key"라는 일반적인 용어를 사용하겠습니다. 즉, 깃털은 틈새를 "잠금 해제"합니다.

솔루션 접근

기본적으로 미로 탐색기 인 AI로 시작하는 것 같습니다 (지도를 미로로 생각하면). AI의 주요 초점은 모든 장소를 탐색하고 매핑하는 것입니다. 그것은 "항상 내가 보았지만 아직 방문하지 않은 가장 가까운 길로 가십시오"와 같은 단순한 것을 기반으로 할 수 있습니다.

그러나 우선 순위를 변경할 수있는 몇 가지 규칙을 탐색하는 중입니다 ...

  • 이미 동일한 키를 가지고 있지 않으면 찾은 키가 필요합니다.
  • 이전에 본 적이없는 잠금 장치를 발견 한 경우 해당 잠금 장치에서 찾은 모든 키를 시도합니다
  • 키가 새로운 유형의 잠금에서 작동 한 경우 키 유형과 잠금 유형을 기억합니다.
  • 이전에 보았고 키가있는 잠금을 발견 한 경우, 기억 된 키 유형을 사용합니다 (예 : 두 번째 빨간색 잠금 발견, 빨간색 키가 빨간색 잠금 이전에 작동 했으므로 빨간색 키만 사용)
  • 잠금 해제 할 수없는 잠금 위치를 기억합니다.
  • 잠금 해제 한 잠금 위치를 기억할 필요는 없습니다.
  • 키를 발견하고 이전에 잠금 해제 할 수있는 잠금 장치를 알고있을 때마다 잠긴 각 잠금 장치를 즉시 방문하여 새로 찾은 키로 잠금 해제하려고합니다.
  • 경로를 잠금 해제하면 탐색 및 매핑 목표로 되돌아가 새 영역으로의 우선 순위를 지정합니다.

마지막 요점에 대한 메모. 이전에 보았지만 방문하지 않은 미 탐사 영역과 새로 잠금 해제 된 경로 뒤의 미 탐사 영역을 체크 아웃하는 중 선택해야하는 경우 새로 잠금 해제 된 경로를 우선 순위로 만들어야합니다. 아마도 유용한 새 키 (또는 잠금)가있는 곳일 것입니다. 이것은 잠긴 경로가 무의미한 막 다른 골목이 아닐 것이라고 가정합니다.

"잠금 가능"키로 아이디어 확장

다른 키 없이는 가져갈 수없는 키가있을 수 있습니다. 또는 키를 그대로 잠그십시오. 오래된 거대 동굴을 알고 있다면 새를 잡으려면 새장이 필요합니다. 나중에 새 뱀이 필요합니다. 그래서 당신은 새장으로 새를 "잠금 해제"하고 (길을 막지 않지만 새장 없이는 집을 수 없습니다) 새와 함께 뱀을 "잠금 해제"합니다 (경로를 막습니다).

규칙을 추가하면 ...

  • 키를 가져올 수없는 경우 (잠긴 상태) 이미 가지고있는 모든 키를 사용해보십시오
  • 잠금을 해제 할 수없는 키를 찾으면 나중에 기억하십시오
  • 새 키를 찾으면 알려진 모든 잠금 키와 잠금 경로에서 시도하십시오.

나는 특정 열쇠를 가지고 다니면 다른 열쇠의 영향을 어떻게 무시할 수 있는지에 대해 전체적으로 다루지 않을 것입니다. .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.