La recherche de chemin standard est assez bonne - vos états sont votre emplacement actuel + votre inventaire actuel. «déménager», c'est soit changer de salle, soit changer d'inventaire. Pas couvert dans cette réponse, mais pas trop d'efforts supplémentaires, est d'écrire une bonne heuristique pour A * - il peut vraiment accélérer la recherche en préférant ramasser les choses plutôt que de s'en éloigner, préférant déverrouiller une porte près de la cible sur la recherche d'un long chemin, etc.
Cette réponse a obtenu beaucoup de votes positifs depuis qu'elle est arrivée en premier et a une démo, mais pour une solution beaucoup plus optimisée et spécialisée, vous devriez également lire la réponse "Faire en arrière est beaucoup plus rapide" /gamedev/ / a / 150155/2624
Preuve de concept Javascript entièrement opérationnelle ci-dessous. Désolé pour la réponse sous forme de vidage de code - j'avais en fait implémenté cela avant d'être convaincu que c'était une bonne réponse, mais cela me semble assez flexible.
Pour commencer en pensant au pathfinding, rappelez-vous que la hiérarchie des algorithmes de pathfinding simples est
- L'étendue de la première recherche est à peu près aussi simple que possible.
- L'algorithme de Djikstra ressemble à la recherche en largeur mais avec des "distances" variables entre les états
- Un * est Djikstras où vous avez un «sens général de la bonne direction» disponible comme une heuristique.
Dans notre cas, le simple encodage d'un «état» comme «emplacement + inventaire» et de «distances» comme «mouvement ou utilisation d'objets» nous permet d'utiliser Djikstra ou A * pour résoudre notre problème.
Voici du code réel illustrant votre niveau d'exemple. Le premier extrait est juste pour comparaison - passez à la deuxième partie si vous voulez voir la solution finale. Nous commençons avec une implémentation de Djikstra qui trouve le chemin correct, mais nous avons ignoré tous les obstacles et les clés. (Essayez-le, vous pouvez le voir juste avant l'arrivée, depuis la salle 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)], []))
Alors, comment ajouter des éléments et des clés à ce code? Simple! au lieu de chaque "état" commencez juste le numéro de la chambre, c'est maintenant un tuple de la salle et notre état d'inventaire:
// 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 }
Les transitions passent désormais d'un tuple (coût, salle) à un tuple (coût, état).
// 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));
};
enfin, nous apportons quelques modifications mineures liées au type à la fonction Djikstra (par exemple, elle correspond toujours à un numéro de salle d'objectif au lieu d'un état complet), et nous obtenons notre réponse complète! Notez que le résultat imprimé va d'abord dans la salle 4 pour récupérer la clé, puis dans la salle 1 pour récupérer la plume, puis dans la salle 6, tue le boss, puis dans la salle 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')], []))
En théorie, cela fonctionne même avec BFS et nous n'avions pas besoin de la fonction de coût pour Djikstra, mais avoir le coût nous permet de dire "récupérer une clé est sans effort, mais combattre un boss est vraiment difficile, et nous préférerions revenir en arrière 100 pas plutôt que de combattre le boss, si on avait le choix ":
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'))