ロックとキーを使用した経路探索?


22

私は、ロックパズルとキーパズルに似たマップを使ったゲームに取り組んでいます。AIは、ロックされた赤いドアの後ろにある可能性のある目標に移動する必要がありますが、赤いキーはロックされた青いドアの後ろにある場合があります...

このパズルは、次の図のようなゼルダ風のダンジョンに似ています。

ゼルダのダンジョン

目標を達成するには、ボスを倒す必要があります。ボスを倒すには、ピットを通過する必要があり、フェザーを集める必要があり、キーを集める必要があります

ゼルダのダンジョンは線形になる傾向があります。ただし、一般的なケースでは問題を解決する必要があります。そう:

  • 目標には、一連のキーのいずれかが必要になる場合があります。そのため、赤キーまたは青キーのいずれかを取得する必要があります。または、ロック解除されたドアが長い距離にある可能性があります!
  • 同じ種類の複数のドアとキーが存在する場合があります。たとえば、マップに複数の赤いキーがあり、1つを収集すると、すべての赤いドアへのアクセスが許可されます。
  • 右の鍵が施錠されたドアの後ろにあるため、目標に到達できない場合があります

そのような地図でどのように経路探索を実行しますか?検索グラフはどのように表示されますか?

注:アクセスできない目標の検出に関する最後のポイントは重要です。たとえば、A *は、目標に到達できない場合は非常に非効率的です。これに効率的に対処したいと思います。

AIがすべてがマップ上のどこにあるかを知っていると仮定します。


4
AIは、ロックを解除した後にのみ物事を認識して発見しますか?たとえば、羽が鍵のかかったドアの後ろにあることを知っていますか?AIは「鍵だから鍵が必要だ」といった概念を理解していますか、「自分の道をふさぐ何かがあるので、見つけたものをすべて試してください。ドアに羽がありますか?」ドアの鍵?はい!」
ティム・ホルト

1
この問題では、前方検索と後方検索のパスファインディングに関するこの問題についての以前の議論がありました。
DMGregory

1
プレイヤーをシミュレートするのではなく、最適化されたダンジョンランを作成しようとしていますか?私の答えは間違いなく、プレイヤーの行動をシミュレートすることでした。
ティム・ホルト

4
残念ながら、アクセスできない目標を検出することは非常に困難です。目標に到達する方法がないことを確認する唯一の方法は、到達可能な空間全体を探索して、その中に目標が含まれていないことを確認することです-これは、まさにA *が行うことで、目標がアクセスできません。少ないスペースを検索するアルゴリズムは、パスが検索をスキップしたスペースの一部に隠れていたため、ゴールへの利用可能なパスを失うリスクがあります。より高いレベルで作業して、すべてのタイルまたはnavmeshポリゴンの代わりに部屋の接続のグラフを検索することで、これを加速できます。
DMGregory

1
オフトピック、私は本能的にゼルダの代わりにチップの挑戦を考えました:)
フラット

回答:


22

標準的な経路探索は十分です -州は現在の場所+現在の在庫です。「移動」とは、部屋を変えるか、在庫を変えることです。この回答ではカバーされていませんが、追加の努力はあまりしていませんが、A *の優れたヒューリスティックを記述しています-離れた場所よりも物を拾い、ターゲットの近くにドアを開けることを好むため、検索を本当に高速化できます長い道のりを探しすぎるなど

この回答には最初からデモがあり、多くの賛成票が寄せられましたが、より最適化された特殊なソリューションについては、「後方に行う方がはるかに高速です」という回答もお読み ください/gamedev/ / a / 150155/2624


以下の完全に機能するJavascriptの概念実証。コードダンプとしての回答でごめんなさい-それが良い回答であると確信する前に実際にこれを実装していましたが、私にはかなり柔軟に思えます。

経路探索について考えるときに始めるために、単純な経路探索アルゴリズムの階層は次のとおりであることに注意してください。

  • 幅優先検索は、可能な限り簡単です。
  • DjikstraのアルゴリズムはBreadth First Searchに似ていますが、状態間で「距離」が異なります
  • A *はジクストラであり、ヒューリスティックとして「正しい方向の一般的な感覚」を利用できます。

この場合、「状態」を「場所+在庫」として、「距離」を「移動またはアイテムの使用」としてエンコードするだけで、DjikstraまたはA *を使用して問題を解決できます。

サンプルレベルを示す実際のコードを次に示します。最初のスニペットは比較のためだけです。最終的なソリューションを確認したい場合は、2番目の部分にジャンプしてください。正しいパスを見つけるDjikstraの実装から始めますが、すべての障害とキーを無視しました。(試してみてください、部屋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)], []))

