Wegfindung mit Schloss und Schlüssel?

22

Ich arbeite an einem Spiel mit Karten, die Schloss- und Schlüsselrätseln ähneln . Die KI muss zu einem Ziel navigieren, das sich möglicherweise hinter einer verschlossenen roten Tür befindet, aber der rote Schlüssel befindet sich möglicherweise hinter einer verschlossenen blauen Tür usw.

Dieses Puzzle ähnelt einem Dungeon im Zelda-Stil.

Zelda Dungeon

Um zum Ziel zu gelangen, musst du den Boss besiegen. Dazu musst du über die Grube gehen, die Feder einsammeln und den Schlüssel einsammeln

Zelda-Dungeons sind in der Regel linear. Allerdings muss ich das Problem im allgemeinen Fall lösen. So:

  • Für das Ziel kann ein Schlüsselsatz erforderlich sein. Vielleicht müssen Sie entweder den roten oder den blauen Schlüssel holen. Oder es könnte eine unverschlossene Tür in der Ferne geben!
  • Es könnte mehrere Türen und Schlüssel einer Art geben. Es können sich beispielsweise mehrere rote Schlüssel auf der Karte befinden. Wenn Sie einen sammeln, erhalten Sie Zugriff auf alle roten Türen.
  • Das Ziel ist möglicherweise nicht erreichbar, da sich die richtigen Schlüssel hinter verschlossenen Türen befinden

Wie würde ich auf einer solchen Karte eine Wegfindung durchführen? Wie würde das Suchdiagramm aussehen?

Hinweis: Der letzte Punkt zum Erkennen unzugänglicher Ziele ist wichtig. Ein * ist beispielsweise äußerst ineffizient, wenn das Ziel nicht erreichbar ist. Damit möchte ich effizient umgehen.

Angenommen, die KI weiß, wo sich alles auf der Karte befindet.

congusbongus
quelle
4
Kennt und entdeckt die KI Dinge erst, wenn sie sie freigeschaltet hat? Weiß es zum Beispiel, dass sich die Feder hinter der verschlossenen Tür befindet? Versteht die KI Konzepte wie "Das ist ein Schloss, also brauche ich einen Schlüssel" oder etwas Einfacheres wie "Ich habe etwas, das mir den Weg versperrt, also probiere alle Dinge aus, die ich darauf gefunden habe. Feder an der Tür? Nein. Schlüssel an der Tür? Ja! "
Tim Holt
1
Zuvor wurde dieses Problem in der Frage nach der Pfadfindung vorwärts und rückwärts erörtert , was für Sie von Nutzen sein könnte.
DMGregory
1
Sie versuchen also nicht, einen Spieler zu simulieren, sondern einen optimierten Dungeonlauf zu erstellen? Meine Antwort war definitiv die Simulation eines Spielerverhaltens.
Tim Holt
4
Leider ist es ziemlich schwierig, ein unzugängliches Ziel zu erkennen. Der einzige Weg, um sicherzugehen , dass es keinen Weg gibt, das Ziel zu erreichen, besteht darin, den gesamten erreichbaren Raum zu erkunden, um sicherzustellen, dass keiner ein Ziel enthält. Genau das, was A * bewirkt, macht es so viele zusätzliche Schritte, wenn das Ziel erreicht ist nicht zugänglich. Bei jedem Algorithmus, der weniger Speicherplatz durchsucht, besteht die Gefahr, dass ein verfügbarer Pfad zum Ziel fehlt, da sich der Pfad in einem Teil des Speicherplatzes versteckt hat, der durchsucht wurde. Sie können dies beschleunigen, indem Sie auf einer höheren Ebene arbeiten und anstelle jedes Kachels oder Navmesh-Polygons das Diagramm der Raumverbindungen durchsuchen.
DMGregory
1
Offtopic, dachte ich instinktiv an Chip's Challenge statt an Zelda :)
Flater

Antworten:

