Шлях із замком та ключем?


22

Я працюю над грою з картами, що нагадують головоломки блокування та клавіш . ШІ повинен орієнтуватися на мету, яка може бути за замкненою червоною дверима, але червоний ключ може бути за замкненими блакитними дверима тощо.

Ця головоломка схожа на підземелля в стилі Зельда, як на цій картинці:

Підземелля Зельди

Щоб дістатися до Цілі, ви повинні перемогти Боса, який вимагає переходу через яму, для чого потрібно зібрати Перо, для чого потрібно зібрати Ключ

Підземелля Зельди, як правило, лінійні. Однак мені потрібно вирішити проблему в загальному випадку. Так:

  • Для цілі може знадобитися один із наборів ключів. Тож, можливо, вам потрібно отримати або червоний ключ, або синій ключ. Або може бути незамкненою двері довгий шлях!
  • Тут може бути кілька дверей та ключів. Наприклад, на карті може бути кілька червоних клавіш, і якщо зібрати один, буде надано доступ до всіх червоних дверей.
  • Мета може бути недосяжною, оскільки праві клавіші знаходяться за замкненими дверима

Як би я здійснив пошук маршрутів на такій карті? Як виглядав би графік пошуку?

Примітка: важливий останній пункт про виявлення недоступних цілей; Наприклад, A * є надзвичайно неефективним, якщо мета недосяжна. Я хотів би з цим ефективно впоратися.

Припустимо, що AI знає, де все на карті.


4
Чи знає AI і виявляє речі лише після того, як він їх розблокує? Наприклад, чи знає воно перо за замкненими дверима? Чи розуміє AI поняття на кшталт "Це замок, тому мені потрібен ключ", або щось більш просте на кшталт "У мене щось блокує шлях, тому спробуйте все, що я знайшов на ньому. Перо на дверях? Ні. Ключ від дверей? Так! "
Тім Холт

1
У цьому питанні було попередньо обговорено цю проблему щодо просування маршрутів вперед та назад , що може бути корисним для вас.
DMGregory

1
Отже, ви не намагаєтеся імітувати гравця, а намагаєтесь створити оптимізований забіг підземелля? Моя відповідь, безумовно, стосувалася моделювання поведінки гравця.
Тім Холт

4
На жаль, виявити недоступну мету досить важко. Єдиний спосіб бути впевненим, що немає шляху досягти мети - дослідити весь доступний простір, щоб переконатися, що жодна з них не містить мети - саме це робить A *, що змушує зробити стільки додаткових кроків, якщо мета недоступний. Будь-який алгоритм, який шукає менше місця, ризикує пропустити доступний шлях до цілі, оскільки шлях ховався в частині простору, який він пропустив у пошуку. Ви можете прискорити це, працюючи на більш високому рівні, шукаючи графік з'єднань кімнати замість кожної плитки або навмеш багатокутника.
DMGregory

1
Офтопік, я інстинктивно подумав про Chip Challenge замість Zelda :)
Flater

Відповіді:


22

Стандартне накладання маршрутів є достатньо хорошим - ваші штати - це ваше поточне місцезнаходження + ваш поточний інвентар. "переміщення" - це або роздягальня, або зміна інвентарю. Не висвітлений у цій відповіді, але не надто багато додаткових зусиль - це написання хорошої евристики для A * - це може дійсно пришвидшити пошук, віддаючи перевагу забрати речі, віддаляючись від нього, вважаючи за краще відмикати двері біля цілі над пошуком довгого шляху тощо.

Ця відповідь отримала багато відгуків, оскільки вона з’явилася вперше та має демонстраційну версію, але для набагато більш оптимізованого та спеціалізованого рішення слід також прочитати відповідь «Робити це набагато швидше назад» /gamedev/ / а / 150155/2624


Повністю функціональне підтвердження Javascript концепту нижче. Вибачте за відповідь як дамп коду - я фактично реалізував це ще до того, як переконався, що це хороша відповідь, але мені це здається досить гнучким.

Щоб почати, думаючи про пошук маршруту, пам’ятайте, що спадковість простих алгоритмів проходження маршруту:

  • Перший пошук ширини приблизно такий же простий, як ви можете отримати.
  • Алгоритм Джикстри - це як пошук першої ширини, але з різними "відстанями" між державами
  • A * - Джикстрас, де у вас є «загальне відчуття правильного напрямку», яке є евристичним.

У нашому випадку просто кодування "стану" як "розташування + інвентар" та "відстані" як "руху або використання предмета" дозволяє нам використовувати Djikstra або A * для вирішення нашої проблеми.

