Thuật toán trò chơi ghép ba


7

Tôi đang cố gắng viết một trò chơi giải đố ghép ba như 'bản thân cuộc gọi của Atlantis'. Thuật toán quan trọng nhất là tìm ra tất cả các khả năng khớp ba có thể. Có bất kỳ dự án nguồn mở nào có thể được tham chiếu không? Hoặc bất kỳ từ khóa cho thuật toán? Tôi đang cố gắng tìm kiếm một thuật toán nhanh hơn để tính toán tất cả các khả năng.

Quy tắc:

  • Đường chéo không được tính.
  • Kích thước là 8x8

Cảm ơn.


Bạn có nghĩa là một lưới gạch 8 x 8, mỗi ô khác nhau và bạn ghép 3 trong một dòng để làm gì đó? Giống như Bejeweled? EDIT: Tìm thấy một liên kết..có, nó trông tương tự như Bejeweled.
Vịt Cộng sản

Một vấn đề khác là sau khi khớp 3 đối tượng liên tiếp và loại bỏ chúng, có cần phải quét lại tất cả các lưới hay chỉ một phần không?
Stan

1
Vì bạn có đủ thời gian (bạn sẽ chơi một số loại hoạt hình khi bạn xóa trận đấu), bạn sẽ có nhiều thời gian để quét lại toàn bộ lưới. (Tôi biết, tôi đã thực hiện nó theo cách tôi đã viết dưới đây).
Kaj

Câu trả lời:


17

Tôi đã giúp chuyển một trong những game này sang một nền tảng cầm tay. Nhìn lại mã AI của họ để tìm ra tiềm năng: yipe, nó phức tạp, tàn bạo (vòng lặp được lồng bốn lần, thỉnh thoảng tự gọi đệ quy, v.v.), và nó không xuất hiện ở tất cả thân thiện với bộ đệm ngay từ cái nhìn đầu tiên.

(Một số sự phức tạp đó đến từ việc cố gắng đánh giá sức mạnh của sự di chuyển trong bối cảnh: định giá chuỗi dài hơn cao hơn, tìm kiếm các combo, v.v ...)

Nhưng nó không thực sự cần phải "tối ưu"; chúng tôi thậm chí không chạm vào mã khi chúng tôi chuyển nó. Nó không bao giờ xuất hiện trên hồ sơ.

Nhìn vào nó bây giờ, ngay cả ở một từ 32 bit trên mỗi ô (và tôi nghĩ rằng họ thực sự đã sử dụng một byte cho mỗi ô), toàn bộ bảng sẽ phù hợp với bộ đệm L1 rất nhỏ và bạn có thể đọc nhiều thứ trên bộ nhớ cache mà không ảnh hưởng tốc độ khung hình quá nhiều. Đặc biệt là vì bạn chỉ cần thực hiện toàn bộ quá trình này một lần mỗi khi cấu hình bảng thay đổi. (Một con theta lớn bay lượn xung quanh n^2không tệ đến mức khủng khiếp với mức rất thấp n, chưa kể đến hệ số nhân nhỏ được cung cấp cho bộ nhớ đệm.)

Điều đó đã được nói: để giải trí, chúng ta hãy cố gắng song song hóa vấn đề. Bắt đầu với các hoạt động bitwise.

Giả sử bạn có một bitmask đại diện cho tất cả các mảnh (chúng ta sẽ gọi chúng là đá) trong một hàng thuộc một loại cụ thể (chúng ta sẽ sử dụng màu sắc để phân biệt các loại). Chúng ta sẽ chỉ bắt đầu nhìn vào những viên đá đỏ và lo lắng về chi phí tính toán bitmask sau này.

// Let's assume top right indexing.
// (The assumption is not necessary, --
//    it just makes the left-shift and right-shift operators 
//    look like they're pointing in the correct direction.)

// this is for row 2
col index  76543210
color      BRRGYRBR // blue, red, red, green, yellow, ...
"red" bits 01100101

Chúng tôi đang tìm kiếm các loạt mà cần chỉ là một trao đổi để trở thành một loạt các 3. Courtesy Kaj, đây là một trong ba kết hợp, về cơ bản: XoX, oXX, hoặc XXonơi Xlà một hòn đá phù hợp và olà cái gì khác.