22

Die Standardpfadfindung ist gut genug - Ihre Bundesstaaten sind Ihr aktueller Standort und Ihr aktueller Bestand. "Umziehen" bedeutet entweder, Räume zu wechseln oder das Inventar zu wechseln. Nicht behandelt in dieser Antwort, aber nicht zu viel zusätzlicher Aufwand, ist das Schreiben einer guten Heuristik für A * - es kann die Suche wirklich beschleunigen, indem es bevorzugt, Dinge aufzuheben, anstatt sich von ihr wegzubewegen, und bevorzugt, eine Tür in der Nähe des Ziels aufzuschließen über die Suche nach einem weiten Weg, etc.

Diese Antwort hat viele positive Stimmen erhalten, seitdem sie auf den Markt kam, und verfügt über eine Demo. Für eine viel optimierte und spezialisierte Lösung sollten Sie jedoch auch die Antwort /gamedev/ lesen / a / 150155/2624


Vollständig funktionsfähiger Javascript Proof of Concept (siehe unten). Entschuldigen Sie die Antwort als Code-Dump - ich hatte sie tatsächlich implementiert, bevor ich überzeugt war, dass sie eine gute Antwort ist, aber sie scheint mir ziemlich flexibel zu sein.

Denken Sie beim Überlegen der Pfadfindung zunächst daran, dass die Hierarchie der einfachen Algorithmen zur Pfadfindung wie folgt lautet:

  • Die Breitensuche ist so einfach wie möglich.
  • Der Djikstra-Algorithmus ähnelt der Breitensuche, jedoch mit unterschiedlichen "Abständen" zwischen den Zuständen
  • Ein * ist Djikstras, bei dem Sie einen allgemeinen Sinn für die richtige Richtung als Heuristik haben.

In unserem Fall können wir Djikstra oder A * verwenden, um unser Problem zu lösen, indem wir lediglich einen "Zustand" als "Ort + Inventar" und "Entfernungen" als "Bewegung oder Verwendung von Gegenständen" codieren.

Hier ist ein aktueller Code, der Ihre Beispielstufe demonstriert. Das erste Snippet dient nur zum Vergleich - springen Sie zum zweiten Teil, wenn Sie die endgültige Lösung sehen möchten. Wir beginnen mit einer Djikstra-Implementierung, die den richtigen Pfad findet, aber wir haben alle Hindernisse und Schlüssel ignoriert. (Probieren Sie es aus, Sie können sehen, dass es nur Linien für das Ziel sind, von Raum 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)], []))

Wie fügen wir diesem Code also Elemente und Schlüssel hinzu? Einfach! Anstelle jedes "Zustandes" beginnt nur die Raumnummer, es ist jetzt ein Tupel des Raumes und unser Inventarzustand:

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

Die Übergänge ändern sich nun von einem (Kosten-, Raum-) Tupel zu einem (Kosten-, Zustands-) Tupel, sodass sowohl das Verschieben in einen anderen Raum als auch das Aufnehmen eines Gegenstands codiert werden können.

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

Schließlich nehmen wir einige geringfügige typbezogene Änderungen an der Djikstra-Funktion vor (z. B. stimmt sie immer noch nur mit einer Zielraumnummer statt mit einem vollständigen Status überein), und wir erhalten unsere vollständige Antwort! Beachten Sie, dass das gedruckte Ergebnis zuerst zu Raum 4 geht, um den Schlüssel aufzuheben, dann zu Raum 1, um die Feder aufzuheben, und dann zu Raum 6, um den Boss zu töten und dann zu Raum 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')], []))

