Kilit ve anahtar ile yol bulma?


22

Kilit ve kilit bulmacalara benzeyen haritalar içeren bir oyun üzerinde çalışıyorum . AI'nın kilitli bir kırmızı kapının arkasında olabilecek bir hedefe gitmesi gerekir, ancak kırmızı anahtar kilitli bir mavi kapının arkasında olabilir, vb.

Bu yap-boz, Zelda tarzı bir zindana benzer, bu resimdeki gibi:

Zelda zindanı

Hedefe ulaşmak için, Anahtarın toplanmasını gerektiren Tüylerin toplanmasını gerektiren çukurun üzerinden geçmeyi gerektiren Patronu yenmelisin.

Zelda zindanları doğrusal olma eğilimindedir. Ancak genel durumdaki sorunu çözmem gerekiyor. Yani:

  • Hedef, bir anahtar setinden birini gerektirebilir. Yani belki kırmızı tuşa ya da mavi tuşa ihtiyacın var. Ya da uzun bir süre açık bir kapı açılabilir!
  • Bir çeşit çoklu kapı ve anahtar olabilir. Örneğin, haritada birden fazla kırmızı tuş bulunabilir ve bir tanesinin toplanması tüm kırmızı kapılara erişim sağlar.
  • Hedef, erişilemez olabilir çünkü sağ tuşlar kilitli kapıların arkasında

Böyle bir haritada yol bulma işlemini nasıl yaparım? Arama grafiği nasıl görünür?

Not: Ulaşılamaz hedeflerin tespitinde son nokta önemlidir; Mesela, A *, hedefe ulaşılamıyorsa son derece verimsizdir. Bununla verimli bir şekilde ilgilenmek isterim.

AI'nın haritada her şeyin nerede olduğunu bildiğini varsayın.


4
AI, yalnızca bir şeyleri açtıktan sonra biliyor mu ve keşfediyor mu? Örneğin, tüyün kilitli kapının arkasında olduğunu biliyor mu? AI, "Bu bir kilit, bu yüzden bir anahtara ihtiyacım var" veya daha basit bir şey gibi, "Yolumu tıkayan bir şey var, bu yüzden üzerinde bulduğum her şeyi deneyin." Kapıdaki anahtar? Evet! "
Tim Holt,

1
Bu soruda , bu sorunun sizin için yararlı olabilecek ileriye doğru geriye doğru yol bulma ile ilgili bazı tartışmaları oldu .
DMGregory

1
Yani bir oyuncuyu simüle etmeye çalışmıyorsun ama optimize edilmiş bir zindan koşusu yaratmaya çalışıyorsun? Cevabım kesinlikle bir oyuncu davranışını taklit etmekti.
Tim Holt

4
Ne yazık ki erişilemez bir hedefi tespit etmek oldukça zor. Hedefe ulaşmanın bir yolu olmadığından emin olmanın tek yolu, hiçbirinin bir hedef içermediğinden emin olmak için ulaşılabilir alanın tamamını araştırmaktır - ki bu, A * hedefine göre daha fazla adım atmasını sağlayan şeydir. ulaşılmaz. Daha az yer arayan herhangi bir algoritma, hedefe uygun bir yolun eksik olması riskini taşır çünkü yol aramayı atladığı alanın bir kısmında saklanıyordu. Bunu, her karo veya navmesh poligonu yerine oda bağlantılarının grafiğini arayarak daha yüksek bir seviyede çalışarak hızlandırabilirsiniz.
DMGregory

1
Offtopic, içgüdüsel olarak Zelda yerine Chip'in Mücadelesini düşündüm :)
Flater

Yanıtlar:


22

Standart yol bulma yeterlidir - eyaletleriniz mevcut konumunuz + mevcut envanterinizdir. "Hareketli" ya oda değiştirmek ya da envanter değiştirmek. Bu cevapta ele alınmayan, ancak fazladan fazla çaba göstermeyen, A * için iyi bir buluşsal yazı yazıyor - hedefin yakınında bir kapının kilidini açmayı tercih ederek, ondan uzaklaşırken bir şeyleri seçmeyi tercih ederek aramayı gerçekten hızlandırabilir etrafta uzun bir yol aramak vb.

Bu cevap ilk geldiği ve bir demosu olduğu için çok fazla yükseldi, ancak çok daha optimize edilmiş ve uzmanlaşmış bir çözüm için, "Geriye doğru yapmak çok daha hızlı" yanıtını da okumalısınız. Https://gamedev.stackexchange.com / a / 150155/2624


