Tìm đường với khóa và chìa khóa?


22

Tôi đang làm việc với một trò chơi với các bản đồ giống với các câu đố khóa và khóa . AI cần điều hướng đến một mục tiêu có thể ở phía sau cánh cửa màu đỏ bị khóa, nhưng chìa khóa đỏ có thể ở đằng sau cánh cửa màu xanh bị khóa, v.v.

Câu đố này tương tự như một hầm ngục kiểu Zelda, giống như bức tranh này:

Hầm ngục Zelda

Để đến được Mục tiêu, bạn phải đánh bại Boss, cần phải vượt qua hố, đòi hỏi phải thu thập Feather, đòi hỏi phải thu thập Chìa khóa

Hầm ngục Zelda có xu hướng tuyến tính. Tuy nhiên, tôi cần giải quyết vấn đề trong trường hợp chung. Vì thế:

  • Mục tiêu có thể yêu cầu một trong các bộ khóa. Vì vậy, có thể bạn cần lấy phím đỏ hoặc phím xanh. Hoặc có thể có một cánh cửa không khóa trên đường dài!
  • Có thể có nhiều cửa và chìa khóa của một loại. Ví dụ, có thể có nhiều khóa màu đỏ trên bản đồ và việc thu thập một khóa sẽ cấp quyền truy cập vào tất cả các cửa màu đỏ.
  • Mục tiêu có thể không truy cập được vì các phím bên phải nằm sau cánh cửa bị khóa

Làm thế nào tôi có thể thực hiện tìm đường trên bản đồ như vậy? Biểu đồ tìm kiếm sẽ như thế nào?

Lưu ý: điểm cuối cùng về việc phát hiện các mục tiêu không thể tiếp cận là rất quan trọng; A *, chẳng hạn, cực kỳ kém hiệu quả nếu mục tiêu không thể truy cập được. Tôi muốn giải quyết vấn đề này một cách hiệu quả.

Giả sử rằng AI biết mọi thứ ở đâu trên bản đồ.


4
Có phải AI chỉ biết và khám phá mọi thứ một khi nó mở khóa chúng? Ví dụ, nó có biết lông ở đằng sau cánh cửa bị khóa không? AI có hiểu các khái niệm như, "Đó là khóa nên tôi cần chìa khóa" hay đơn giản hơn là, "Tôi có thứ gì đó cản đường tôi, vì vậy hãy thử tất cả những thứ tôi tìm thấy trên đó. Feather trên cửa? Không. Chìa khóa trên cửa? Vâng! "
Tim Holt

1
Có một số cuộc thảo luận trước đây về vấn đề này trong câu hỏi này về việc tìm đường đi ngược so với lùi , có thể có ích cho bạn.
DMGregory

1
Vì vậy, bạn không cố gắng mô phỏng một người chơi, nhưng đang cố gắng tạo ra một hầm ngục tối ưu hóa? Câu trả lời của tôi chắc chắn là về việc mô phỏng một hành vi của người chơi.
Tim Holt

4
Thật không may, việc phát hiện một mục tiêu không thể tiếp cận là khá khó khăn. Cách duy nhất để chắc chắn rằng không có cách nào để đạt được mục tiêu là khám phá toàn bộ không gian có thể tiếp cận để đảm bảo không có mục tiêu nào chứa mục tiêu - đó chính xác là những gì A * thực hiện để có thêm nhiều bước nếu mục tiêu là không thể truy cập Bất kỳ thuật toán nào tìm kiếm ít rủi ro không gian đều thiếu một đường dẫn có sẵn đến mục tiêu vì đường dẫn đó đang ẩn trong một phần của không gian mà nó bỏ qua tìm kiếm. Bạn có thể tăng tốc điều này bằng cách làm việc ở cấp độ cao hơn, tìm kiếm biểu đồ của các kết nối phòng thay vì mọi đa giác gạch hoặc navmesh.
DMGregory

1
Không chính thức, tôi theo bản năng nghĩ về Thử thách của Chip thay vì Zelda :)
Flater

Câu trả lời:


22

