Thuật toán tối ưu cho trò chơi 2048 là gì?


1920

Gần đây tôi đã tình cờ thấy trò chơi 2048 . Bạn hợp nhất các ô tương tự bằng cách di chuyển chúng theo bất kỳ hướng nào trong bốn hướng để tạo các ô "lớn hơn". Sau mỗi lần di chuyển, một ô mới xuất hiện ở vị trí trống ngẫu nhiên với giá trị là 2hoặc 4. Trò chơi chấm dứt khi tất cả các ô được lấp đầy và không có động thái nào có thể hợp nhất các ô hoặc bạn tạo một ô có giá trị là 2048.

Một, tôi cần tuân theo một chiến lược được xác định rõ ràng để đạt được mục tiêu. Vì vậy, tôi nghĩ đến việc viết một chương trình cho nó.

Thuật toán hiện tại của tôi:

while (!game_over) {
    for each possible move:
        count_no_of_merges_for_2-tiles and 4-tiles
    choose the move with a large number of merges
}

Những gì tôi đang làm là tại bất kỳ thời điểm nào, tôi sẽ cố gắng hợp nhất các ô với các giá trị 24, nghĩa là tôi cố gắng có 2và các 4ô, tối thiểu nhất có thể. Nếu tôi thử theo cách này, tất cả các ô khác sẽ tự động được hợp nhất và chiến lược có vẻ tốt.

Nhưng, khi tôi thực sự sử dụng thuật toán này, tôi chỉ nhận được khoảng 4000 điểm trước khi trò chơi kết thúc. Điểm tối đa AFAIK cao hơn 20.000 điểm một chút so với điểm hiện tại của tôi. Có một thuật toán tốt hơn so với ở trên?


84
Điều này có thể giúp! ov3y.github.io/2048-AI
cegprakash

5
@ nitish712 bằng cách này, thuật toán của bạn rất tham lam vì bạn có choose the move with large number of mergesnhanh chóng dẫn đến tối ưu cục bộ
Khaled.K

21
@ 500-InternalServerError: Nếu tôi triển khai AI với việc cắt tỉa cây trò chơi alpha-beta, có thể giả định rằng các khối mới được đặt bất lợi. Đó là một giả định trong trường hợp xấu nhất, nhưng có thể hữu ích.
Charles

6
Một trò giải trí thú vị khi bạn không có thời gian để đạt điểm cao: Cố gắng đạt điểm thấp nhất có thể. Về lý thuyết, nó xen kẽ 2 giây và 4 giây.
Đánh dấu Hurd

7
Thảo luận về tính hợp pháp của câu hỏi này có thể được tìm thấy trên meta: meta.stackexchange.com/questions/227266/ory
Jeroen Vannevel 30/03/2016

Câu trả lời:


1266

Tôi đã phát triển một AI 2048 sử dụng expectimax tối ưu hóa, thay vì Minimax search được sử dụng bởi thuật toán @ ovolve của. AI chỉ đơn giản là thực hiện tối đa hóa tất cả các di chuyển có thể, tiếp theo là kỳ vọng vào tất cả các lần xuất hiện của gạch có thể (được xác định bằng xác suất của các ô, tức là 10% cho 4 và 90% cho 2). Theo như tôi biết, không thể cắt tỉa tối ưu hóa kỳ vọng (ngoại trừ loại bỏ các nhánh cực kỳ khó xảy ra), và vì vậy thuật toán được sử dụng là một tìm kiếm vũ lực được tối ưu hóa cẩn thận.

Hiệu suất

AI trong cấu hình mặc định của nó (độ sâu tìm kiếm tối đa là 8) mất từ ​​10ms đến 200ms để thực hiện di chuyển, tùy thuộc vào độ phức tạp của vị trí bảng. Trong thử nghiệm, AI đạt được tốc độ di chuyển trung bình 5-10 lần di chuyển mỗi giây trong toàn bộ trò chơi. Nếu độ sâu tìm kiếm bị giới hạn ở 6 lần di chuyển, AI có thể dễ dàng thực hiện hơn 20 lần di chuyển mỗi giây, điều này tạo ra một số cách xem thú vị .

Để đánh giá hiệu suất điểm số của AI, tôi đã chạy AI 100 lần (kết nối với trò chơi trình duyệt thông qua điều khiển từ xa). Đối với mỗi ô, đây là tỷ lệ của các trò chơi mà ô đó đạt được ít nhất một lần:

2048: 100%
4096: 100%
8192: 100%
16384: 94%
32768: 36%

Điểm tối thiểu trên tất cả các lần chạy là 124024; số điểm tối đa đạt được là 794076. Điểm trung bình là 387222. AI không bao giờ thất bại trong việc đạt được ô 2048 (vì vậy nó không bao giờ thua trò chơi dù chỉ một lần trong 100 trò chơi); trên thực tế, nó đã đạt được gạch 8192 ít nhất một lần trong mỗi lần chạy!

Đây là ảnh chụp màn hình của hoạt động tốt nhất:

Gạch 32768, điểm 794076

Trò chơi này mất 27830 lần di chuyển trong 96 phút, hoặc trung bình 4,8 di chuyển mỗi giây.

Thực hiện

Cách tiếp cận của tôi mã hóa toàn bộ bảng (16 mục) dưới dạng một số nguyên 64 bit duy nhất (trong đó các ô là nybble, tức là các khối 4 bit). Trên máy 64 bit, điều này cho phép toàn bộ bo mạch được truyền xung quanh trong một thanh ghi máy.

Các hoạt động dịch chuyển bit được sử dụng để trích xuất các hàng và cột riêng lẻ. Một hàng hoặc cột đơn có số lượng 16 bit, do đó, bảng có kích thước 65536 có thể mã hóa các phép biến đổi hoạt động trên một hàng hoặc cột đơn. Ví dụ: các di chuyển được triển khai thành 4 lần tra cứu vào một "bảng hiệu ứng di chuyển" được tính toán trước, mô tả cách mỗi lần di chuyển ảnh hưởng đến một hàng hoặc một cột (ví dụ: bảng "di chuyển sang phải" chứa mục "1122 -> 0023" mô tả cách thức hàng [2,2,4,4] trở thành hàng [0,0,4,8] khi di chuyển sang phải).

Ghi điểm cũng được thực hiện bằng cách sử dụng bảng tra cứu. Các bảng chứa điểm heuristic được tính trên tất cả các hàng / cột có thể và điểm tổng hợp cho một bảng chỉ đơn giản là tổng các giá trị bảng trên mỗi hàng và cột.

Đại diện bảng này, cùng với phương pháp tra cứu bảng để di chuyển và ghi điểm, cho phép AI tìm kiếm một số lượng lớn trạng thái trò chơi trong một khoảng thời gian ngắn (hơn 10.000.000 trạng thái trò chơi mỗi giây trên một lõi của máy tính xách tay giữa năm 2011 của tôi).

Bản thân tìm kiếm wishimax được mã hóa thành một tìm kiếm đệ quy xen kẽ giữa các bước "kỳ vọng" (kiểm tra tất cả các vị trí và giá trị của gạch có thể, và tính trọng số của chúng theo xác suất của từng khả năng) và các bước "tối đa hóa" và chọn một trong những điểm số tốt nhất). Việc tìm kiếm cây kết thúc khi nó nhìn thấy một vị trí đã thấy trước đó (sử dụng bảng chuyển vị ), khi nó đạt đến giới hạn độ sâu được xác định trước hoặc khi nó đạt đến trạng thái bảng rất khó xảy ra (ví dụ: nó đạt được bằng cách lấy các ô 6 "4" trong một hàng từ vị trí bắt đầu). Độ sâu tìm kiếm điển hình là 4-8 di chuyển.

Heuristic

Một số phương pháp phỏng đoán được sử dụng để hướng thuật toán tối ưu hóa tới các vị trí thuận lợi. Sự lựa chọn chính xác của heuristic có ảnh hưởng rất lớn đến hiệu suất của thuật toán. Các heuristic khác nhau được tính trọng số và kết hợp thành một điểm số vị trí, xác định mức độ "tốt" của một vị trí bảng nhất định. Việc tìm kiếm tối ưu hóa sau đó sẽ nhằm mục đích tối đa hóa điểm trung bình của tất cả các vị trí bảng có thể. Điểm thực tế, như được hiển thị trong trò chơi, không được sử dụng để tính điểm của hội đồng quản trị, vì nó quá nặng nề trong việc ủng hộ gạch hợp nhất (khi việc sáp nhập chậm có thể tạo ra lợi ích lớn).