Ось фактичний код, що демонструє ваш приклад рівня. Перший фрагмент лише для порівняння - перейдіть до другої частини, якщо ви хочете побачити остаточне рішення. Ми починаємо з реалізації 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, і нам не потрібна функція витрат для Djikstra, але наявність вартості дозволяє нам сказати: "забрати ключ - це нелегко, але боротися з начальником - це дуже важко, і ми б краще відмовитися 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'))

Так, включення інвентарного / ключового стану в графік пошуку - це одне рішення. Мене хвилюють підвищені вимоги до місця - карта з 4 клавішами потребує 16-кратного простору графа, що не містить ключів.
congusbongus

8
@congusbongus Ласкаво просимо до проблеми повного подорожі продавця. Не існує загального рішення, яке вирішило б це в поліноміальний час.
щурячий вирод

1
@congusbongus Я взагалі не думаю, що ваш графік пошуку буде настільки великим, але якщо вас турбує простір, просто запакуйте свої дані - ви можете використовувати 24-бітні показники номера (16 мільйонів номерів повинні вистачить для когось) і трохи кожного для предметів, які вас цікавлять, як ворота (до 8 унікальних). Якщо ви хочете пофантазувати, ви можете використовувати залежності, щоб упакувати предмети в ще менші біти, тобто використовувати один і той же біт для "ключ" і "бос", оскільки є непряма перехідна залежність
Джиммі

@Jimmy Хоча це не особисте, я вдячний згадкою моєї відповіді :)
Jibb Smart

13

Назад A * зробить трюк

Як було обговорено у цій відповіді на запитання про пряму та зворотну маршрутизацію , прокручування назад є відносно простим рішенням цієї проблеми. Це дуже схоже на GOAP (цільове планування дій), плануючи ефективні рішення, зводячи до мінімуму безцільне запитання.

У нижній частині цієї відповіді я розбиваю, як вона поводиться з прикладом, який ви подали.

Детально

Шлях від пункту призначення до початку. Якщо в процесі пошуку ви натрапили на зачинені двері, у вас з'явилася нова гілка до вашої прокладки шляху, яка продовжується через двері, як ніби вона розблокована, при цьому головна гілка продовжує оглядатися за іншим шляхом. Гілка, яка продовжується через двері, ніби її розблокована, вже не шукає агента ШІ - тепер шукає ключ, який може використовувати для проходу через двері. Для A * його новим евристичним є відстань до ключа + відстань до агента AI, а не просто відстань до агента AI.

Якщо гілка незамкнених дверей знайде ключ, то вона продовжує шукати агента AI.

Це рішення стає дещо складнішим, коли є кілька життєздатних ключів, але ви можете відповідати відповідним чином. Оскільки гілки мають фіксований пункт призначення, воно все ще дозволяє використовувати евристику для оптимізації пошуку шляху (A *), і неможливі шляхи, сподіваємось, швидко відрізаються - якщо немає шляху навколо замкнених дверей, гілка, яка не робить Через пропуск дверей швидко не вистачає варіантів, і гілка, яка проходить через двері і шукає ключ, продовжується самостійно.

Звичайно, там, де є різноманітні життєздатні варіанти (декілька клавіш, інші предмети для обходу дверей, довгий шлях навколо дверей), багато гілок будуть збережені, що впливає на продуктивність. Але ви також знайдете найшвидший варіант і зможете ним скористатися.


У дії

У вашому конкретному прикладі наведення маршруту від цілі до початку:

  1. Ми швидко стикаємося з дверима боса. Відділення А продовжується через двері, тепер шукає боса для боротьби. Відділення B залишається застряглим у приміщенні і незабаром закінчується, коли виявить, що виходу немає.

  2. Відділення А знаходить начальника і зараз шукає Старт, але зустрічає яму.

  3. Гілка А продовжується над ямою, але тепер вона шукає перо, і відповідно зробить бджолину лінію у напрямку до пера. Відділення C створено, яке намагається знайти шлях навколо ями, але закінчується, як тільки не вдається. Це або на деякий час його ігнорують, якщо ваш евристичний A * виявить, що філія А все ще виглядає найбільш перспективною.

  4. Відділення А стикається із замкненими дверима і продовжує через зачинені двері, ніби відчинені, але тепер шукає ключ. Відділення D продовжується і через зачинені двері, все ще шукаючи перо, але тоді воно шукатиме ключ. Це тому, що ми не знаємо, чи потрібно нам спочатку знайти ключ або пір’я, а що стосується проходження маршруту, Старт може бути з іншого боку цієї двері. Відділення E намагається знайти шлях навколо замкнених дверей, і не вдається.

  5. Відділення D швидко знаходить перо і продовжує шукати ключ. Дозволено пройти через зачинені двері ще раз, оскільки він все ще шукає ключ (і він працює у напрямку часу назад). Але, як тільки у нього є ключ, він не зможе пройти через зачинені двері (оскільки він не міг пройти через зачинені двері, перш ніж знайшов ключ).

  6. Гілки A і D продовжують змагатись, але коли гілка A дістанеться до ключа, вона шукає перо, і не зможе дістатися до пера, оскільки йому доведеться знову пройти крізь зачинені двері. Відділення D, з іншого боку, дійшовши до ключа, звертає свою увагу на Старт і знаходить його без ускладнень.

  7. Відділення D виграє. Він знайшов зворотний шлях. Заключний шлях: Пуск -> Ключ -> Перо -> Бос -> Мета.