Tìm đường tiêu chuẩn là đủ tốt - trạng thái của bạn là vị trí hiện tại của bạn + hàng tồn kho hiện tại của bạn. "Di chuyển" là thay đổi phòng hoặc thay đổi hàng tồn kho. Không nằm trong câu trả lời này, nhưng không có quá nhiều nỗ lực bổ sung, đang viết một heuristic tốt cho A * - nó thực sự có thể tăng tốc tìm kiếm bằng cách thích nhặt những thứ hơn di chuyển ra khỏi nó, thích mở khóa một cánh cửa gần mục tiêu tìm kiếm một chặng đường dài, v.v.

Câu trả lời này đã nhận được rất nhiều sự ủng hộ kể từ khi nó xuất hiện lần đầu tiên và có một bản demo, nhưng để có một giải pháp chuyên biệt và tối ưu hơn nhiều, bạn cũng nên đọc câu trả lời "Làm ngược lại nhanh hơn nhiều" /gamedev/ / a / 150155/2624


Javascript hoạt động đầy đủ của khái niệm dưới đây. Xin lỗi vì câu trả lời như một bãi chứa mã - tôi đã thực sự thực hiện điều này trước khi tôi tin rằng đó là một câu trả lời tốt, nhưng nó có vẻ khá linh hoạt đối với tôi.

Để bắt đầu khi nghĩ về tìm đường, hãy nhớ rằng sự thừa kế của các thuật toán tìm đường đơn giản là:

  • Breadth First Search là đơn giản như bạn có thể nhận được.
  • Thuật toán của Djikstra giống như Tìm kiếm đầu tiên của Breadth nhưng với "khoảng cách" khác nhau giữa các quốc gia
  • A * là Djikstras nơi bạn có 'ý thức chung về hướng đúng' có sẵn như là một heuristic.

Trong trường hợp của chúng tôi, chỉ cần mã hóa "trạng thái" là "vị trí + khoảng không quảng cáo" và "khoảng cách" là "sử dụng vật phẩm hoặc chuyển động" cho phép chúng tôi sử dụng Djikstra hoặc A * để giải quyết vấn đề của mình.

Đây là một số mã thực tế chứng minh mức độ ví dụ của bạn. Đoạn mã đầu tiên chỉ để so sánh - chuyển sang phần thứ hai nếu bạn muốn xem giải pháp cuối cùng. Chúng tôi bắt đầu với việc triển khai của Djikstra tìm ra đường dẫn chính xác, nhưng chúng tôi đã bỏ qua tất cả các chướng ngại vật và chìa khóa. (Hãy dùng thử, bạn có thể thấy nó chỉ là những con ong cho kết thúc, từ phòng 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)], []))

Vì vậy, làm thế nào để chúng ta thêm các mục và khóa vào mã này? Đơn giản! thay vì mỗi "trạng thái" chỉ bắt đầu số phòng, giờ đây là một bộ phòng và trạng thái tồn kho của chúng tôi:

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

Chuyển đổi bây giờ thay đổi từ một tuple (chi phí, phòng) sang một tuple (chi phí, trạng thái), do đó có thể mã hóa cả "chuyển sang phòng khác" và "nhặt một vật phẩm"

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

cuối cùng, chúng tôi thực hiện một số thay đổi nhỏ liên quan đến loại đối với chức năng Djikstra (ví dụ: nó vẫn chỉ khớp với số phòng mục tiêu thay vì trạng thái đầy đủ) và chúng tôi nhận được câu trả lời đầy đủ! Lưu ý kết quả được in trước tiên vào phòng 4 để lấy chìa khóa, sau đó đến phòng 1 để nhặt lông, sau đó đến phòng 6, giết sếp, sau đó đến phòng 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')], []))

Về lý thuyết, điều này hoạt động ngay cả với BFS và chúng tôi không cần chức năng chi phí cho Djikstra, nhưng có chi phí cho phép chúng tôi nói "nhặt chìa khóa là dễ dàng, nhưng chiến đấu với một ông chủ thực sự khó khăn, và chúng tôi muốn quay lại 100 bước thay vì chiến đấu với ông chủ, nếu chúng ta có sự lựa chọ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'))