Ban đầu, tôi đã sử dụng hai phương pháp phỏng đoán rất đơn giản, cấp "tiền thưởng" cho các ô vuông mở và để có các giá trị lớn ở rìa. Các heuristic này thực hiện khá tốt, thường đạt được 16384 nhưng không bao giờ đạt tới 32768.

Petr Morávek (@xificurk) đã lấy AI của tôi và thêm hai phương pháp phỏng đoán mới. Các heuristic đầu tiên là một hình phạt cho việc có các hàng và cột không đơn điệu tăng khi thứ hạng tăng lên, đảm bảo rằng các hàng nhỏ không đơn điệu sẽ không ảnh hưởng mạnh đến điểm số, nhưng các hàng lớn không đơn điệu làm tổn hại đáng kể đến điểm số. Các heuristic thứ hai tính số lượng hợp nhất tiềm năng (giá trị bằng nhau liền kề) ngoài không gian mở. Hai heuristic này phục vụ để đẩy thuật toán về phía các bảng đơn điệu (dễ hợp nhất hơn) và hướng tới các vị trí bảng với nhiều sự hợp nhất (khuyến khích nó sắp xếp hợp nhất khi có thể để có hiệu quả cao hơn).

Hơn nữa, Petr cũng tối ưu hóa các trọng số heuristic bằng chiến lược "tối ưu hóa meta" (sử dụng thuật toán gọi là CMA-ES ), trong đó chính các trọng số đã được điều chỉnh để đạt được điểm trung bình cao nhất có thể.

Hiệu quả của những thay đổi này là vô cùng quan trọng. Thuật toán đã đi từ việc đạt được ô 16384 khoảng 13% thời gian để đạt được hơn 90% thời gian và thuật toán bắt đầu đạt được 32768 trên 1/3 thời gian (trong khi các heuristic cũ chưa bao giờ tạo ra một lát 32768) .

Tôi tin rằng vẫn còn chỗ để cải thiện các heuristic. Thuật toán này chắc chắn chưa "tối ưu", nhưng tôi cảm thấy như nó đang đến khá gần.


Việc AI đạt được điểm số 32768 trong hơn một phần ba số trò chơi của mình là một cột mốc lớn; Tôi sẽ ngạc nhiên khi biết liệu có bất kỳ người chơi nào của con người đã đạt được 32768 trên trò chơi chính thức (tức là không sử dụng các công cụ như savestates hoặc hoàn tác). Tôi nghĩ rằng gạch 65536 là trong tầm tay!

Bạn có thể thử AI cho chính mình. Mã này có sẵn tại https://github.com/nneonneo/2048-ai .


12
@RobL: 2 xuất hiện 90% thời gian; 4 xuất hiện 10% thời gian. Nó nằm trong mã nguồn : var value = Math.random() < 0.9 ? 2 : 4;.
nneonneo

35
Hiện đang chuyển sang Cuda để GPU hoạt động với tốc độ thậm chí còn tốt hơn!
nimsson

25
@nneonneo Tôi đã chuyển mã của bạn bằng emscripten sang javascript và bây giờ nó hoạt động khá tốt trong trình duyệt ! Thật tuyệt khi xem, không cần phải biên dịch và mọi thứ ... Trong Firefox, hiệu suất khá tốt ...
Reverse_engineer

7
Giới hạn lý thuyết trong lưới 4 x 4 thực tế là IS 131072 chứ không phải 65536. Tuy nhiên, điều đó đòi hỏi phải có 4 đúng lúc (tức là toàn bộ bảng chứa đầy 4 .. 65536 mỗi lần - 15 trường bị chiếm) và bảng phải được thiết lập tại đó Khoảnh khắc để bạn thực sự có thể kết hợp.
Bodo Thiesen

5
@nneonneo Bạn có thể muốn kiểm tra AI của chúng tôi, có vẻ tốt hơn nữa, đạt 32k trong 60% số trò chơi: github.com/aszczepanski/2048
cauchy

1253

Tôi là tác giả của chương trình AI mà những người khác đã đề cập trong chủ đề này. Bạn có thể xem AI hoạt động hoặc đọc nguồn .

Hiện tại, chương trình đạt được tỷ lệ thắng 90% khi chạy bằng javascript trong trình duyệt trên máy tính xách tay của tôi trong khoảng 100 mili giây thời gian suy nghĩ cho mỗi lần di chuyển, do đó, dù không hoàn hảo (nhưng nó vẫn hoạt động khá tốt).

Vì trò chơi là một không gian trạng thái riêng biệt, thông tin hoàn hảo, trò chơi theo lượt như cờ vua và cờ đam, tôi đã sử dụng các phương pháp tương tự đã được chứng minh để hoạt động trên các trò chơi đó, cụ thể là tìm kiếm minimax với cắt tỉa alpha-beta . Vì đã có rất nhiều thông tin về thuật toán đó, tôi sẽ chỉ nói về hai phương pháp phỏng đoán chính mà tôi sử dụng trong hàm đánh giá tĩnh và chính thức hóa nhiều trực giác mà người khác đã thể hiện ở đây.

Tính đơn điệu

Heuristic này cố gắng đảm bảo rằng các giá trị của các ô đều tăng hoặc giảm dọc theo cả hai hướng trái / phải và lên / xuống. Heuristic này một mình nắm bắt trực giác mà nhiều người khác đã đề cập, rằng gạch có giá trị cao hơn nên được nhóm trong một góc. Nó thường sẽ ngăn các gạch có giá trị nhỏ hơn bị mồ côi và sẽ giữ cho bảng rất ngăn nắp, với các gạch nhỏ hơn xếp tầng và lấp đầy vào các gạch lớn hơn.

Đây là một ảnh chụp màn hình của một lưới hoàn toàn đơn điệu. Tôi đã đạt được điều này bằng cách chạy thuật toán với hàm eval được thiết lập để bỏ qua các heuristic khác và chỉ xem xét tính đơn điệu.

Một bảng 2048 hoàn toàn đơn điệu

Độ mịn

Các heuristic ở trên có xu hướng tạo ra các cấu trúc trong đó các gạch liền kề đang giảm giá trị, nhưng tất nhiên để hợp nhất, các gạch liền kề cần phải có cùng giá trị. Do đó, độ mịn heuristic chỉ đo sự khác biệt giá trị giữa các gạch lân cận, cố gắng giảm thiểu số lượng này.

Một nhà bình luận trên Hacker News đã đưa ra một hình thức thú vị về ý tưởng này về mặt lý thuyết đồ thị.

Đây là một ảnh chụp màn hình của một lưới hoàn toàn trơn tru, lịch sự của ngã ba nhại tuyệt vời này .

Một bảng 2048 hoàn toàn trơn tru

Gạch miễn phí

Và cuối cùng, có một hình phạt cho việc có quá ít gạch miễn phí, vì các tùy chọn có thể nhanh chóng hết khi bảng trò chơi quá chật chội.

Và đó là nó! Tìm kiếm trong không gian trò chơi trong khi tối ưu hóa các tiêu chí này mang lại hiệu suất tốt đáng kể. Một lợi thế của việc sử dụng một cách tiếp cận tổng quát như thế này thay vì một chiến lược di chuyển được mã hóa rõ ràng là thuật toán thường có thể tìm thấy các giải pháp thú vị và bất ngờ. Nếu bạn xem nó chạy, nó thường sẽ tạo ra những bước di chuyển đáng ngạc nhiên nhưng hiệu quả, như đột nhiên chuyển sang bức tường hoặc góc mà nó đang xây dựng để chống lại.

Biên tập:

Đây là một minh chứng cho sức mạnh của phương pháp này. Tôi đã khai thác các giá trị gạch (để nó tiếp tục sau khi đạt đến năm 2048) và đây là kết quả tốt nhất sau tám thử nghiệm.

4096

Vâng, đó là 4096 cùng với 2048. =) Điều đó có nghĩa là nó đã đạt được gạch 2048 khó nắm bắt ba lần trên cùng một bảng.