Tamamen operasyonel Javascript aşağıdaki kavram kanıtı. Bir kod dökümü olarak cevap için özür dilerim - bunu iyi bir cevap olduğuna ikna etmeden önce uygulamıştım, ancak bana oldukça esnek görünüyor.

Yol bulma hakkında düşünürken başlamak için, basit yol bulma algoritmalarının heirarşisinin:

  • Genişlik İlk Arama alabildiğin kadar basit.
  • Djikstra'nın Algoritması, Breadth First Search'e benziyor ancak eyaletler arasında değişken "mesafeler" var
  • Y *, bir sezgisel olarak mevcut olan 'doğru yöne genel bir anlayışa sahip olduğunuz' Djikstralardır.

Bizim durumumuzda, sadece bir "durumu" bir "konum + envanter" olarak ve "mesafeleri" bir "hareket veya madde kullanımı" olarak kodlamak, sorunumuzu çözmek için Djikstra veya A * kullanmamıza izin verir.

Örnek seviyenizi gösteren bazı gerçek kodlar. İlk snippet sadece karşılaştırma için - son çözümü görmek istiyorsanız ikinci bölüme atlayın. Djikstra'nın doğru yolu bulan uygulamasıyla başlıyoruz, ancak tüm engelleri ve anahtarları görmezden geldik. (Deneyin, sadece finiş için beelines görebilirsiniz, oda 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)], []))

Peki bu koda nasıl eşya ve anahtar ekleriz? Basit! Her “devlet” yerine sadece oda numarası başlar, o şimdi odanın bir parçası ve envanter durumumuz:

 // 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 }

Geçişler artık bir (maliyet, oda) dizisinden bir (maliyet, durum) dizisine değişmektedir, bu yüzden hem "başka bir odaya taşınmak" hem de "bir öğe almak" ı kodlayabilir

// 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));
};

Son olarak, Djikstra işlevinde bazı küçük tiplerle ilgili değişiklikler yapıyoruz (örneğin, hala tam bir durum yerine bir hedef oda numarasıyla eşleşiyor) ve tam cevabımızı alıyoruz! Basılı sonucun anahtarı almak için önce oda 4'e, ardından tüyü almak için oda 1'e, sonra oda 6'ya, patronu öldürür, sonra oda 5'e gittiğine dikkat edin.

// 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')], []))

Teorik olarak, bu BFS ile bile çalışıyor ve Djikstra'nın maliyet işlevine ihtiyacımız yoktu, ancak maliyete sahip olmak "anahtar teslim almak zahmetsiz, ancak bir patronla savaşmak gerçekten zor, ve geri dönmek istiyoruz" dememize izin veriyor Seçim şansımız olsa patronla savaşmak yerine 100 adım attı ":

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'))

Evet, arama grafiğindeki envanter / anahtar durumu dahil bir çözümdür. Yine de artan alan gereksinimlerinden endişe duyuyorum - 4 anahtarlı bir harita, anahtarsız bir grafiğin 16 katını gerektirir.
congusbongus

8
@congusbongus NP komple seyahat eden satıcı sorununa hoş geldiniz. Polinom zamanında bunu çözecek genel bir çözüm yoktur.
cırcır ucube

1
@congusbongus Genelde, arama grafiğinizin çok üstünde kalacağını sanmıyorum, ancak alanla ilgileniyorsanız, verilerinizi toplayın - oda göstergesi için 24 bit kullanabilirsiniz (16 milyon oda, herkese yetecek kadar) ve her biri için geçit olarak kullandığınız eşyalar için biraz olsun (en fazla 8 benzersiz). Eğer fantezi almak istiyorsanız, öğeleri daha da küçük parçalara paketlemek için bağımlılıkları kullanabilirsiniz, yani dolaylı bir geçişli dpendency olduğundan "key" ve "boss" için aynı biti kullanın
Jimmy

@Jimmy Kişisel olmasa da, cevabımın sözünü takdir ediyorum :)
Jibb Smart

13

Geriye doğru bir * hile yapacak

Bu cevapta ileriye doğru geriye doğru yol bulma hakkındaki bir soruya cevap olarak tartışıldığı gibi geriye doğru yol bulma bu soruna nispeten basit bir çözümdür. Bu, GOAP'a (Hedefe Yönelik Eylem Planlaması) benzer şekilde çalışır ve amaçsız merakları en aza indirirken verimli çözümler planlar.