Có, bao gồm hàng tồn kho / trạng thái khóa trong biểu đồ tìm kiếm là một giải pháp. Tôi lo ngại về các yêu cầu không gian gia tăng mặc dù - một bản đồ có 4 phím yêu cầu gấp 16 lần không gian của đồ thị không có khóa.
congusbongus

8
@congusbongus chào mừng bạn đến với vấn đề nhân viên bán hàng du lịch NP-Complete. Không có giải pháp chung sẽ giải quyết điều đó trong thời gian đa thức.
ratchet freak

1
@congusbongus Tôi thường không nghĩ rằng biểu đồ tìm kiếm của bạn sẽ có quá nhiều chi phí, nhưng nếu bạn lo lắng về không gian, chỉ cần đóng gói dữ liệu của mình - bạn có thể sử dụng 24 bit cho chỉ báo phòng (16 triệu phòng nên là đủ cho bất cứ ai) và một chút cho mỗi mục bạn sử dụng làm cổng (tối đa 8 mục duy nhất). Nếu bạn muốn có được sự ưa thích, bạn có thể sử dụng các phụ thuộc để đóng gói các mục thành các bit thậm chí nhỏ hơn, tức là sử dụng cùng một bit cho "khóa" và "ông chủ" vì có một sự phụ thuộc quá độ gián tiếp
Jimmy

@Jimmy Mặc dù nó không mang tính cá nhân, tôi đánh giá cao việc đề cập đến câu trả lời của tôi :)
Jibb Smart

13

Ngược A * sẽ thực hiện thủ thuật

Như đã thảo luận trong câu trả lời này cho một câu hỏi về tìm đường tiến và lùi , tìm đường lùi là một giải pháp tương đối đơn giản cho vấn đề này. Điều này hoạt động rất giống với GOAP (Lập kế hoạch hành động theo mục tiêu), lập kế hoạch cho các giải pháp hiệu quả trong khi giảm thiểu sự thắc mắc vô mục đích.

Ở dưới cùng của câu trả lời này, tôi có một bản phân tích về cách nó xử lý ví dụ bạn đã đưa ra.

Chi tiết

Pathfind từ đích đến bắt đầu. Nếu, trong quá trình tìm đường, bạn bắt gặp một cánh cửa bị khóa, bạn có một nhánh mới để tìm đường đi tiếp tục qua cánh cửa như thể nó được mở khóa, với nhánh chính tiếp tục tìm kiếm một con đường khác. Chi nhánh tiếp tục đi qua cánh cửa như thể nó được mở khóa không còn tìm kiếm tác nhân AI - giờ đây nó đang tìm kiếm một chìa khóa mà nó có thể sử dụng để đi qua cánh cửa. Với A *, heuristic mới của nó là khoảng cách đến khóa + khoảng cách đến tác nhân AI, thay vì chỉ khoảng cách đến tác nhân AI.

Nếu nhánh cửa mở khóa tìm thấy chìa khóa, thì nó tiếp tục tìm kiếm tác nhân AI.

Giải pháp này được thực hiện phức tạp hơn một chút khi có sẵn nhiều khóa khả thi, nhưng bạn có thể phân nhánh phù hợp. Bởi vì các nhánh có một đích cố định, nó vẫn cho phép bạn sử dụng phương pháp heuristic để tối ưu hóa việc tìm đường (A *) và các đường dẫn không thể hy vọng sẽ bị cắt nhanh chóng - nếu không có cách nào xung quanh cánh cửa bị khóa, nhánh đó không Không đi qua cửa nhanh chóng hết các lựa chọn và chi nhánh đi qua cửa và tự tìm chìa khóa tiếp tục.

Tất nhiên, nơi có sẵn nhiều tùy chọn khả thi (nhiều chìa khóa, vật phẩm khác để phá cửa, đường dài quanh cửa), nhiều chi nhánh sẽ được duy trì, ảnh hưởng đến hiệu suất. Nhưng bạn cũng sẽ tìm thấy tùy chọn nhanh nhất và có thể sử dụng tùy chọn đó.


