Il percorso standard è abbastanza buono : i tuoi stati sono la tua posizione corrente + il tuo inventario attuale. "traslocare" significa cambiare stanza o cambiare inventario. Non trattato in questa risposta, ma non troppo sforzo aggiuntivo, sta scrivendo una buona euristica per A *: può davvero accelerare la ricerca preferendo raccogliere le cose piuttosto che allontanarsi da essa, preferendo sbloccare una porta vicino al bersaglio a cercare molto, ecc.
Questa risposta ha ottenuto molti voti da quando è arrivata prima e ha una demo, ma per una soluzione molto più ottimizzata e specializzata, dovresti anche leggere la risposta "Farlo indietro è molto più veloce" /gamedev/ / a / 150155/2624
Di seguito il concetto di Javascript pienamente operativo. Ci scusiamo per la risposta come un dump del codice: in realtà lo avevo implementato prima di essere convinto che fosse una buona risposta, ma mi sembra abbastanza flessibile.
Per iniziare quando pensi al pathfinding, ricorda che l'erarchia dei semplici algoritmi di path path è:
- Breadth First Search è il più semplice possibile.
- L'algoritmo di Djikstra è come Breadth First Search ma con "distanze" variabili tra gli stati
- Un * è Djikstras in cui hai un 'senso generale della giusta direzione' disponibile come euristico.
Nel nostro caso, la semplice codifica di uno "stato" come "posizione + inventario" e "distanze" come "movimento o utilizzo dell'oggetto" ci consente di utilizzare Djikstra o A * per risolvere il nostro problema.
Ecco un codice reale che dimostra il tuo livello di esempio. Il primo frammento è solo per confronto: passa alla seconda parte se vuoi vedere la soluzione finale. Iniziamo con un'implementazione di Djikstra che trova il percorso corretto, ma abbiamo ignorato tutti gli ostacoli e le chiavi. (Provalo, puoi vederlo solo prima del traguardo, dalla stanza 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)], []))
Quindi, come possiamo aggiungere elementi e chiavi a questo codice? Semplice! invece di ogni "stato" inizia solo il numero della stanza, ora è una tupla della stanza e il nostro stato di 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 }
Le transizioni ora cambiano da tupla (costo, stanza) a tupla (costo, stato), quindi possono codificare sia "spostarsi in un'altra stanza" sia "raccogliere un oggetto"
// 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));
};
infine, apportiamo alcune modifiche minori relative al tipo alla funzione Djikstra (ad esempio, continua a corrispondere al numero di una goal room anziché a uno stato completo) e otteniamo la nostra risposta completa! Nota che il risultato stampato va prima nella stanza 4 per ritirare la chiave, quindi nella stanza 1 per raccogliere la piuma, quindi va nella stanza 6, uccide il boss, quindi va nella stanza 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')], []))
In teoria, questo funziona anche con BFS e non abbiamo avuto bisogno della funzione di costo per Djikstra, ma avere il costo ci consente di dire "raccogliere una chiave è senza sforzo, ma combattere un boss è davvero difficile e preferiremmo tornare indietro 100 passi anziché combattere il capo, se potessimo scegliere ":
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'))