Theoretisch funktioniert dies sogar mit BFS, und wir haben die Kostenfunktion für Djikstra's nicht benötigt, aber wenn wir die Kosten haben, können wir sagen: "Einen Schlüssel abzuholen ist mühelos, aber gegen einen Boss zu kämpfen ist wirklich schwer, und wir wollen lieber zurück 100 Schritte statt gegen den Boss zu kämpfen, wenn wir die Wahl hätten ":

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'))
Jimmy
quelle
Ja, die Aufnahme von Inventar / Schlüsselstatus in das Suchdiagramm ist eine Lösung. Ich bin jedoch besorgt über den erhöhten Platzbedarf - eine Karte mit 4 Schlüsseln benötigt das 16-fache des Platzes eines Diagramms ohne Schlüssel.
congusbongus
8
@congusbongus Willkommen zum NP-vollständigen Problem des Handlungsreisenden. Es gibt keine allgemeine Lösung, die das in der Polynomzeit löst.
Ratschenfreak
1
@congusbongus Ich denke im Allgemeinen nicht, dass Ihr Suchdiagramm so viel Aufwand verursachen wird, aber wenn Sie sich Gedanken über Speicherplatz machen, packen Sie einfach Ihre Daten - Sie könnten 24-Bit für die Raumanzeige verwenden (16 Millionen Räume sollten genug für alle) und jeweils ein bisschen für die Gegenstände, die Sie als Tore verwenden möchten (bis zu 8 einzigartige). Wenn Sie Lust auf Phantasie haben, können Sie Abhängigkeiten verwenden, um Elemente in noch kleinere Bits zu packen. Verwenden Sie also dasselbe Bit für "key" und "boss", da es eine indirekte transitiven Abhängigkeit gibt
Jimmy
@ Jimmy Auch wenn es nicht persönlich ist, ich freue mich über die Erwähnung meiner Antwort :)
Jibb Smart
13

Rückwärts A * erledigt den Trick

Wie in dieser Antwort auf die Frage nach der Vorwärts- und Rückwärtspfadfindung erörtert , ist die Rückwärtspfadfindung eine relativ einfache Lösung für dieses Problem. Dies funktioniert sehr ähnlich wie GOAP (Goal Oriented Action Planning), bei dem effiziente Lösungen geplant und ziellose Fragen minimiert werden.

Am Ende dieser Antwort habe ich eine Aufschlüsselung, wie es mit dem von Ihnen angegebenen Beispiel umgeht.

Im Detail

Wegfindung vom Ziel zum Start. Wenn Sie bei Ihrer Wegfindung auf eine verschlossene Tür stoßen, haben Sie einen neuen Zweig zu Ihrer Wegfindung, der durch die Tür verläuft, als wäre sie nicht verschlossen, und der Hauptzweig sucht weiter nach einem anderen Pfad. Der Zweig, der durch die Tür geht, als wäre er unverschlossen, sucht nicht mehr nach dem KI-Agenten - er sucht jetzt nach einem Schlüssel, mit dem er die Tür passieren kann. Bei A * ist die neue Heuristik die Entfernung zum Schlüssel + Entfernung zum KI-Agenten, anstatt nur die Entfernung zum KI-Agenten.

Wenn der Zweig mit der nicht gesperrten Tür den Schlüssel findet, sucht er weiter nach dem KI-Agenten.

Diese Lösung wird etwas komplizierter, wenn mehrere funktionsfähige Schlüssel verfügbar sind, Sie können jedoch entsprechend verzweigen. Da die Zweige ein festes Ziel haben, können Sie dennoch eine Heuristik verwenden, um die Pfadfindung (A *) zu optimieren, und unmögliche Pfade werden hoffentlich schnell abgeschnitten - wenn kein Weg an der verschlossenen Tür vorbei führt, führt der Zweig nicht Wenn Sie nicht durch die Tür gehen, gehen Ihnen die Optionen schnell aus und der Zweig, der durch die Tür geht und nach dem Schlüssel sucht, geht von alleine weiter.

Wenn eine Vielzahl praktikabler Optionen zur Verfügung steht (mehrere Schlüssel, andere Elemente zur Umgehung der Tür, langer Weg um die Tür), werden natürlich viele Zweige beibehalten, die die Leistung beeinträchtigen. Sie finden aber auch die schnellste Option und können diese verwenden.