Trong hành động

Trong ví dụ cụ thể của bạn, tìm đường dẫn từ Mục tiêu đến Bắt đầu:

  1. Chúng tôi nhanh chóng bắt gặp một cánh cửa ông chủ. Chi nhánh A tiếp tục qua cửa, bây giờ tìm kiếm một ông chủ để chiến đấu. Chi nhánh B bị mắc kẹt trong phòng, và sẽ sớm hết hạn khi không tìm thấy lối thoát.

  2. Chi nhánh A tìm thấy ông chủ và hiện đang tìm kiếm Bắt đầu, nhưng gặp phải một cái hố.

  3. Nhánh A tiếp tục qua hố, nhưng bây giờ nó đang tìm kiếm lông vũ, và sẽ tạo ra một đường ong về phía lông vũ theo. Nhánh C được tạo ra để cố gắng tìm đường quanh hố, nhưng sẽ hết hạn ngay khi không thể. Điều đó, hoặc nó bị bỏ qua trong một thời gian, nếu heuristic A * của bạn thấy rằng Chi nhánh A vẫn có vẻ hứa hẹn nhất.

  4. Chi nhánh A bắt gặp cánh cửa bị khóa và tiếp tục đi qua cánh cửa bị khóa như thể nó được mở khóa, nhưng giờ nó đang tìm chìa khóa. Chi nhánh D tiếp tục đi qua cánh cửa bị khóa, vẫn tìm kiếm chiếc lông vũ, nhưng sau đó nó sẽ tìm chìa khóa. Điều này là do chúng ta không biết trước tiên chúng ta cần tìm chìa khóa hay chiếc lông vũ, và khi có liên quan đến việc tìm đường, thì Bắt đầu có thể ở phía bên kia của cánh cửa này. Chi nhánh E cố gắng tìm cách xung quanh cánh cửa bị khóa, và thất bại.

  5. Chi nhánh D nhanh chóng tìm thấy chiếc lông vũ và tiếp tục tìm kiếm chiếc chìa khóa. Nó được phép đi qua cánh cửa bị khóa một lần nữa, vì nó vẫn đang tìm chìa khóa (và nó đang hoạt động ngược thời gian). Nhưng một khi nó có chìa khóa, nó sẽ không thể đi qua cánh cửa bị khóa (vì nó không thể đi qua cánh cửa bị khóa trước khi tìm thấy chìa khóa).

  6. Chi nhánh A và D tiếp tục cạnh tranh, nhưng khi Chi nhánh A đạt được chìa khóa, nó sẽ tìm kiếm chiếc lông vũ và nó sẽ không đến được chiếc lông vũ vì nó phải đi qua cánh cửa bị khóa một lần nữa. Mặt khác, Chi nhánh D, khi đạt được chìa khóa, chuyển sự chú ý của mình sang Bắt đầu và tìm thấy nó mà không có sự phức tạp.

  7. Chi nhánh D thắng. Nó đã tìm thấy con đường ngược lại. Đường dẫn cuối cùng là: Bắt đầu -> Khóa -> Feather -> Boss -> Mục tiêu.


6

Chỉnh sửa : Điều này được viết từ quan điểm của một AI ra ngoài để khám phá và khám phá mục tiêu và không biết vị trí của các phím, khóa hoặc đích trước thời hạn.

Đầu tiên, giả định rằng AI có một số loại mục tiêu tổng thể. Ví dụ: "Tìm ông chủ" trong ví dụ của bạn. Vâng, bạn muốn đánh bại nó, nhưng thực sự đó là về việc tìm kiếm nó. Giả sử nó không có ý tưởng làm thế nào để đạt được mục tiêu, chỉ là nó tồn tại. Và nó sẽ biết nó khi nó tìm thấy nó. Một khi mục tiêu được đáp ứng, AI có thể ngừng hoạt động để giải quyết vấn đề.

Ngoài ra, tôi sẽ sử dụng thuật ngữ chung "khóa" và "khóa" ở đây, ngay cả khi nó có thể là một khoảng cách và một chiếc lông vũ. Tức là, lông "mở khóa" khoảng cách "khóa".