Bu cevabın en altında, verdiğiniz örneği nasıl ele aldığına dair bir analiz var.

Detayda

Hedeften başlangıca giden yol. Yol bulmanızda kilitli bir kapıyla karşılaşırsanız, ana kol başka bir yol aramaya devam ederken, ana yolla etrafa bakmaya devam ederken, kapıdan içeri girmiş gibi devam eden yeni bir kolunuz olur. Kapıdan açılmış gibi devam eden şube artık AI ajanını aramıyor - şimdi kapıdan geçmek için kullanabileceği bir anahtar arıyor. A * ile yeni sezgisel özelliği, yalnızca AI aracısına uzaklık yerine, tuşa + AI aracısına olan uzaklıktır.

Kilitlenmemiş kapı kolu anahtarı bulursa, AI aracısını aramaya devam eder.

Kullanılabilir çoklu anahtar bulunduğunda bu çözüm biraz daha karmaşık hale gelir, ancak buna göre dallanabilirsiniz. Dalların sabit bir hedefi olduğundan, yol bulmayı (A *) optimize etmek için bir sezgisel tarama kullanmanıza izin verir ve imkansız yollar umarım hızlı bir şekilde kesilir - kilitli kapının etrafında bir yol yoksa, Kapıdan geçmeden hızlı bir şekilde seçenek tükenir ve kapıdan geçen ve anahtarı arayan dal kendi kendine devam eder.

Tabii ki, çeşitli uygulanabilir seçenekler mevcut olduğunda (birçok anahtar, kapıyı çevreleyen diğer öğeler, kapının etrafındaki uzun yol), performansı etkileyen birçok dal korunacaktır. Ancak aynı zamanda en hızlı seçeneği bulacaksınız ve bunu kullanabileceksiniz.


Eylemde

Özel örneğinizde, Hedeften Başlamaya kadar yol bulma:

  1. Patron kapısıyla hemen karşılaşırız. Şube A kapıdan devam ediyor, şimdi savaşacak bir patron arıyor. B Şubesi odanın içinde sıkışmış halde kalır ve çıkışın bulunmadığını bulduğunda yakında sona erecektir.

  2. Şube A, patronu bulur ve şimdi de Başlangıç'ı arar, ancak bir çukurla karşılaşır.

  3. Şube A, çukur üzerinde devam eder, ancak şimdi kuş tüyü arar ve kuş tüyüne doğru bir arı çizgisi oluşturur. Çukurun etrafında bir yol bulmaya çalışan, ancak C yapamadığı zaman sona erecek olan Şube yaratılır. Eğer A * sezgiseliniz Şube A'nın hala en umut verici göründüğünü bulursa, bu, ya da bir süre göz ardı edilir.

  4. Şube A, kilitli kapıyla karşılaşır ve kilitli kapıdan kilidi açılmış gibi devam eder, ancak şimdi anahtarı arar. Şube D, kilitli kapıdan da geçerek hala tüyü aramaya devam eder, fakat sonra anahtarı arayacaktır. Bunun sebebi, önce anahtarı veya tüyü bulmamız gerekip gerekmediğini bilmememizdir ve yol bulma ile ilgili olarak, Başlat bu kapının diğer tarafında olabilir. E Şubesi kilitli kapının etrafında bir yol bulmaya çalışır ve başarısız olur.

  5. Şube D hızla tüyü bulur ve anahtarı aramaya devam eder. Yine anahtarı aradığı için kilitli kapıdan tekrar geçmesine izin veriliyor (ve zamanda geriye doğru çalışıyor). Ancak bir kez anahtarı bulunduğunda, kilitli kapıdan geçemez (çünkü anahtarı bulunmadan önce kilitli kapıdan geçemez).

  6. Şube A ve D rekabet etmeye devam eder, ancak Şube A anahtara ulaştığında, kuş tüyü arar ve tekrar kilitli kapıdan geçmesi gerektiğinden kuş tüyüne ulaşamaz. Şube D ise, anahtara ulaştıktan sonra dikkatini Başlat'a çevirir ve komplikasyonsuz bulur.

  7. D Şubesi kazanır. Ters yolunu buldu. Son yol şudur: Başlat -> Anahtar -> Tüy -> Patron -> Hedef.


6

Düzenleme : Bu, bir hedefi araştırmak ve keşfetmek için çıkan bir AI bakış açısıyla yazılmıştır ve zamanından önce anahtarların, kilitlerin veya hedeflerin konumunu bilmez.