それでは、このコードにどのようにアイテムとキーを追加するのでしょうか?シンプル!すべての「状態」ではなく部屋番号だけで始まるのではなく、部屋と在庫状態のタプルになります。

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

遷移は(コスト、部屋)タプルから(コスト、状態)タプルに変わるため、「別の部屋への移動」と「アイテムのピックアップ」の両方をエンコードできます。

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

最後に、Djikstra関数にいくつかのマイナーなタイプ関連の変更を加え(たとえば、完全な状態ではなく、目標の部屋番号で一致しているだけです)、完全な答えが得られます!印刷結果は最初に部屋4に行き、鍵を拾い、次に部屋1に行き、羽を拾い、部屋6に行き、ボスを殺し、部屋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')], []))

理論的には、これはBFSでも機能し、ジクストラのコスト関数は必要ありませんでしたが、コストがあることで、「キーを拾うのは簡単ですが、ボスと戦うのは本当に難しいです。選択肢があったら、ボスと戦うのではなく100歩」

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

はい、検索グラフに在庫/キーの状態を含めることが1つのソリューションです。ただし、必要なスペースが増えることを心配しています。4つのキーを持つマップには、キーのないグラフの16倍のスペースが必要です。
コンガスボン

8
@congusbongusは、NP完全巡回セールスマン問題へようこそ。多項式時間でそれを解決する一般的な解決策はありません。
ラチェットフリーク

1
@congusbongus一般的に、検索グラフがそれほどオーバーヘッドになるとは思いませんが、スペースが心配な場合は、データをパックするだけです-ルームインジケータに24ビットを使用できます(1600万の部屋が誰でも十分です)、ゲートとして使用することに興味のあるアイテムごとに少しずつ(最大8つのユニークなもの)。あなたは空想を取得したい場合は、間接的な推移dpendencyがありますので、「キー」と「ボス」に同じビットを使用し、すなわち、さらに小さなビットにアイテムをダウンパックに依存関係を使用することができます
ジミー・

@ジミーそれは個人的ではありませんが、私の答えに言及してくれて感謝しています:)
ジブスマート

13

後方A *がトリックを行います

前方経路探索後方経路探索に関する質問に対するこの回答で説明したように、後方経路探索はこの問題の比較的簡単な解決策です。これはGOAP(Goal Oriented Action Planning)と非常によく似ており、目的のない疑問を最小限に抑えながら効率的なソリューションを計画します。

この回答の最後に、あなたが与えた例をどのように処理するかの内訳があります。

詳細に

目的地から開始までのパス検索。パスファインディングでロックされたドアに出くわすと、パスファインディングへの新しいブランチがあり、ロックが解除されているかのようにドアを通過し、メインブランチは別のパスを探し続けます。ロックが解除されているかのようにドアを通り抜けるブランチは、AIエージェントを探していません。ドアを通過するために使用できるキーを探しています。A *では、新しいヒューリスティックは、キーまでの距離+ AIエージェントまでの距離であり、AIエージェントまでの距離ではありません。

ロックされていないドアブランチがキーを見つけると、AIエージェントを探し続けます。

複数の実行可能なキーがある場合、このソリューションは少し複雑になりますが、それに応じて分岐できます。ブランチの行き先は固定されているため、ヒューリスティックを使用してパス検索(A *)を最適化できます。また、ロックされたドアの周りに方法がない場合、不可能なパスはすぐに切断されることが期待されます。 「ドアを通過しない」オプションはすぐに使い果たされ、ドアを通過してキーを探すブランチは自動的に続行されます。

もちろん、さまざまな実行可能なオプション(複数のキー、ドアを回避する他のアイテム、ドアの周りの長いパス)が利用できる場合、多くのブランチが維持され、パフォーマンスに影響します。ただし、最速のオプションもあり、それを使用できます。


実行中