Cách tiếp cận giải pháp

Có vẻ như bạn bắt đầu trước tiên chỉ với một AI về cơ bản là một nhà thám hiểm mê cung (nếu bạn nghĩ rằng bản đồ của bạn là một mê cung). Khám phá và vạch ra tất cả những nơi nó có thể đi sẽ là trọng tâm chính của AI. Nó có thể hoàn toàn dựa trên một cái gì đó đơn giản như, "Luôn đi đến con đường gần nhất tôi đã thấy nhưng chưa đến thăm."

Tuy nhiên, một vài quy tắc sẽ có hiệu lực trong khi khám phá có thể thay đổi mức độ ưu tiên ...

  • Nó sẽ lấy bất kỳ khóa nào được tìm thấy, trừ khi nó đã có cùng khóa
  • Nếu nó tìm thấy một khóa mà nó chưa từng thấy trước đây, nó sẽ thử mọi khóa mà nó đã tìm thấy trên khóa đó
  • Nếu một khóa hoạt động trên một loại khóa mới, nó sẽ nhớ loại khóa và loại khóa
  • Nếu nó tìm thấy khóa mà nó đã thấy trước đó và có khóa, nó sẽ sử dụng loại khóa đã nhớ (ví dụ: khóa đỏ thứ hai được tìm thấy, khóa đỏ đã hoạt động trước khi khóa đỏ, vì vậy chỉ cần sử dụng phím đỏ)
  • Nó sẽ nhớ vị trí của bất kỳ khóa nào mà nó không thể mở khóa
  • Không cần nhớ vị trí ổ khóa đã mở khóa
  • Bất cứ khi nào nó tìm thấy một khóa và biết về bất kỳ ổ khóa nào có thể mở khóa trước đó, nó sẽ truy cập ngay vào từng ổ khóa bị khóa đó và cố gắng mở khóa bằng khóa tìm thấy mới
  • Khi nó mở khóa một con đường, nó sẽ quay trở lại mục tiêu thăm dò và lập bản đồ, ưu tiên bước vào khu vực mới

Một lưu ý về điểm cuối cùng đó. Nếu nó phải chọn giữa việc kiểm tra một khu vực chưa được khám phá mà nó đã thấy trước đó (nhưng chưa được truy cập) so với khu vực chưa được khám phá phía sau một đường dẫn mới được mở khóa, thì nên ưu tiên đường dẫn mới được mở khóa. Đó có lẽ là nơi có khóa mới (hoặc khóa) sẽ hữu ích. Điều này giả định rằng một con đường bị khóa có lẽ sẽ không phải là một ngõ cụt vô nghĩa.

Mở rộng ý tưởng với các phím "Có thể khóa"

Bạn có khả năng có thể có các khóa không thể lấy mà không có khóa khác. Hoặc khóa phím như nó được. Nếu bạn biết Hang động khổng lồ cũ của mình, bạn cần phải có lồng chim để bắt chim - thứ mà bạn cần sau này cho một con rắn. Vì vậy, bạn "mở khóa" con chim bằng lồng (không chặn đường nhưng không thể nhặt được nếu không có lồng), sau đó "mở khóa" con rắn (chặn đường của bạn) với con chim.

Vì vậy, thêm một số quy tắc ...

  • Nếu không thể lấy khóa (khóa), hãy thử mọi khóa bạn đã có trên đó
  • Nếu bạn tìm thấy một khóa bạn không thể mở khóa, hãy nhớ nó sau
  • Nếu bạn tìm thấy một khóa mới, hãy thử nó trên mọi khóa đã biết cũng như đường dẫn bị khóa

Tôi thậm chí sẽ không hiểu toàn bộ về việc mang theo một chiếc chìa khóa nào đó có thể phủ nhận tác dụng của một chiếc chìa khóa khác (Hang động khổng lồ, cây gậy sợ chim và phải được thả xuống trước khi có thể nhặt được con chim, nhưng sau đó cần thiết để tạo ra cây cầu ma thuật) .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.