89
Bạn có thể coi máy tính đặt các ô '2' và '4' làm 'đối thủ'.
Ngụy Yên

29
@WeiYen Chắc chắn, nhưng liên quan đến nó như là một vấn đề tối thiểu không trung thành với logic trò chơi, bởi vì máy tính đang đặt các ô ngẫu nhiên với xác suất nhất định, thay vì cố tình giảm thiểu điểm số.
koo

57
Mặc dù AI đang đặt gạch ngẫu nhiên, mục tiêu không phải là mất. Không may mắn là điều tương tự như đối thủ chọn cách di chuyển tồi tệ nhất cho bạn. Phần "tối thiểu" có nghĩa là bạn cố gắng chơi một cách bảo thủ để không có những động tác khủng khiếp mà bạn có thể gặp xui xẻo.
FryGuy

196
Tôi đã có ý tưởng tạo ra một ngã ba năm 2048, trong đó máy tính thay vì đặt 2s và 4s ngẫu nhiên sử dụng AI của bạn để xác định vị trí đặt các giá trị. Kết quả: sự vô dụng tuyệt đối. Có thể dùng thử tại đây: sztupy.github.io/2048-Hard
SztupY 17/03/2016

30
@SztupY Wow, điều này thật xấu xa. Nhắc nhở tôi về qntm.org/hatetris Hatetris, người cũng cố gắng đặt tác phẩm sẽ cải thiện tình hình của bạn ít nhất.
Patashu 17/03/2016

145

Tôi bắt đầu thích thú với ý tưởng về AI cho trò chơi này không chứa trí thông minh được mã hóa cứng (tức là không có heuristic, chức năng ghi điểm, v.v.). AI chỉ nên "biết" các quy tắc của trò chơi và "tìm ra" cách chơi trò chơi. Điều này trái ngược với hầu hết các AI (như những người trong chủ đề này), nơi chơi trò chơi về cơ bản là lực lượng vũ trang được điều khiển bởi một chức năng ghi điểm đại diện cho sự hiểu biết của con người về trò chơi.

Thuật toán AI

Tôi tìm thấy một thuật toán chơi đơn giản nhưng đáng ngạc nhiên: Để xác định bước di chuyển tiếp theo cho một bảng nhất định, AI chơi trò chơi trong bộ nhớ bằng cách sử dụng các bước di chuyển ngẫu nhiên cho đến khi trò chơi kết thúc. Điều này được thực hiện nhiều lần trong khi theo dõi điểm số trò chơi kết thúc. Sau đó, điểm cuối trung bình cho mỗi di chuyển bắt đầu được tính toán. Di chuyển bắt đầu với điểm cuối trung bình cao nhất được chọn là bước tiếp theo.

Chỉ với 100 lần chạy (tức là trong các trò chơi trí nhớ) mỗi lần di chuyển, AI đạt được 2048 ô 80% số lần và 4096 ô 50% số lần. Sử dụng 10000 lần chạy sẽ nhận được gạch 2048 100%, 70% cho gạch 4096 và khoảng 1% cho gạch 8192.

Xem nó trong hành động

Điểm đạt được tốt nhất được hiển thị ở đây:

điểm cao nhất

Một sự thật thú vị về thuật toán này là trong khi các trò chơi ngẫu nhiên không có gì đáng ngạc nhiên là khá tệ, việc chọn nước đi tốt nhất (hoặc kém nhất) dẫn đến chơi trò chơi rất tốt: Một trò chơi AI điển hình có thể đạt 70000 điểm và 3000 lần di chuyển, nhưng trò chơi ngẫu nhiên trong bộ nhớ từ bất kỳ vị trí nhất định nào mang lại trung bình 340 điểm bổ sung trong khoảng 40 lần di chuyển thêm trước khi chết. (Bạn có thể tự mình nhìn thấy điều này bằng cách chạy AI và mở bảng điều khiển gỡ lỗi.)

Biểu đồ này minh họa điểm này: Đường màu xanh hiển thị điểm số của bảng sau mỗi lần di chuyển. Đường màu đỏ hiển thị điểm số trò chơi kết thúc chạy ngẫu nhiên tốt nhất của thuật toán từ vị trí đó. Về bản chất, các giá trị màu đỏ đang "kéo" các giá trị màu xanh lên trên về phía chúng, vì chúng là dự đoán tốt nhất của thuật toán. Thật thú vị khi thấy đường màu đỏ chỉ là một chút nhỏ phía trên đường màu xanh ở mỗi điểm, nhưng đường màu xanh tiếp tục tăng lên nhiều hơn nữa.

biểu đồ cho điểm

Tôi thấy khá ngạc nhiên khi thuật toán không cần phải thực sự thấy trước việc chơi trò chơi tốt để chọn các động tác tạo ra nó.

Tìm kiếm sau tôi thấy thuật toán này có thể được phân loại là thuật toán Tìm kiếm cây Monte Carlo thuần túy .

Thực hiện và liên kết

Đầu tiên tôi tạo một phiên bản JavaScript có thể thấy trong hành động ở đây . Phiên bản này có thể chạy 100 giây trong thời gian tốt. Mở bàn điều khiển để biết thêm thông tin. ( nguồn )

Sau đó, để chơi xung quanh một số chi tiết, tôi đã sử dụng cơ sở hạ tầng được tối ưu hóa cao @nneonneo và triển khai phiên bản của tôi trong C ++. Phiên bản này cho phép tối đa 100000 lượt chạy mỗi lần di chuyển và thậm chí 1000000 nếu bạn đủ kiên nhẫn. Hướng dẫn xây dựng được cung cấp. Nó chạy trong bảng điều khiển và cũng có một điều khiển từ xa để chơi phiên bản web. ( nguồn )

Các kết quả

Đáng ngạc nhiên, việc tăng số lần chạy không cải thiện đáng kể việc chơi trò chơi. Dường như có một giới hạn cho chiến lược này ở khoảng 80000 điểm với gạch 4096 và tất cả những cái nhỏ hơn, rất gần với gạch 8192 đạt được. Việc tăng số lần chạy từ 100 lên 100000 sẽ tăng tỷ lệ đạt được giới hạn điểm này (từ 5% đến 40%) nhưng không vượt qua được.

Chạy 10000 chạy với mức tăng tạm thời lên 1000000 gần các vị trí quan trọng được quản lý để phá vỡ rào cản này dưới 1% số lần đạt được điểm tối đa 129892 và ô 8192.

Cải tiến

Sau khi thực hiện thuật toán này, tôi đã thử nhiều cải tiến bao gồm sử dụng điểm tối thiểu hoặc tối đa hoặc kết hợp giữa min, max và avg. Tôi cũng đã thử sử dụng độ sâu: Thay vì thử K chạy mỗi lần di chuyển, tôi đã thử di chuyển K cho mỗi danh sách di chuyển có độ dài nhất định (ví dụ: "lên, lên, trái") và chọn nước đi đầu tiên trong danh sách di chuyển ghi điểm tốt nhất.

Sau đó, tôi đã thực hiện một cây ghi điểm có tính đến xác suất có điều kiện để có thể chơi di chuyển sau một danh sách di chuyển nhất định.

Tuy nhiên, không có ý tưởng nào trong số này cho thấy bất kỳ lợi thế thực sự nào so với ý tưởng đầu tiên đơn giản. Tôi đã để lại mã cho những ý tưởng này nhận xét trong mã C ++.

Tôi đã thêm một cơ chế "Tìm kiếm sâu" tạm thời tăng số lần chạy lên 1000000 khi bất kỳ lần chạy nào được quản lý để vô tình đạt đến ô cao nhất tiếp theo. Điều này cung cấp một cải tiến thời gian.

Tôi rất muốn biết liệu có ai có ý tưởng cải tiến khác để duy trì tính độc lập miền của AI không.

2048 Biến thể và Bản sao

Để giải trí, tôi cũng đã triển khai AI dưới dạng bookmarklet , nối vào các điều khiển của trò chơi. Điều này cho phép AI hoạt động với trò chơi gốc và nhiều biến thể của nó .

Điều này có thể là do tính chất độc lập miền của AI. Một số biến thể khá khác biệt, chẳng hạn như bản sao lục giác.