6

Редагувати : Це написано з точки зору інтелектуального інтелекту, який призначений досліджувати та виявляти мету, і не знає розташування ключів, замків чи пунктів призначення достроково.

По-перше, припустимо, що AI має якусь загальну мету. Наприклад, "Знайди начальника" у вашому прикладі. Так, ви хочете його побити, але насправді справа в тому, щоб знайти його. Припустимо, що він не має уявлення, як дістатися до мети, просто що вона існує. І знатиме це, коли знайде. Після досягнення мети ШІ може припинити роботу над вирішенням проблеми.

Також я тут вживаю загальний термін «замок» і «ключ», навіть якщо це може бути пропасть і перо. Тобто, перо «відмикає» прірву «замок».

Підхід до рішення

Здається, ви б почали спочатку просто з ШІ, який в основному був дослідником лабіринту (якщо ви думаєте про свою карту як про лабіринт). Вивчення та складання карт усіх місць, на які він може поїхати, було б головним напрямком роботи ШІ. Це могло б грунтуватися виключно на чомусь простому, наприклад, "Завжди йдіть до найближчого шляху, який я бачив, але ще не відвідував".

Однак, вивчаючи, що може змінити пріоритет, слід застосувати декілька правил ...

  • Він би взяв будь-який ключ, який він знайшов, якщо тільки він вже не мав той самий ключ
  • Якщо б він знайшов замок, якого ніколи не бачив, він би спробував кожен ключ, який знайшов у цьому замці
  • Якщо ключ працював над новим типом блокування, він запам'ятав би тип ключа та тип блокування
  • Якщо він знайшов замок, який він бачив раніше, і мав ключ, він використовував би запам’ятовуваний тип ключа (наприклад, знайдено другий червоний замок, червоний ключ працював до цього на червоному блокуванні, тому просто використовуйте червоний ключ)
  • Він запам'ятав би місце будь-якого замка, який він не міг розблокувати
  • Не потрібно було б пам’ятати про розташування замків, які він розблокував
  • Щоразу, коли знайде ключ і знає про будь-які раніше розблоковані замки, він негайно відвідає кожен із цих замкнених замків і спробує розблокувати їх за допомогою нового знайденого ключа
  • Коли б він ніколи не розблокував шлях, він просто повернеться до мети розвідки та картографування, надаючи пріоритет кроку в новій області

Примітка до цього останнього пункту. Якщо йому доведеться вибрати між невідомою територією, яку вона раніше бачила (але не відвідувала), порівняно з невивченою зоною позаду щойно розблокованого шляху, вона повинна зробити пріоритетну для щойно розблокованого шляху. Ось, напевно, є нові ключі (або блокування), які будуть корисні. Це передбачає, що заблокований шлях, ймовірно, не буде безглуздим тупиком.

Розширення ідеї за допомогою клавіш "Замикаються"

У вас потенційно можуть бути ключі, які неможливо взяти без іншого ключа. Або заблоковані клавіші як би. Якщо ви знаєте свої старі колосальні печери, вам потрібно мати пташину клітку, щоб зловити птаха - що вам потрібно згодом для змії. Таким чином, ви "розблокуєте" птаха з кліткою (яка не блокує шлях, але її не можна забрати без клітки), а потім "розблокуєте" змію (яка блокує ваш шлях) разом з птахом.

Тож додавання деяких правил ...

  • Якщо ключ неможливо взяти (він заблокований), спробуйте кожен ключ, який у вас уже є
  • Якщо ви знайдете ключ, який ви не можете розблокувати, запам'ятайте його на потім
  • Якщо ви знайдете новий ключ, спробуйте його на кожному відомому заблокованому ключі, а також на заблокованому шляху

Я навіть не буду вникати у все питання про те, як носіння певного ключа може заперечити дію іншого ключа (Колосальні печери, стрижень відлякує птаха і його потрібно скинути, перш ніж птах може бути зібраний, але це потрібно пізніше, щоб створити магічний міст) .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.