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