7
+1. Là một sinh viên AI tôi thấy điều này thực sự thú vị. Sẽ có một cái nhìn tốt hơn về điều này trong thời gian rảnh.
Isaac

4
Thật đáng kinh ngạc! Tôi chỉ mất hàng giờ để tối ưu hóa trọng lượng cho một chức năng heuristic tốt cho kỳ vọng và tôi thực hiện điều này trong 3 phút và điều này hoàn toàn phá vỡ nó.
Brendan Annable

8
Sử dụng tốt mô phỏng Monte Carlo.
nneonneo

5
Xem chơi này là kêu gọi một sự giác ngộ. Điều này thổi tất cả các heuristic và nó hoạt động. Xin chúc mừng !
Stéphane Gourichon

4
Cho đến nay, giải pháp thú vị nhất ở đây.
shebaw

126

EDIT: Đây là một thuật toán ngây thơ, mô hình hóa quá trình suy nghĩ có ý thức của con người và nhận được kết quả rất yếu so với AI tìm kiếm tất cả các khả năng vì nó chỉ nhìn về phía trước. Nó đã được gửi sớm trong dòng thời gian đáp ứng.

Tôi đã tinh chỉnh thuật toán và đánh bại trò chơi! Nó có thể thất bại do sự xui xẻo đơn giản ở gần cuối (bạn buộc phải di chuyển xuống, điều mà bạn không bao giờ nên làm, và một ô xuất hiện ở nơi cao nhất của bạn. Chỉ cần cố gắng giữ hàng trên cùng, để di chuyển sang trái không phá vỡ mô hình), nhưng về cơ bản, bạn sẽ có một phần cố định và một phần di động để chơi. Đây là mục tiêu của bạn:

Sẵn sàng hoàn thành

Đây là mô hình tôi chọn theo mặc định.

1024 512 256 128
  8   16  32  64
  4   2   x   x
  x   x   x   x

Góc được chọn là tùy ý, về cơ bản bạn không bao giờ nhấn một phím (di chuyển bị cấm) và nếu bạn làm thế, bạn nhấn lại lần nữa và cố gắng sửa nó. Đối với các ô tương lai, mô hình luôn hy vọng ô ngẫu nhiên tiếp theo là 2 và xuất hiện ở phía đối diện với mô hình hiện tại (trong khi hàng đầu tiên không đầy đủ, ở góc dưới bên phải, khi hàng đầu tiên được hoàn thành, ở phía dưới bên trái góc).

Đây là thuật toán. Khoảng 80% chiến thắng (có vẻ như luôn luôn có thể giành chiến thắng với các kỹ thuật AI "chuyên nghiệp" hơn, tuy nhiên tôi không chắc chắn về điều này.)

initiateModel();

while(!game_over)
{    
    checkCornerChosen(); // Unimplemented, but it might be an improvement to change the reference point

    for each 3 possible move:
        evaluateResult()
    execute move with best score
    if no move is available, execute forbidden move and undo, recalculateModel()
 }

 evaluateResult() {
     calculatesBestCurrentModel()
     calculates distance to chosen model
     stores result
 }

 calculateBestCurrentModel() {
      (according to the current highest tile acheived and their distribution)
  }

Một vài gợi ý về các bước còn thiếu. Đây:thay đổi mô hình

Mô hình đã thay đổi do may mắn được gần hơn với mô hình dự kiến. Mô hình mà AI đang cố gắng đạt được là

 512 256 128  x
  X   X   x   x
  X   X   x   x
  x   x   x   x

Và chuỗi để đạt được điều đó đã trở thành:

 512 256  64  O
  8   16  32  O
  4   x   x   x
  x   x   x   x

Các Ođại diện cấm không gian ...

Vì vậy, nó sẽ nhấn phải, sau đó lại đúng, sau đó (phải hoặc trên cùng tùy thuộc vào nơi 4 đã tạo) sau đó sẽ tiến hành hoàn thành chuỗi cho đến khi nhận được:

Chuỗi hoàn thành

Vì vậy, bây giờ mô hình và chuỗi đã trở lại:

 512 256 128  64
  4   8  16   32
  X   X   x   x
  x   x   x   x

Con trỏ thứ hai, nó đã gặp xui xẻo và vị trí chính của nó đã bị lấy mất. Có khả năng nó sẽ thất bại, nhưng nó vẫn có thể đạt được nó:

Nhập mô tả hình ảnh ở đây

Ở đây mô hình và chuỗi là:

  O 1024 512 256
  O   O   O  128
  8  16   32  64
  4   x   x   x

Khi nó đạt được mức 128, nó sẽ đạt được toàn bộ một hàng:

  O 1024 512 256
  x   x  128 128
  x   x   x   x
  x   x   x   x

execute move with best scoreLàm thế nào bạn có thể đánh giá điểm số tốt nhất trong số các trạng thái tiếp theo có thể?
Khaled.K

heuristic được định nghĩa trong evaluateResultbạn về cơ bản cố gắng đến gần với kịch bản tốt nhất có thể.
Daren

@Daren Tôi đang chờ chi tiết cụ thể của bạn
ashu

@ashu Tôi đang làm việc với nó, những tình huống bất ngờ đã khiến tôi không có thời gian để hoàn thành nó. Trong khi đó tôi đã cải tiến thuật toán và hiện tại nó giải quyết được 75% thời gian.
Daren

13
Điều tôi thực sự thích về chiến lược này là tôi có thể sử dụng nó khi chơi trò chơi một cách thủ công, nó đã giúp tôi đạt tới 37k điểm.
Cephalepad

94

Tôi sao chép ở đây nội dung của một bài đăng trên blog của tôi


Giải pháp tôi đề xuất rất đơn giản và dễ thực hiện. Mặc dù, nó đã đạt được số điểm 131040. Một số điểm chuẩn của các màn trình diễn thuật toán được trình bày.

Ghi bàn

Thuật toán

Thuật toán chấm điểm heuristic

Giả định dựa trên thuật toán của tôi khá đơn giản: nếu bạn muốn đạt được điểm cao hơn, bảng phải được giữ gọn gàng nhất có thể. Cụ thể, thiết lập tối ưu được đưa ra theo thứ tự giảm tuyến tính và đơn điệu của các giá trị gạch. Trực giác này sẽ cung cấp cho bạn giới hạn trên cho một giá trị ô: Strong đó n là số ô trên bảng.

(Có khả năng tiếp cận ô 131072 nếu ô 4 được tạo ngẫu nhiên thay vì ô 2 khi cần)

Hai cách tổ chức bảng có thể được hiển thị trong các hình ảnh sau:

nhập mô tả hình ảnh ở đây

Để thực thi việc sắp xếp các ô theo thứ tự giảm đơn điệu, điểm si được tính là tổng của các giá trị tuyến tính trên bảng nhân với các giá trị của chuỗi hình học với tỷ lệ chung r <1.

S

S

Một số đường dẫn tuyến tính có thể được đánh giá cùng một lúc, điểm cuối cùng sẽ là điểm tối đa của bất kỳ đường dẫn nào.

Quy tắc quyết định

Quy tắc quyết định được triển khai không hoàn toàn thông minh, mã trong Python được trình bày ở đây:

@staticmethod
def nextMove(board,recursion_depth=3):
    m,s = AI.nextMoveRecur(board,recursion_depth,recursion_depth)
    return m

@staticmethod
def nextMoveRecur(board,depth,maxDepth,base=0.9):
    bestScore = -1.
    bestMove = 0
    for m in range(1,5):
        if(board.validMove(m)):
            newBoard = copy.deepcopy(board)
            newBoard.move(m,add_tile=True)

            score = AI.evaluate(newBoard)
            if depth != 0:
                my_m,my_s = AI.nextMoveRecur(newBoard,depth-1,maxDepth)
                score += my_s*pow(base,maxDepth-depth+1)

            if(score > bestScore):
                bestMove = m
                bestScore = score
    return (bestMove,bestScore);

Việc triển khai minmax hoặc Expectiminimax chắc chắn sẽ cải thiện thuật toán. Rõ ràng một quy tắc quyết định phức tạp hơn sẽ làm chậm thuật toán và nó sẽ cần một thời gian để thực hiện. Tôi sẽ thử thực hiện minimax trong tương lai gần. (giữ nguyên)