具体的な例では、ゴールからスタートまでのパスファインディング:

  1. すぐにボスのドアに遭遇します。支店Aはドアを通り抜け、今度は上司を探して戦います。ブランチBは部屋に閉じ込められたままで、出口が見つからないとすぐに期限切れになります。

  2. 支店Aはボスを見つけ、スタートを探していますが、ピットに遭遇します。

  3. ブランチAはピットの上を続きますが、今では羽を探しており、それに応じて羽に向かってビーラインを作成します。ピットCの周りに道を見つけようとするブランチCが作成されますが、できないとすぐに期限切れになります。A *ヒューリスティックがブランチAがまだ最も有望であると判断した場合、それはしばらく無視されます。

  4. 支店Aはロックされたドアに遭遇し、ロックが解除されているかのようにロックされたドアを通過しますが、現在はキーを探しています。ブランチDも同様にロックされたドアを通って羽を探し続けますが、その後、キーを探します。これは、最初にキーまたはフェザーを見つける必要があるかどうかがわからないためです。パスファインディングに関する限り、開始はこのドアの反対側にある可能性があります。支店Eは、施錠されたドアを迂回する方法を見つけようとして失敗します。

  5. ブランチDはすぐに羽を見つけ、キーを探し続けます。鍵を探しているので、鍵のかかったドアを再び通過することができます(そして、時間をさかのぼって機能します)。しかし、キーを取得すると、ロックされたドアを通過できなくなります(キーを見つける前にロックされたドアを通過できなかったため)。

  6. ブランチAとDは引き続き競合しますが、ブランチAがキーに到達すると、羽を探しており、再びロックされたドアを通過する必要があるため、羽に到達できません。一方、ブランチDは、キーに到達すると、スタートに注意を向け、問題なくそれを見つけます。

  7. ブランチDが勝ちます。リバースパスが見つかりました。最終パスは、[スタート]-> [キー]-> [フェザー]-> [ボス]-> [ゴール]です。


6

編集:これはAIの観点から書かれたもので、目標を探求し発見しようとしており、事前にキー、ロック、または目的地の場所を知りません。

まず、AIに何らかの全体的な目標があると仮定します。たとえば、あなたの例では「ボスを見つける」。ええ、あなたはそれを打ち負かしたいのですが、本当にそれを見つけることです。目標に到達する方法がわからない、ただそれが存在すると仮定します。そして、それを見つけるとそれを知るでしょう。目標が達成されると、AIは問題を解決するために作業を停止できます。

また、ここでは一般的な用語である「ロック」と「キー」を使用します。たとえそれが割れ目や羽であったとしてもです。すなわち、羽はキャズム「ロック」を「ロック解除」します。

ソリューションアプローチ

基本的に迷路探検家であるAIから始めたようです(マップを迷路と考える場合)。それが行くことができるすべての場所を探索し、マッピングすることは、主にAIの焦点です。「私が見たことがありますが、まだ訪問していない最も近いパスに常に移動する」などの単純なものだけに基づいている可能性があります。

ただし、優先順位を変更する可能性のある調査中にいくつかのルールが有効になります...

  • 既に同じキーを持っている場合を除き、見つかったキーが必要です
  • 以前に見たことのないロックが見つかった場合、そのロックで見つかったすべてのキーを試します
  • キーが新しいタイプのロックで機能する場合、キータイプとロックタイプを記憶します。
  • 以前に見たキーがあったロックが見つかった場合、記憶されたキータイプを使用します(たとえば、2番目の赤いロックが検出され、赤いキーが赤いロックで以前に機能していたため、単に赤いキーを使用します)
  • ロックを解除できなかったロックの場所を記憶します
  • ロックを解除したロックの場所を覚えておく必要はありません
  • キーを見つけて以前にロック解除可能なロックを知っていた場合は、ロックされたロックのそれぞれにすぐにアクセスし、見つかった新しいキーでロックを解除しようとします
  • パスのロックを解除するたびに、探索とマッピングの目標に戻り、新しい領域へのステップを優先します。

その最後の点に関するメモ。以前に見た(ただし訪問していない)未探索の領域と、新たにロック解除されたパスの背後にある未探索の領域をチェックアウトすることを選択する必要がある場合、新たにロック解除されたパスを優先する必要があります。それはおそらく、役に立つ新しいキー(またはロック)がある場所です。これは、ロックされたパスが無意味な行き止まりではないことを前提としています。

「ロック可能な」キーでアイデアを広げる

別のキーなしでは取得できないキーを潜在的に持つことができます。またはキーをロックします。古い巨大な洞窟を知っているなら、鳥を捕まえるために鳥かごを持っている必要があります。これは後でヘビに必要です。そのため、鳥をケージで「ロック解除」し(パスをブロックしませんが、ケージなしでは手に入れることはできません)、次に鳥でヘビ(パスをブロックする)を「ロック解除」します。

いくつかのルールを追加...

  • キーを取得できない(ロックされている)場合は、既にキーを持っているすべてのキーを試してください
  • ロック解除できないキーを見つけた場合は、後で覚えておいてください
  • 新しいキーを見つけたら、既知のロックされたすべてのキーとロックされたパスで試してみてください

特定のキーを持ち運ぶと別のキーの効果がどのように無効になるかについては詳しく説明しません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.