In Aktion

In Ihrem konkreten Beispiel: Wegfindung vom Ziel zum Start:

  1. Wir stoßen schnell auf eine Cheftür. Zweig A geht weiter durch die Tür und sucht nach einem Boss zum Kämpfen. Zweig B bleibt im Raum stecken und verfällt bald, wenn er keinen Ausweg findet.

  2. Ast A findet den Boss und sucht nun den Start, trifft aber auf eine Grube.

  3. Zweig A geht weiter über die Grube, sucht nun aber nach der Feder und zieht dementsprechend eine Bienenlinie in Richtung der Feder. Zweig C wird erstellt, der versucht, sich in der Grube zurechtzufinden, verfällt jedoch, sobald er nicht mehr in der Lage ist. Das oder es wird für eine Weile ignoriert, wenn Ihre A * -Heuristik feststellt, dass Zweig A immer noch sehr vielversprechend aussieht.

  4. Zweig A trifft auf die verschlossene Tür und geht durch die verschlossene Tür, als wäre sie unverschlossen, aber jetzt sucht er nach dem Schlüssel. Zweig D geht ebenfalls durch die verschlossene Tür und sucht immer noch nach der Feder, aber dann sucht er nach dem Schlüssel. Dies liegt daran, dass wir nicht wissen, ob wir zuerst den Schlüssel oder die Feder finden müssen. In Bezug auf die Wegfindung befindet sich der Start möglicherweise auf der anderen Seite dieser Tür. Zweig E versucht, einen Weg um die verschlossene Tür zu finden, und schlägt fehl.

  5. Zweig D findet schnell die Feder und sucht weiter nach dem Schlüssel. Es darf wieder durch die verschlossene Tür gehen, da es immer noch nach dem Schlüssel sucht (und sich in der Zeit rückwärts bewegt). Aber sobald es den Schlüssel hat, kann es die verschlossene Tür nicht passieren (da es die verschlossene Tür nicht passieren konnte, bevor es den Schlüssel gefunden hat).

  6. Zweig A und D konkurrieren weiter, aber wenn Zweig A den Schlüssel erreicht, sucht er nach der Feder und kann die Feder nicht erreichen, weil sie wieder durch die verschlossene Tür gehen muss. Zweig D hingegen richtet seine Aufmerksamkeit beim Erreichen des Schlüssels auf den Start und findet ihn ohne Komplikationen.

  7. Zweig D gewinnt. Es hat den umgekehrten Weg gefunden. Der letzte Pfad ist: Start -> Schlüssel -> Feder -> Boss -> Ziel.

Jibb Smart
quelle
6

Bearbeiten : Dies ist aus der Sicht einer KI geschrieben, die darauf aus ist, ein Ziel zu erkunden und zu entdecken, und die die Position von Schlüsseln, Schlössern oder Zielen nicht im Voraus kennt.

Nehmen Sie zunächst an, dass die KI eine Art Gesamtziel hat. ZB "Find the Boss" in Ihrem Beispiel. Ja, du willst es schlagen, aber es geht wirklich darum, es zu finden. Angenommen, es hat keine Ahnung, wie es zum Ziel kommt, nur, dass es existiert. Und es wird es wissen, wenn es es findet. Sobald das Ziel erreicht ist, kann die KI aufhören, an der Lösung des Problems zu arbeiten.

Außerdem werde ich hier den Oberbegriff "Schloss" und "Schlüssel" verwenden, auch wenn es eine Kluft und eine Feder sein könnte. Das heißt, Feder "öffnet" den Abgrund "Sperre".

Lösungsansatz