Điểm chuẩn

  • T1 - 121 bài kiểm tra - 8 đường dẫn khác nhau - r = 0,125
  • Thử nghiệm T2 - 122 - 8 đường dẫn khác nhau - r = 0,25
  • T3 - 132 bài kiểm tra - 8 đường dẫn khác nhau - r = 0,5
  • Thử nghiệm T4 - 211 - 2 đường dẫn khác nhau - r = 0,125
  • Các thử nghiệm T5 - 274 - 2 đường dẫn khác nhau - r = 0,25
  • Các thử nghiệm T6 - 211 - 2 đường dẫn khác nhau - r = 0,5

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

Trong trường hợp của T2, bốn bài kiểm tra trong mười tạo ra gạch 4096 với điểm trung bình là S42000

Mã này có thể được tìm thấy trên GiHub tại liên kết sau: https://github.com/Nicola17/term2048-AI Nó dựa trên term2048 và nó được viết bằng Python. Tôi sẽ triển khai một phiên bản hiệu quả hơn trong C ++ càng sớm càng tốt.


Không tệ, minh họa của bạn đã cho tôi một ý tưởng, về việc đưa các vectơ hợp nhất vào đánh giá
Khaled.K

Xin chào. Bạn có chắc chắn các hướng dẫn được cung cấp trong trang github áp dụng cho dự án của bạn? Tôi muốn dùng thử nhưng những thứ đó dường như là hướng dẫn cho trò chơi có thể chơi ban đầu và không phải là tự động AI. Bạn có thể cập nhật những cái đó? Cảm ơn.
JD Gamboa

41

Nỗ lực của tôi sử dụng kỳ vọng như các giải pháp khác ở trên, nhưng không có bảng điều khiển. Giải pháp của Nneonneo có thể kiểm tra 10 triệu bước di chuyển, độ sâu xấp xỉ 4 với 6 gạch còn lại và 4 di chuyển có thể (2 * 6 * 4) 4 . Trong trường hợp của tôi, độ sâu này mất quá nhiều thời gian để khám phá, tôi điều chỉnh độ sâu của tìm kiếm kỳ vọng theo số lượng gạch miễn phí còn lại:

depth = free > 7 ? 1 : (free > 4 ? 2 : 3)

Điểm của các bảng được tính bằng tổng trọng số của bình phương số lượng gạch miễn phí và sản phẩm chấm của lưới 2D với điều này:

[[10,8,7,6.5],
 [.5,.7,1,3],
 [-.5,-1.5,-1.8,-2],
 [-3.8,-3.7,-3.5,-3]]

mà buộc phải tổ chức gạch giảm dần trong một loại rắn từ gạch trên cùng bên trái.

mã bên dưới hoặc trên github :

var n = 4,
	M = new MatrixTransform(n);

var ai = {weights: [1, 1], depth: 1}; // depth=1 by default, but we adjust it on every prediction according to the number of free tiles

var snake= [[10,8,7,6.5],
            [.5,.7,1,3],
            [-.5,-1.5,-1.8,-2],
            [-3.8,-3.7,-3.5,-3]]
snake=snake.map(function(a){return a.map(Math.exp)})

initialize(ai)

function run(ai) {
	var p;
	while ((p = predict(ai)) != null) {
		move(p, ai);
	}
	//console.log(ai.grid , maxValue(ai.grid))
	ai.maxValue = maxValue(ai.grid)
	console.log(ai)
}

function initialize(ai) {
	ai.grid = [];
	for (var i = 0; i < n; i++) {
		ai.grid[i] = []
		for (var j = 0; j < n; j++) {
			ai.grid[i][j] = 0;
		}
	}
	rand(ai.grid)
	rand(ai.grid)
	ai.steps = 0;
}

function move(p, ai) { //0:up, 1:right, 2:down, 3:left
	var newgrid = mv(p, ai.grid);
	if (!equal(newgrid, ai.grid)) {
		//console.log(stats(newgrid, ai.grid))
		ai.grid = newgrid;
		try {
			rand(ai.grid)
			ai.steps++;
		} catch (e) {
			console.log('no room', e)
		}
	}
}

function predict(ai) {
	var free = freeCells(ai.grid);
	ai.depth = free > 7 ? 1 : (free > 4 ? 2 : 3);
	var root = {path: [],prob: 1,grid: ai.grid,children: []};
	var x = expandMove(root, ai)
	//console.log("number of leaves", x)
	//console.log("number of leaves2", countLeaves(root))
	if (!root.children.length) return null
	var values = root.children.map(expectimax);
	var mx = max(values);
	return root.children[mx[1]].path[0]

}

function countLeaves(node) {
	var x = 0;
	if (!node.children.length) return 1;
	for (var n of node.children)
		x += countLeaves(n);
	return x;
}

function expectimax(node) {
	if (!node.children.length) {
		return node.score
	} else {
		var values = node.children.map(expectimax);
		if (node.prob) { //we are at a max node
			return Math.max.apply(null, values)
		} else { // we are at a random node
			var avg = 0;
			for (var i = 0; i < values.length; i++)
				avg += node.children[i].prob * values[i]
			return avg / (values.length / 2)
		}
	}
}

function expandRandom(node, ai) {
	var x = 0;
	for (var i = 0; i < node.grid.length; i++)
		for (var j = 0; j < node.grid.length; j++)
			if (!node.grid[i][j]) {
				var grid2 = M.copy(node.grid),
					grid4 = M.copy(node.grid);
				grid2[i][j] = 2;
				grid4[i][j] = 4;
				var child2 = {grid: grid2,prob: .9,path: node.path,children: []};
				var child4 = {grid: grid4,prob: .1,path: node.path,children: []}
				node.children.push(child2)
				node.children.push(child4)
				x += expandMove(child2, ai)
				x += expandMove(child4, ai)
			}
	return x;
}

function expandMove(node, ai) { // node={grid,path,score}
	var isLeaf = true,
		x = 0;
	if (node.path.length < ai.depth) {
		for (var move of[0, 1, 2, 3]) {
			var grid = mv(move, node.grid);
			if (!equal(grid, node.grid)) {
				isLeaf = false;
				var child = {grid: grid,path: node.path.concat([move]),children: []}
				node.children.push(child)
				x += expandRandom(child, ai)
			}
		}
	}
	if (isLeaf) node.score = dot(ai.weights, stats(node.grid))
	return isLeaf ? 1 : x;
}



var cells = []
var table = document.querySelector("table");
for (var i = 0; i < n; i++) {
	var tr = document.createElement("tr");
	cells[i] = [];
	for (var j = 0; j < n; j++) {
		cells[i][j] = document.createElement("td");
		tr.appendChild(cells[i][j])
	}
	table.appendChild(tr);
}

function updateUI(ai) {
	cells.forEach(function(a, i) {
		a.forEach(function(el, j) {
			el.innerHTML = ai.grid[i][j] || ''
		})
	});
}


updateUI(ai);
updateHint(predict(ai));

function runAI() {
	var p = predict(ai);
	if (p != null && ai.running) {
		move(p, ai);
		updateUI(ai);
		updateHint(p);
		requestAnimationFrame(runAI);
	}
}
runai.onclick = function() {
	if (!ai.running) {
		this.innerHTML = 'stop AI';
		ai.running = true;
		runAI();
	} else {
		this.innerHTML = 'run AI';
		ai.running = false;
		updateHint(predict(ai));
	}
}


function updateHint(dir) {
	hintvalue.innerHTML = ['↑', '→', '↓', '←'][dir] || '';
}

document.addEventListener("keydown", function(event) {
	if (!event.target.matches('.r *')) return;
	event.preventDefault(); // avoid scrolling
	if (event.which in map) {
		move(map[event.which], ai)
		console.log(stats(ai.grid))
		updateUI(ai);
		updateHint(predict(ai));
	}
})
var map = {
	38: 0, // Up
	39: 1, // Right
	40: 2, // Down
	37: 3, // Left
};
init.onclick = function() {
	initialize(ai);
	updateUI(ai);
	updateHint(predict(ai));
}


function stats(grid, previousGrid) {

	var free = freeCells(grid);

	var c = dot2(grid, snake);

	return [c, free * free];
}