İlk olarak, AI'nın bir tür genel amacı olduğunu varsayalım. Örneğin, örneğinde "Patronu bul". Evet, onu yenmek istiyorsun, ama gerçekten onu bulmakla ilgili. Hedefe nasıl ulaşılacağı hakkında hiçbir fikri olmadığını, sadece var olduğunu varsayalım. Ve onu bulduğunda bilecek. Hedef karşılandığında, AI sorunu çözmek için çalışmayı durdurabilir.

Ayrıca, burada "kilit" ve "anahtar" terimlerini, bir uçurum ve kuş tüyü olsa bile kullanacağım. Yani, tüy "uçurum" kilidini "açar".

Çözüm Yaklaşımı

Öncelikle temelde bir labirent gezgini olan bir AI ile başlayacaksınız gibi görünüyor (haritanızı bir labirent olarak düşünüyorsanız). Gidebileceği tüm yerleri araştırmak ve haritalamak, AI'nın odak noktası olacaktır. "Her zaman gördüğüm en yakın yola git, ancak henüz ziyaret etme" gibi basit bir şeye dayanabilir.

Ancak, önceliği değiştirebilecek olanı araştırırken bazı kurallara uyulur ...

  • Zaten aynı anahtara sahip olmadıkça, bulduğu herhangi bir anahtarı alırdı.
  • Daha önce hiç görmediği bir kilit bulduysa, o kilitte bulduğu her anahtarı denerdi.
  • Bir anahtar yeni bir kilit türünde çalıştıysa, bu anahtar türünü ve kilit türünü hatırlardı.
  • Daha önce gördüğü ve anahtarı olduğu bir kilit bulduysa, hatırlanan anahtar türünü kullanırdı (örneğin, ikinci kırmızı kilit bulundu, kırmızı tuş daha önce kırmızı kilit üzerinde çalışıyordu, bu yüzden sadece kırmızı tuş kullanıldı)
  • Kilidini açamayacağı herhangi bir kilit konumunu hatırlardı
  • Kilidi açılmış olan kilitlerin konumunu hatırlamak gerekmez.
  • Bir anahtar bulduğunda ve daha önce açılabilir kilitleri bildiğinde, kilitli kilitlerin her birini hemen ziyaret eder ve yeni bulunan anahtarla kilidini açmayı denerdi.
  • Ne zaman bir yol açsa, keşfe ve haritalama hedefine geri dönecek ve yeni alana adım atmaya öncelik verecek

Bu son noktada bir not. Yeni açılmış bir yolun arkasındaki keşfedilmemiş bir alana karşı daha önce görülen (ancak ziyaret edilmemiş) keşfedilmemiş bir alanı kontrol etmek arasında seçim yapmak zorunda kalırsa, yeni açılmış yolu öncelikli yapmalıdır. Muhtemelen faydalı olacak yeni anahtarların (veya kilitlerin) olduğu yer burasıdır. Bu, kilitli bir yolun muhtemelen anlamsız bir çıkmaz olmayacağını varsayar.

"Kilitlenebilir" Tuşlarla Fikri Genişletmek

Potansiyel olarak başka bir anahtar olmadan alınamayan anahtarlara sahip olabilirsiniz. Ya da kilitli anahtarları olduğu gibi. Eski Devasa Mağaralarınızı biliyorsanız, kuşu yakalamak için kuş kafesine sahip olmalısınız - ki daha sonra bir yılan için ihtiyacınız olacak. Böylece, kuşun kafesiyle kilidini açarsın (bu, yolu engellemez ancak kafes olmadan alınamaz), ve daha sonra kuşu (yolunu tıkayan) yılanı "kilidini" açar.

Yani bazı kurallar eklemek ...

  • Bir anahtar alınamıyorsa (kilitliyse), üzerinde bulunan her anahtarı deneyin
  • Kilidi açamayacağınız bir anahtar bulursanız, daha sonra
  • Yeni bir anahtar bulursanız, gidip bilinen her kilitli tuşun yanı sıra kilitli yolda da deneyin.

Belirli bir anahtar taşımanın başka bir anahtarın etkisini nasıl olumsuz etkileyebileceği ile ilgili her şeye girmeyeceğim (Devasa Mağaralar, çubuk kuşu korkutuyor ve kuş alınmadan önce bırakılması gerekiyor, ancak daha sonra büyülü köprü oluşturmak için gerekli) .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.