Es scheint, als würdest du zuerst mit einer KI beginnen, die im Grunde genommen ein Labyrinthforscher war (wenn du deine Karte als Labyrinth ansiehst). Das Erkunden und Kartieren aller möglichen Orte wäre der Hauptfokus der KI. Es könnte lediglich auf etwas Einfachem basieren, wie "Gehe immer zu dem nächsten Pfad, den ich gesehen, aber noch nicht besucht habe."

Während der Erkundung treten jedoch einige Regeln in Kraft, die die Priorität ändern könnten ...

  • Es würde jeden gefundenen Schlüssel benötigen, es sei denn, er hatte bereits den gleichen Schlüssel
  • Wenn es ein Schloss fand, das es noch nie gesehen hatte, würde es jeden Schlüssel ausprobieren, den es auf diesem Schloss gefunden hatte
  • Wenn ein Schlüssel mit einem neuen Schloßtyp funktioniert, werden der Schlüsseltyp und der Schloßtyp gespeichert
  • Wenn es ein Schloss gefunden hat, das es zuvor gesehen hat und über den Schlüssel verfügt, wird der gespeicherte Schlüsseltyp verwendet (z. B. zweites rotes Schloss gefunden, roter Schlüssel hat vor dem roten Schloss funktioniert, also nur roten Schlüssel verwenden).
  • Es würde sich an die Position eines Schlosses erinnern, das es nicht öffnen konnte
  • Es musste sich nicht an die Position der entsperrten Schlösser erinnern
  • Jedes Mal, wenn ein Schlüssel gefunden wurde und zuvor entsperrbare Schlösser bekannt waren, wurde jedes dieser gesperrten Schlösser sofort aufgesucht und versucht, sie mit dem neu gefundenen Schlüssel zu entsperren
  • Immer wenn ein Pfad freigeschaltet wurde, kehrte er einfach zum Erkundungs- und Kartierungsziel zurück und priorisierte das Betreten des neuen Bereichs

Ein Hinweis zu diesem letzten Punkt. Wenn es zwischen dem Auschecken eines nicht erkundeten Bereichs, den es zuvor gesehen (aber nicht besucht) hat, und einem nicht erkundeten Bereich hinter einem neu entsperrten Pfad wählen muss, sollte der neu entsperrte Pfad die Priorität haben. Dort gibt es wahrscheinlich neue Schlüssel (oder Schlösser), die nützlich sein werden. Dies setzt voraus, dass ein gesperrter Pfad wahrscheinlich keine sinnlose Sackgasse ist.

Erweiterung der Idee mit "abschließbaren" Schlüsseln

Sie könnten möglicherweise Schlüssel haben, die ohne einen anderen Schlüssel nicht genommen werden können. Oder sozusagen gesperrte Schlüssel. Wenn Sie Ihre alten Kolossalen Höhlen kennen, benötigen Sie den Vogelkäfig, um den Vogel zu fangen - den Sie später für eine Schlange benötigen. Sie "entsperren" also den Vogel mit dem Käfig (der den Weg nicht blockiert, aber ohne den Käfig nicht aufgenommen werden kann) und "entsperren" dann die Schlange (die Ihren Weg blockiert) mit dem Vogel.

Also einige Regeln hinzufügen ...

  • Wenn ein Schlüssel nicht genommen werden kann (er ist gesperrt), versuchen Sie es mit jedem Schlüssel, den Sie bereits haben
  • Wenn Sie einen Schlüssel finden, den Sie nicht entsperren können, merken Sie sich diesen für später
  • Wenn Sie einen neuen Schlüssel finden, probieren Sie ihn auf jedem bekannten gesperrten Schlüssel sowie auf jedem gesperrten Pfad aus

Ich werde mich nicht einmal damit befassen, wie das Tragen eines bestimmten Schlüssels die Wirkung eines anderen Schlüssels zunichte machen könnte (Colossal Caves, Rute erschreckt den Vogel und muss fallen gelassen werden, bevor der Vogel aufgenommen werden kann, wird aber später benötigt, um eine magische Brücke zu bauen). .

Tim Holt
quelle