function dist2(a, b) { //squared 2D distance
	return Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2)
}

function dot(a, b) {
	var r = 0;
	for (var i = 0; i < a.length; i++)
		r += a[i] * b[i];
	return r
}

function dot2(a, b) {
	var r = 0;
	for (var i = 0; i < a.length; i++)
		for (var j = 0; j < a[0].length; j++)
			r += a[i][j] * b[i][j]
	return r;
}

function product(a) {
	return a.reduce(function(v, x) {
		return v * x
	}, 1)
}

function maxValue(grid) {
	return Math.max.apply(null, grid.map(function(a) {
		return Math.max.apply(null, a)
	}));
}

function freeCells(grid) {
	return grid.reduce(function(v, a) {
		return v + a.reduce(function(t, x) {
			return t + (x == 0)
		}, 0)
	}, 0)
}

function max(arr) { // return [value, index] of the max
	var m = [-Infinity, null];
	for (var i = 0; i < arr.length; i++) {
		if (arr[i] > m[0]) m = [arr[i], i];
	}
	return m
}

function min(arr) { // return [value, index] of the min
	var m = [Infinity, null];
	for (var i = 0; i < arr.length; i++) {
		if (arr[i] < m[0]) m = [arr[i], i];
	}
	return m
}

function maxScore(nodes) {
	var min = {
		score: -Infinity,
		path: []
	};
	for (var node of nodes) {
		if (node.score > min.score) min = node;
	}
	return min;
}


function mv(k, grid) {
	var tgrid = M.itransform(k, grid);
	for (var i = 0; i < tgrid.length; i++) {
		var a = tgrid[i];
		for (var j = 0, jj = 0; j < a.length; j++)
			if (a[j]) a[jj++] = (j < a.length - 1 && a[j] == a[j + 1]) ? 2 * a[j++] : a[j]
		for (; jj < a.length; jj++)
			a[jj] = 0;
	}
	return M.transform(k, tgrid);
}

function rand(grid) {
	var r = Math.floor(Math.random() * freeCells(grid)),
		_r = 0;
	for (var i = 0; i < grid.length; i++) {
		for (var j = 0; j < grid.length; j++) {
			if (!grid[i][j]) {
				if (_r == r) {
					grid[i][j] = Math.random() < .9 ? 2 : 4
				}
				_r++;
			}
		}
	}
}

function equal(grid1, grid2) {
	for (var i = 0; i < grid1.length; i++)
		for (var j = 0; j < grid1.length; j++)
			if (grid1[i][j] != grid2[i][j]) return false;
	return true;
}

function conv44valid(a, b) {
	var r = 0;
	for (var i = 0; i < 4; i++)
		for (var j = 0; j < 4; j++)
			r += a[i][j] * b[3 - i][3 - j]
	return r
}

function MatrixTransform(n) {
	var g = [],
		ig = [];
	for (var i = 0; i < n; i++) {
		g[i] = [];
		ig[i] = [];
		for (var j = 0; j < n; j++) {
			g[i][j] = [[j, i],[i, n-1-j],[j, n-1-i],[i, j]]; // transformation matrix in the 4 directions g[i][j] = [up, right, down, left]
			ig[i][j] = [[j, i],[i, n-1-j],[n-1-j, i],[i, j]]; // the inverse tranformations
		}
	}
	this.transform = function(k, grid) {
		return this.transformer(k, grid, g)
	}
	this.itransform = function(k, grid) { // inverse transform
		return this.transformer(k, grid, ig)
	}
	this.transformer = function(k, grid, mat) {
		var newgrid = [];
		for (var i = 0; i < grid.length; i++) {
			newgrid[i] = [];
			for (var j = 0; j < grid.length; j++)
				newgrid[i][j] = grid[mat[i][j][k][0]][mat[i][j][k][1]];
		}
		return newgrid;
	}
	this.copy = function(grid) {
		return this.transform(3, grid)
	}
}
body {
	font-family: Arial;
}
table, th, td {
	border: 1px solid black;
	margin: 0 auto;
	border-collapse: collapse;
}
td {
	width: 35px;
	height: 35px;
	text-align: center;
}
button {
	margin: 2px;
	padding: 3px 15px;
	color: rgba(0,0,0,.9);
}
.r {
	display: flex;
	align-items: center;
	justify-content: center;
	margin: .2em;
	position: relative;
}
#hintvalue {
	font-size: 1.4em;
	padding: 2px 8px;
	display: inline-flex;
	justify-content: center;
	width: 30px;
}
<table title="press arrow keys"></table>
<div class="r">
    <button id=init>init</button>
    <button id=runai>run AI</button>
    <span id="hintvalue" title="Best predicted move to do, use your arrow keys" tabindex="-1"></span>
</div>


3
Không chắc chắn tại sao điều này không có nhiều upvote. Nó thực sự hiệu quả vì sự đơn giản của nó.
David Greydanus

Cảm ơn, câu trả lời muộn và nó hoạt động không thực sự tốt (hầu như luôn luôn trong [1024, 8192]), chức năng chi phí / thống kê cần nhiều công việc hơn
caub

Làm thế nào bạn có trọng lượng các không gian trống?
David Greydanus

1
Thật đơn giản cost=1x(number of empty tiles)²+1xdotproduct(snakeWeights,grid)và chúng tôi cố gắng tối đa hóa chi phí này
caub

cảm ơn @Robusto, tôi nên cải thiện mã một ngày nào đó, nó có thể được đơn giản hóa
caub

38

Tôi là tác giả của bộ điều khiển 2048 đạt điểm cao hơn bất kỳ chương trình nào khác được đề cập trong chủ đề này. Một triển khai hiệu quả của bộ điều khiển có sẵn trên github . Trong một repo riêng cũng có mã được sử dụng để huấn luyện chức năng đánh giá trạng thái của bộ điều khiển. Phương pháp đào tạo được mô tả trong bài báo .

Bộ điều khiển sử dụng tìm kiếm kỳ vọng với chức năng đánh giá trạng thái được học từ đầu (không có chuyên môn về con người năm 2048) bằng một biến thể của việc học khác biệt theo thời gian (một kỹ thuật học tăng cường). Hàm giá trị trạng thái sử dụng mạng n-tuple , về cơ bản là hàm tuyến tính trọng số của các mẫu được quan sát trên bảng. Nó liên quan đến hơn 1 tỷ trọng lượng , tổng cộng.

Hiệu suất

Với tốc độ 1 di chuyển / s: 609104 (trung bình 100 trò chơi)

Ở tốc độ 10 di chuyển / s: 589355 (trung bình 300 trò chơi)

Ở mức 3 ply (khoảng 1500 di chuyển / s): 511759 (trung bình 1000 trò chơi)

Thống kê ô cho 10 di chuyển / s như sau:

2048: 100%
4096: 100%
8192: 100%
16384: 97%
32768: 64%
32768,16384,8192,4096: 10%

(Dòng cuối cùng có nghĩa là có các ô đã cho cùng một lúc trên bảng).

Dành cho 3 lớp:

2048: 100%
4096: 100%
8192: 100%
16384: 96%
32768: 54%
32768,16384,8192,4096: 8%

Tuy nhiên, tôi chưa bao giờ quan sát thấy nó có được gạch 65536.


4
Kết quả khá ấn tượng. Tuy nhiên, bạn có thể cập nhật câu trả lời để giải thích (đại khái, nói một cách đơn giản ... Tôi chắc chắn rằng các chi tiết đầy đủ sẽ quá dài để đăng ở đây) làm thế nào chương trình của bạn đạt được điều này? Như trong một giải thích sơ bộ về cách thức hoạt động của thuật toán học tập?
Cedric Mamo

27

Tôi nghĩ rằng tôi đã tìm thấy một thuật toán hoạt động khá tốt, vì tôi thường đạt được số điểm trên 10000, tốt nhất cá nhân của tôi là khoảng 16000. Giải pháp của tôi không nhằm mục đích giữ các số lớn nhất ở một góc, mà để giữ nó ở hàng đầu.

Xin vui lòng xem mã dưới đây:

while( !game_over ) {
    move_direction=up;
    if( !move_is_possible(up) ) {
        if( move_is_possible(right) && move_is_possible(left) ){
            if( number_of_empty_cells_after_moves(left,up) > number_of_empty_cells_after_moves(right,up) ) 
                move_direction = left;
            else
                move_direction = right;
        } else if ( move_is_possible(left) ){
            move_direction = left;
        } else if ( move_is_possible(right) ){
            move_direction = right;
        } else {
            move_direction = down;
        }
    }
    do_move(move_direction);
}

5
Tôi đã chạy 100.000 trò chơi thử nghiệm điều này so với chiến lược tuần hoàn tầm thường "lên, phải, lên, trái, ..." (và xuống nếu cần). Chiến lược tuần hoàn đã hoàn thành "điểm số gạch trung bình" 770.6, trong khi chiến lược này chỉ đạt được 396.7. Bạn có đoán được tại sao lại như vậy không? Tôi nghĩ rằng nó làm quá nhiều up, ngay cả khi trái hoặc phải sẽ hợp nhất nhiều hơn nữa.
Thomas Ahle

1
Các gạch có xu hướng xếp chồng theo cách không tương thích nếu chúng không được dịch chuyển theo nhiều hướng. Nói chung, sử dụng chiến lược theo chu kỳ sẽ dẫn đến các ô lớn hơn ở trung tâm, khiến cho việc điều động trở nên chật chội hơn nhiều.
bcdan

25

Đã có một triển khai AI cho trò chơi này ở đây . Trích từ README:

Thuật toán được lặp đi sâu tìm kiếm alpha-beta sâu đầu tiên. Hàm đánh giá cố gắng giữ các hàng và cột đơn điệu (tất cả đều giảm hoặc tăng) trong khi giảm thiểu số lượng gạch trên lưới.

Ngoài ra còn có một cuộc thảo luận trên Hacker News về thuật toán này mà bạn có thể thấy hữu ích.


4
Đây sẽ là câu trả lời hàng đầu, nhưng sẽ rất hay nếu thêm chi tiết về cách triển khai: ví dụ: cách mô hình bảng trò chơi (dưới dạng biểu đồ), tối ưu hóa được sử dụng (tối thiểu sự khác biệt giữa các ô), v.v.
Alceu Costa

1
Đối với độc giả tương lai: Đây là chương trình tương tự được giải thích bởi tác giả của nó (ovolve) trong câu trả lời trên cùng thứ hai ở đây. Câu trả lời này và các đề cập khác về chương trình của ovolve trong cuộc thảo luận này, đã thúc đẩy ovolve xuất hiện và viết lên cách thuật toán của ông hoạt động; câu trả lời đó hiện có số điểm 1200.
MultiplyByZer0

23

Thuật toán

while(!game_over)
{
    for each possible move:
        evaluate next state

    choose the maximum evaluation
}

Đánh giá

Evaluation =
    128 (Constant)
    + (Number of Spaces x 128)
    + Sum of faces adjacent to a space { (1/face) x 4096 }
    + Sum of other faces { log(face) x 4 }
    + (Number of possible next moves x 256)
    + (Number of aligned values x 2)

Chi tiết đánh giá

128 (Constant)

Đây là một hằng số, được sử dụng làm đường cơ sở và cho các mục đích sử dụng khác như thử nghiệm.

+ (Number of Spaces x 128)

Nhiều không gian làm cho trạng thái linh hoạt hơn, chúng tôi nhân với 128 (là trung vị) vì một lưới chứa 128 mặt là trạng thái không thể tối ưu.

+ Sum of faces adjacent to a space { (1/face) x 4096 }

Ở đây chúng tôi đánh giá các khuôn mặt có khả năng hợp nhất, bằng cách đánh giá chúng ngược, gạch 2 trở thành giá trị 2048, trong khi gạch 2048 được đánh giá 2.

+ Sum of other faces { log(face) x 4 }

Ở đây, chúng ta vẫn cần kiểm tra các giá trị xếp chồng, nhưng theo cách ít hơn không làm gián đoạn các tham số linh hoạt, vì vậy chúng ta có tổng {x trong [4,44]}.

+ (Number of possible next moves x 256)

Một trạng thái linh hoạt hơn nếu nó có nhiều tự do chuyển đổi có thể.

+ (Number of aligned values x 2)

Đây là một kiểm tra đơn giản về khả năng hợp nhất trong trạng thái đó, mà không cần nhìn về phía trước.

Lưu ý: Các hằng số có thể được điều chỉnh ..


2
Tôi sẽ chỉnh sửa điều này sau, để thêm mã trực tiếp @ nitish712
Khaled.K

9
% Win của thuật toán này là gì?
cegprakash

Tại sao bạn cần một constant? Nếu tất cả những gì bạn đang làm là so sánh điểm số, thì điều đó ảnh hưởng đến kết quả của những so sánh đó như thế nào?
bcdan

@bcdan heuristic (hay còn gọi là điểm so sánh) phụ thuộc vào việc so sánh giá trị dự kiến ​​của trạng thái tương lai, tương tự như cách heuristic cờ vua hoạt động, ngoại trừ đây là một heuristic tuyến tính, vì chúng ta không xây dựng một cây để biết N bước tiếp theo tốt nhất
Khaled.K

12

Đây không phải là câu trả lời trực tiếp cho câu hỏi của OP, đây là nhiều nội dung (thử nghiệm) tôi đã cố gắng giải quyết vấn đề tương tự và thu được một số kết quả và có một số quan sát mà tôi muốn chia sẻ, tôi tò mò liệu chúng ta có thể có một số những hiểu biết sâu sắc hơn từ điều này.

Tôi vừa thử triển khai minimax với cắt tỉa alpha-beta với mức cắt sâu của cây tìm kiếm ở 3 và 5. Tôi đã cố gắng giải quyết vấn đề tương tự cho lưới 4 x 4 như một bài tập dự án cho khóa học edX ColumbiaX: CSMM.101x AI) .

Tôi đã áp dụng kết hợp lồi (đã thử các trọng số heuristic khác nhau) của một số hàm đánh giá heuristic, chủ yếu từ trực giác và từ các hàm được thảo luận ở trên:

  1. Tính đơn điệu
  2. Không gian trống có sẵn

Trong trường hợp của tôi, trình phát máy tính là hoàn toàn ngẫu nhiên, nhưng tôi vẫn giả định các cài đặt đối nghịch và triển khai tác nhân trình phát AI làm trình phát tối đa.

Tôi có lưới 4 x 4 để chơi game.

Quan sát:

Nếu tôi gán quá nhiều trọng số cho chức năng heuristic đầu tiên hoặc chức năng heuristic thứ hai, cả hai trường hợp điểm số mà người chơi AI đạt được đều thấp. Tôi đã chơi với nhiều bài tập trọng lượng có thể cho các hàm heuristic và thực hiện kết hợp lồi, nhưng rất hiếm khi người chơi AI có thể đạt điểm 2048. Hầu hết các lần nó dừng ở 1024 hoặc 512.

Tôi cũng đã thử góc heuristic, nhưng vì một số lý do nó làm cho kết quả tồi tệ hơn, có trực giác nào không?

Ngoài ra, tôi đã cố gắng tăng mức cắt giảm độ sâu tìm kiếm từ 3 lên 5 (Tôi không thể tăng thêm nữa vì tìm kiếm không gian vượt quá thời gian cho phép ngay cả khi cắt tỉa) và thêm một heuristic nhìn vào các giá trị của các ô liền kề và đưa ra nhiều điểm hơn nếu chúng có thể hợp nhất, nhưng tôi vẫn không thể có được 2048.

Tôi nghĩ sẽ tốt hơn nếu sử dụng Expectimax thay vì minimax, nhưng tôi vẫn muốn giải quyết vấn đề này chỉ với minimax và đạt được điểm số cao như 2048 hoặc 4096. Tôi không chắc liệu mình có thiếu gì không.

Bên dưới hình ảnh động cho thấy một vài bước cuối cùng của trò chơi được chơi bởi tác nhân AI với trình phát máy tính:

nhập mô tả hình ảnh ở đây