(Ý tưởng này được mượn từ cuốn sách Tuyệt vời của Hacker tuyệt vời ; xem thêm fxtbook nếu những điều đó làm bạn hài lòng.)

// using c-style bitwise operators:
// & is "and"
// ^ is "xor"
// | is "or"
// << and >> are arithmetic (non-sign-extending) shifts

redBitsThisRow = redBitsRows[2]

// find the start of an XoX sequence
startOfXoXSequence = redBitsThisRow & (redBitsThisRow << 2);
// for our example, this will be 00000100

// find any two stones together in a row
startOfXXSequence = redBitsThisRow & (redBitsThisRow << 1);
// for our example, this will be 01000000

Sẽ hữu ích hơn khi biết vị trí của những viên đá bị thiếu, không phải là sự khởi đầu của chuỗi XX hoặc XoX:

// give us any sequences that might want a stone from the left
missingLeftStone = startOfXXSequence << 1;
// for our example, this will be 10000000

// give us any sequences that might want a stone from the right
missingRightStone = startOfXXSequence >> 2;
// for our example, this will be 00010000

// give us any sequences that might want a stone from the top or bottom
missingTopOrBottomStone = missingRightStone | missingLeftStone | (startOfXoXSequence >> 1)
// for our example, this will be 10010010

(Khoảng 1 tải và 9 hướng dẫn ALU - 5 ca, 2 ors, 2 ands - với CPU khủng khiếp không bao gồm bộ chuyển đổi nội tuyến. Trên nhiều kiến ​​trúc, các ca này có thể miễn phí.)

Chúng ta có thể lấp đầy những chỗ còn thiếu không?

// look to the left, current row
leftMatches = redBitsThisRow & (missingLeftStone << 1)

// look to the right, current row
rightMatches = redBitsThisRow & (missingRightStone >> 1)

// look on the row above
topMatches = redBitsRow[1] & missingTopOrBottomStone

// look on the row below
bottomMatches = redBitsRow[3] & missingTopOrBottomStone

(2 lần tải khác và 6 hướng dẫn ALU - 4 và 2, 2 ca - với CPU kém. Lưu ý rằng hàng 0 và hàng 7 có thể gây rắc rối cho bạn - bạn có thể chọn phân nhánh xung quanh các tính toán này hoặc tránh phân nhánh bằng cách phân bổ không gian cho hai hàng thừa, một trước 0 và một sau 7 và bỏ trống chúng.)

Bây giờ chúng ta có một số lọ "khớp" cho biết vị trí của một hòn đá có thể hoán đổi để tạo ra một trận đấu.

Điều này giả định một phương pháp nội tuyến "đếm số không" hoặc rất rẻ:

swapType = RIGHT_TO_LEFT;
matches = leftMatches;
while ( (colIdx = ctz(matches)) < WORD_BITS ) {
   // rowIdx is 2 in our examples above
   workingSwaps.insert( SwapInfo(rowIdx, colIdx, swapType) );
   // note that this SwapInfo construction could do some more advanced logic:
   //   run the swap on a temporary board and see how much score it accumulates
   //   assign some sort of value based on preferring one type of match to another, etc

   matches = matches ^ (1<<colIdx); // clear the match, so we can loop to the next
}
// repeat for LEFT_TO_RIGHT with rightMatches
// repeat for TOP_TO_BOTTOM with topMatches
// repeat for BOTTOM_TO_TOP with bottomMatches

Lưu ý rằng không có logic bit nào bị phá vỡ trong môi trường endian nhỏ và endian lớn. Nó trở nên khó khăn hơn nhiều đối với các bảng rộng hơn kích thước từ máy của bạn. (Bạn có thể thử nghiệm một cái gì đó như thế std::bitsetnày.)

Còn cột thì sao? Có thể dễ nhất chỉ cần có hai bản sao của bảng, một bản được lưu theo thứ tự hàng và một bản được lưu theo thứ tự cột. Nếu bạn có quyền truy cập bảng getters và setters, điều này sẽ là tầm thường. Tôi không ngại giữ hai mảng cập nhật, sau khi tất cả một tập hợp trở nên rowArray[y][x] = newType; colArray[x][y] = newType;đơn giản.

