La búsqueda de ruta estándar es suficiente : sus estados son su ubicación actual + su inventario actual. "mudarse" es cambiar de habitación o cambiar el inventario. No está cubierto en esta respuesta, pero no es un esfuerzo adicional, es escribir una buena heurística para A *: realmente puede acelerar la búsqueda al preferir recoger cosas en lugar de alejarse, prefiriendo desbloquear una puerta cerca del objetivo sobre buscar un largo camino, etc.
Esta respuesta ha recibido muchos votos positivos desde que llegó primero y tiene una demostración, pero para una solución mucho más optimizada y especializada, también debe leer la respuesta "Hacerlo al revés es mucho más rápido" /gamedev/ / a / 150155/2624
Prueba de concepto Javascript completamente operativa a continuación. Perdón por la respuesta como un volcado de código: en realidad había implementado esto antes de convencerme de que era una buena respuesta, pero me parece bastante flexible.
Para comenzar cuando piense en la búsqueda de rutas, recuerde que la jerarquía de los algoritmos de búsqueda de rutas simples es:
- Breadth First Search es lo más simple posible.
- El algoritmo de Djikstra es como Breadth First Search pero con diferentes "distancias" entre estados
- A * es Djikstras donde tiene un 'sentido general de la dirección correcta' disponible como heurístico.
En nuestro caso, simplemente codificar un "estado" como "ubicación + inventario" y "distancias" como "movimiento o uso de elementos" nos permite usar Djikstra o A * para resolver nuestro problema.
Aquí hay un código real que demuestra su nivel de ejemplo. El primer fragmento es solo para comparación: salte a la segunda parte si desea ver la solución final. Comenzamos con una implementación de Djikstra que encuentra el camino correcto, pero hemos ignorado todos los obstáculos y claves. (Pruébelo, puede verlo solo para el final, desde la habitación 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)], []))
Entonces, ¿cómo agregamos elementos y claves a este código? ¡Simple! en lugar de que cada "estado" comience solo con el número de habitación, ahora es una tupla de la habitación y nuestro estado de inventario:
// 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 }
Las transiciones ahora cambian de ser una tupla (costo, habitación) a una tupla (costo, estado), por lo que pueden codificar tanto "mudarse a otra habitación" como "recoger un artículo"
// 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));
};
finalmente, realizamos algunos cambios menores relacionados con el tipo en la función Djikstra (por ejemplo, todavía coincide con un número de sala de meta en lugar de un estado completo), ¡y obtenemos nuestra respuesta completa! Tenga en cuenta que el resultado impreso primero va a la sala 4 para recoger la llave, luego va a la sala 1 para recoger la pluma, luego va a la sala 6, mata al jefe y luego a la sala 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 teoría, esto funciona incluso con BFS y no necesitábamos la función de costo para Djikstra, pero tener el costo nos permite decir "recoger una llave es fácil, pero luchar contra un jefe es realmente difícil, y preferimos retroceder 100 pasos en lugar de luchar contra el jefe, si tuviéramos la opción ":
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'))