Bất kỳ hiểu biết sẽ thực sự rất hữu ích, cảm ơn trước. (Đây là liên kết của bài viết trên blog của tôi cho bài viết này: https://sandipanweb.wordpress.com/2017/03/06/using-minimax-with-alpha-beta-pruning-and-heuristic-evaluation-to-solve -2048-trò chơi với máy tính / và video youtube: https://www.youtube.com/watch?v=VnVFilfZ0r4 )

Hoạt hình sau đây cho thấy một vài bước cuối cùng của trò chơi được chơi trong đó tác nhân người chơi AI có thể nhận được 2048 điểm, lần này cũng thêm giá trị tuyệt đối heuristic:

nhập mô tả hình ảnh ở đây

Các hình dưới đây cho thấy cây trò chơi được khám phá bởi tác nhân AI của người chơi giả định máy tính là đối thủ chỉ trong một bước:

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây


9

Tôi đã viết một bộ giải 2048 bằng Haskell, chủ yếu vì tôi đang học ngôn ngữ này ngay bây giờ.

Việc triển khai trò chơi của tôi hơi khác so với trò chơi thực tế, trong đó một ô mới luôn là '2' (thay vì 90% 2 và 10% 4). Và rằng gạch mới không phải là ngẫu nhiên, mà luôn là cái đầu tiên có sẵn từ trên cùng bên trái. Biến thể này còn được gọi là Det 2048 .

Kết quả là, người giải quyết này là xác định.

Tôi đã sử dụng một thuật toán đầy đủ ủng hộ gạch trống. Nó hoạt động khá nhanh ở độ sâu 1-4, nhưng ở độ sâu 5, nó trở nên khá chậm với tốc độ khoảng 1 giây mỗi lần di chuyển.

Dưới đây là mã thực hiện thuật toán giải. Lưới được biểu diễn dưới dạng một dãy số nguyên dài 16. Và ghi bàn được thực hiện đơn giản bằng cách đếm số ô vuông trống.

bestMove :: Int -> [Int] -> Int
bestMove depth grid = maxTuple [ (gridValue depth (takeTurn x grid), x) | x <- [0..3], takeTurn x grid /= [] ]

gridValue :: Int -> [Int] -> Int
gridValue _ [] = -1
gridValue 0 grid = length $ filter (==0) grid  -- <= SCORING
gridValue depth grid = maxInList [ gridValue (depth-1) (takeTurn x grid) | x <- [0..3] ]

Tôi nghĩ nó khá thành công vì sự đơn giản của nó. Kết quả mà nó đạt được khi bắt đầu với một lưới trống và giải quyết ở độ sâu 5 là:

Move 4006
[2,64,16,4]
[16,4096,128,512]
[2048,64,1024,16]
[2,4,16,2]

Game Over

Mã nguồn có thể được tìm thấy ở đây: https://github.com/popovitsj/2048-haskell


Cố gắng mở rộng nó với các quy tắc thực tế. Đó là một thử thách tốt trong việc tìm hiểu về trình tạo ngẫu nhiên của Haskell!
Thomas Ahle

Tôi đã rất thất vọng với Haskell khi cố gắng làm điều đó, nhưng có lẽ tôi sẽ thử lại lần thứ hai! Tôi đã thấy rằng trò chơi trở nên dễ dàng hơn đáng kể mà không cần ngẫu nhiên.
wvdz

Nếu không có sự ngẫu nhiên, tôi khá chắc chắn rằng bạn có thể tìm ra cách để luôn nhận được 16k hoặc 32k. Tuy nhiên, ngẫu nhiên trong Haskell không phải là xấu, bạn chỉ cần một cách để vượt qua 'hạt giống'. Hoặc làm điều đó một cách rõ ràng, hoặc với đơn nguyên ngẫu nhiên.
Thomas Ahle

Tinh chỉnh thuật toán để nó luôn đạt 16k / 32k cho một trò chơi không ngẫu nhiên có thể là một thử thách thú vị khác ...
wvdz

Bạn nói đúng, nó khó hơn tôi nghĩ. Tôi đã tìm thấy chuỗi này: [LÊN, TRÁI, TRÁI, LÊN, TRÁI, XUỐNG, TRÁI] luôn thắng trò chơi, nhưng nó không vượt quá 2048. (Trong trường hợp không di chuyển hợp pháp, thuật toán chu kỳ chỉ chọn cái tiếp theo theo thứ tự theo chiều kim đồng hồ)
Thomas Ahle

6

Thuật toán này không tối ưu để chiến thắng trò chơi, nhưng nó khá tối ưu về hiệu suất và số lượng mã cần thiết:

  if(can move neither right, up or down)
    direction = left
  else
  {
    do
    {
      direction = random from (right, down, up)
    }
    while(can not move in "direction")
  }

10
nó hoạt động tốt hơn nếu bạn nói random from (right, right, right, down, down, up) như vậy không phải tất cả các động thái đều có xác suất như nhau. :)
Daren

3
Trên thực tế, nếu bạn hoàn toàn mới với trò chơi, nó thực sự giúp chỉ sử dụng 3 phím, về cơ bản thuật toán này làm gì. Vì vậy, không tệ như nó thoạt nhìn.
Chữ số

5
Vâng, nó dựa trên quan sát của riêng tôi với trò chơi. Cho đến khi bạn phải sử dụng hướng thứ 4, trò chơi sẽ tự giải quyết mà không cần quan sát. "AI" này sẽ có thể đạt tới 512/1024 mà không cần kiểm tra giá trị chính xác của bất kỳ khối nào.
API-Beast

3
Một AI thích hợp sẽ cố gắng tránh đến trạng thái mà nó chỉ có thể di chuyển sang một hướng bằng mọi giá.
API-Beast

3
Chỉ sử dụng 3 hướng thực sự là một chiến lược rất tốt! Nó chỉ cho tôi gần đến năm 2048 chơi trò chơi bằng tay. Nếu bạn kết hợp điều này với các chiến lược khác để quyết định giữa 3 động tác còn lại, nó có thể rất mạnh mẽ. Chưa kể rằng việc giảm sự lựa chọn xuống còn 3 có tác động lớn đến hiệu suất.
wvdz

4

Nhiều câu trả lời khác sử dụng AI với tính toán tìm kiếm tốn kém về tương lai có thể, heuristic, học tập và những thứ tương tự. Đây là những ấn tượng và có lẽ là cách chính xác về phía trước, nhưng tôi muốn đóng góp một ý tưởng khác.

Mô hình loại chiến lược mà người chơi giỏi của trò chơi sử dụng.

Ví dụ:

13 14 15 16
12 11 10  9
 5  6  7  8
 4  3  2  1

Đọc các ô vuông theo thứ tự hiển thị ở trên cho đến khi giá trị bình phương tiếp theo lớn hơn giá trị hiện tại. Điều này trình bày vấn đề cố gắng hợp nhất một ô khác có cùng giá trị vào ô vuông này.

Để giải quyết vấn đề này, có hai cách để di chuyển mà không để lại hoặc tệ hơn và kiểm tra cả hai khả năng có thể ngay lập tức phát hiện ra nhiều vấn đề hơn, đây là danh sách các phụ thuộc, mỗi vấn đề cần giải quyết vấn đề khác trước. Tôi nghĩ rằng tôi có chuỗi này hoặc trong một số trường hợp, cây phụ thuộc trong nội bộ khi quyết định bước đi tiếp theo của tôi, đặc biệt là khi bị mắc kẹt.


Ngói cần hợp nhất với hàng xóm nhưng quá nhỏ: Hợp nhất một hàng xóm khác với cái này.

Gạch lớn hơn theo cách: Tăng giá trị của gạch nhỏ hơn xung quanh.

Vân vân...


Toàn bộ cách tiếp cận có thể sẽ phức tạp hơn thế này nhưng không phức tạp hơn nhiều. Nó có thể là cơ học này trong cảm giác thiếu điểm số, trọng lượng, tế bào thần kinh và tìm kiếm sâu các khả năng. Cây khả năng thậm chí cần phải đủ lớn để cần bất kỳ nhánh nào.


5
Bạn đang mô tả một tìm kiếm địa phương với heuristic. Điều đó sẽ khiến bạn bị mắc kẹt, vì vậy bạn cần lên kế hoạch trước cho các bước tiếp theo. Điều đó lần lượt dẫn bạn đến một tìm kiếm và chấm điểm các giải pháp (để quyết định). Vì vậy, điều này thực sự không khác biệt so với bất kỳ giải pháp trình bày khác.
runDOSrun
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.