... nhưng quản lý rowBits[color][row]colBits[color][col]trở nên đáng ghét.

Tuy nhiên, như một cách nhanh chóng, nếu bạn có rowBitscolBits, bạn có thể chạy cùng một mã với rowBits trỏ vào colBits thay thế. Mã giả, giả sử chiều rộng bảng = chiều cao bảng = 8 trong trường hợp này ...

foreach color in colors {
    foreach bits in rowBits, colBits {
        foreach row in 0..7 { // row is actually col the second time through
            // find starts, as above but in bits[row]
            // find missings, as above
            // generate matches, as above but in bits[row-1], bits[row], and bits[row+1]
            // loop across bits in each matches var,
            //    evaluate and/or collect them, again as above
        }
    }
}

Điều gì xảy ra nếu chúng ta không muốn làm phiền với việc chuyển đổi một mảng 2D đẹp thành bit? Với bảng 8 x 8, 8 bit cho mỗi ô và bộ xử lý có khả năng 64 bit, chúng tôi có thể thoát khỏi nó: 8 ô = 8 byte = 64 bit. Bây giờ chúng tôi đang bị khóa với chiều rộng bảng của chúng tôi, nhưng điều này có vẻ đầy hứa hẹn.

Giả sử "0" được bảo lưu, các viên đá bắt đầu từ 1 và đi tới NUM_STONE_TYPES.

startOfXXSequence = rowBytes ^ (rowBytes << (8*1))
// now all bytes that are 0x00 are the start of a XX sequence

startOfXoXSequence = rowBytes ^ (rowBytes << (8*2))
// all bytes that are 0x00 are the start of a XoX sequence

Lưu ý điều này không cần một lượt cho mỗi màu. Trong BRBRBRGYchúng ta sẽ nhận được một startOfXoXSequencethứ có thể giống như 0x00 00 00 00 aa bb cc dd- bốn byte trên cùng sẽ bằng 0, cho thấy một chuỗi có thể bắt đầu từ đó.

Sắp muộn rồi, vì vậy tôi sẽ rời khỏi đây và có thể quay lại sau - bạn có thể tiếp tục đi theo hướng này với các xors và thủ thuật "phát hiện byte 0 đầu tiên" hoặc bạn có thể xem xét một số tiện ích mở rộng SIMD nguyên .


Yêu thích việc thực hiện toán học bit (Tôi là một người thích hiệu suất): o) Tuy nhiên, đối với lưới 8 x 8 thì thực sự không cần thiết. Tôi đã thực hiện một cách ngây thơ một cách trắng trợn ở trên khi chạy một cách vui vẻ ở tốc độ 60fps trong khi nó đồng thời cào một cửa sổ đồ họa của trò chơi đang chạy để đọc đầu vào thực tế từ đó.
Kaj

@Kaj: yeah, tôi không đồng ý =) Tôi có lẽ nên rõ ràng hơn về điều đó. Ngay cả hàm khổng lồ với vòng lặp được lồng bốn lần, đôi khi sẽ tự gọi nó một cách đệ quy trong mã mà tôi đã đề cập chạy tốt trên nền tảng giới hạn mà chúng tôi đã chuyển nó sang. Nó được gọi rất ít khi và có một dữ liệu nhỏ được đặt ngang qua.
leander

+1 cho độ sâu tuyệt vời và tín dụng phù hợp mặc dù: op
Kaj

Tôi thích câu trả lời này là thực tế ngay từ đầu, nhưng nó không dừng lại ở đó và lao vào học viện sau này ...
MartinTeeVarga

Đối với một hiệu suất tốt hơn, bạn có thể precalculate leftMatches, rightMatches, missingTopOrBottomStonecho mỗi sự kết hợp tốt trên hàng / cột. Các bit cần thiết để lưu trữ các tính toán sẽ là columns * 3 * (2 ^ columns). 2 ^ columnslà các khả năng và columns * 3là các bit cần thiết cho cả 3 phép tính.
Veehmot

9

Bạn đang xem xét vấn đề. Trong một dòng 8, có 6 vị trí có thể có cho một trận đấu-3, do đó, đối với toàn bộ bảng 8x8, chỉ có 96 trận đấu 3 có thể có. Kiểm tra 96 ​​khả năng sử dụng một lượng thời gian CPU không đáng kể. Có lẽ bạn đang sử dụng chu kỳ đồng hồ gấp hàng nghìn lần chỉ cần vẽ một khung hình.

Nếu bạn đang tìm kiếm các động tác hoán đổi hai lần liền kề có thể thực hiện khớp 3 giây ... Đối với mỗi vị trí có thể, nếu đã có 2 mảnh khớp nhau, có nhiều nhất 3 lần hoán đổi có thể thực hiện khớp 3. Đối với một dòng, có nhiều nhất là 23 di chuyển để xem xét, và cho toàn bộ hội đồng quản trị có 112 di chuyển để xem xét. Kiểm tra 112 di chuyển cũng là một lượng thời gian CPU không đáng kể.


7

Chỉ có một vài kết hợp có thể tạo ra một trận đấu. XX với một dưới hoặc trên trung tâm (và xoay 90 độ) và xx. với một bên phải hoặc trên cùng của '.' (và được xoay và nhân đôi trên cả hai biến thể trục. Vì vậy, điều đó tạo ra 14 mẫu tìm kiếm khác nhau cần khớp với lưới (tối đa 4x3) so với lưới 8x8. Tôi chỉ cần dùng lực và trượt mẫu dọc theo lưới 8x8 và kiểm tra nếu các vị trí có x là như nhau. Nếu vậy, đó là một tập hợp có thể tạo ra một bộ ba.
Vì vậy, về cơ bản (ví dụ) slide

..
xx.x
...
hoặc (ví dụ anotheher)
. x.
x. .
x.

trên bảng - nếu cả ba vị trí x có cùng một biểu tượng thì đó là một động thái có thể.
Và đó là 14 lần cho tất cả các di chuyển có thể.


2
Đây là câu trả lời duy nhất dường như tìm thấy các động thái hợp lệ thực tế, trái ngược với việc chỉ tìm kiếm các bước chạy hiện có của ba.
JasonD

1
Ohhh tôi hiểu nhầm câu hỏi. Như tôi có vẻ thích làm gần đây. : - \
Ricket

Đừng khó khăn với bản thân vì đã cố gắng giúp đỡ! Câu hỏi đã được mở cho cả hai cách giải thích.
Kaj

2

Nếu tốc độ thực sự là một vấn đề, tôi sẽ nói hãy thử một cái gì đó như:

  • Lặp lại toàn bộ lưới, bắt đầu từ trên cùng bên trái.

  • Tại mọi điểm trên lưới cho đến và bao gồm cột thứ 6, hãy kiểm tra một ô bên phải và xuống (bạn không cần kiểm tra bên trái và lên trên vì bạn đã thực hiện việc này)

  • Khi bạn nhấn cột thứ 7, chỉ kiểm tra xuống. (Không có chỗ cho 3 người khi chỉ có 2 cột)

  • Nếu chúng khớp, sau đó thử kiểm tra thêm một ô dọc / xuống. Nếu họ không, di chuyển đến gạch tiếp theo.

  • Khi bạn hoàn thành cột thứ 8, di chuyển xuống một hàng.

Nhưng đối với lưới 8x8, không cần tối ưu hóa nhiều. Một hình thức dễ dàng hơn sẽ là (tương tự):

  • Lặp lại toàn bộ lưới, bắt đầu từ trên cùng bên trái.

    • Tại mọi điểm trên lưới cho đến và bao gồm cột thứ 6, hãy kiểm tra một ô bên phải và xuống (bạn không cần kiểm tra bên trái và lên trên vì bạn đã thực hiện việc này)

    • Nếu chúng khớp, sau đó thử kiểm tra thêm một ô dọc / xuống. Nếu họ không, di chuyển đến gạch tiếp theo. Xử lý các lỗi ngoài bảng là 'không giống nhau'.

    • Khi bạn hoàn thành cột thứ 8, di chuyển xuống một hàng.

Điều này sẽ cho phép mã đơn giản hơn nhiều trong vòng lặp for, vì bạn không có cấu trúc if với một số mã ở một bên và mã được điều chỉnh một chút bên dưới nó.


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.