게임 2048에 가장 적합한 알고리즘은 무엇입니까?


1919

나는 최근에 게임 2048을 우연히 발견했다 . 비슷한 타일을 네 방향 중 하나로 움직여 "더 큰"타일을 만듭니다. 이동할 때마다 새 타일이 임의의 빈 위치에 2또는 값 중 하나로 나타납니다 4. 모든 상자가 채워지고 타일을 병합 할 수있는 동작이 없거나 값이 인 타일을 만들면 게임이 종료됩니다 2048.

하나, 목표를 달성하기 위해 잘 정의 된 전략을 따라야합니다. 그래서 나는 그것을위한 프로그램을 작성하는 것을 생각했습니다.

내 현재 알고리즘 :

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
}

내가하고있는 일은 언제든지 타일을 값 2과 병합 4하려고 시도 합니다. 즉, 가능한 최소 24타일을 사용 하려고 합니다. 이 방법으로 시도하면 다른 모든 타일이 자동으로 병합되고 전략이 좋아 보입니다.

그러나 실제로이 알고리즘을 사용하면 게임이 끝나기 전에 약 4000 포인트 만 얻습니다. 최대 점수 AFAIK는 현재 점수보다 훨씬 큰 20,000 점을 약간 넘습니다. 위보다 더 나은 알고리즘이 있습니까?


84
도움이 될 것입니다! ov3y.github.io/2048-AI
cegprakash

5
당신이 가지고 있기 때문에 @ 그런데 nitish712, 당신의 알고리즘은 욕심 choose the move with large number of merges신속하게 지역 최적해로 이어질하는
Khaled.K

21
500 InternalServerError @ : 경우에 나는 알파 - 베타 게임 나무 가지 치기와 인공 지능을 구현했다, 새로운 블록이 adversarially 배치된다고 가정 할 것이다. 최악의 가정이지만 유용 할 수 있습니다.
Charles

6
높은 점수를 목표로 할 시간이 없을 때 재미있는 산만 : 가능한 최저 점수를 얻으십시오. 이론적으로 2와 4가 번갈아 나타납니다.
Mark Hurd

7
이 질문의 정당성에 대한 논의는 메타에서 찾을 수 있습니다 : meta.stackexchange.com/questions/227266/...
제론 Vannevel에게

답변:


1266

@ovolve의 알고리즘에서 사용하는 minimax 검색 대신 expectimax 최적화를 사용하여 2048 AI를 개발했습니다 . AI는 모든 가능한 움직임에 대한 최대화를 수행 한 다음 가능한 모든 타일 스폰에 대한 기대치 (타일 확률에 따라 가중, 즉 4의 경우 10 %, 2의 경우 90 %)를 수행합니다. 내가 아는 한, 기대 최적화를 제거 할 수는 없으며 (매우 드물게 가지를 제거하는 것을 제외하고) 사용 된 알고리즘은 신중하게 최적화 된 무차별 대입 검색입니다.

공연

기본 구성의 AI (최대 검색 깊이 8)는 보드 위치의 복잡성에 따라 이동을 실행하는 데 10ms에서 200ms까지 걸립니다. 테스트에서 AI는 전체 게임을 진행하는 동안 초당 평균 이동 속도가 5-10 회 달성합니다. 검색 깊이가 6 이동으로 제한되면 AI는 초당 20+ 이동을 쉽게 실행할 수있어 재미있는 시청이 가능 합니다.

AI의 점수 성능을 평가하기 위해 AI를 100 회 실행했습니다 (리모콘을 통해 브라우저 게임에 연결됨). 각 타일에 대해 해당 타일을 한 번 이상 달성 한 게임 비율은 다음과 같습니다.

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

모든 경기에서 최소 점수는 124024입니다. 달성 한 최대 점수는 794076입니다. 평균 점수는 387222입니다. AI는 2048 타일을 얻지 못했습니다 (100 게임에서 한 번이라도 게임을 잃지 않았습니다). 실제로, 매 실행마다 적어도 한 번 8192 타일을 달성했습니다 !

최고의 실행의 스크린 샷은 다음과 같습니다.

32768 타일, 794076 점

이 게임은 96 분에 걸쳐 27830 번의 이동 또는 초당 평균 4.8 개의 움직임이 필요했습니다.

이행

내 접근법은 전체 보드 (16 개 항목)를 단일 64 비트 정수로 인코딩합니다 (여기서 타일은 nybbles, 즉 4 비트 청크). 64 비트 시스템에서는 전체 보드를 단일 시스템 레지스터로 전달할 수 있습니다.

비트 시프트 연산은 개별 행과 열을 추출하는 데 사용됩니다. 단일 행 또는 열은 16 비트 수량이므로 크기가 65536 인 테이블은 단일 행 또는 열에서 작동하는 변환을 인코딩 할 수 있습니다. 예를 들어, 이동은 각 이동이 단일 행 또는 열에 미치는 영향을 설명하는 사전 계산 된 "이동 효과 테이블"에 대한 4 개의 조회로 구현됩니다 (예 : "오른쪽 이동"테이블에는 "1122-> 0023"항목이 포함되어 있습니다. 행 [2,2,4,4]는 오른쪽으로 이동하면 행 [0,0,4,8]이됩니다.

스코어링은 테이블 조회를 사용하여 수행됩니다. 테이블에는 가능한 모든 행 / 열에서 계산 된 휴리스틱 점수가 포함되며 보드의 결과 점수는 각 행과 열의 테이블 값 합계입니다.

이 보드 표현은 이동 및 스코어링을위한 테이블 조회 접근 방식과 함께 AI가 짧은 기간 (2011 년 중반 노트북의 한 코어에서 초당 10,000,000 개 이상의 게임 상태)으로 수많은 게임 상태를 검색 할 수 있도록합니다.

expectimax 검색 자체는 "예측"단계 (가능한 모든 타일 스폰 위치 및 값 테스트 및 각 가능성의 확률에 따라 최적화 된 점수 가중치 적용)와 "최대화"단계 (모든 가능한 이동 테스트) 사이를 번갈아 반복하는 재귀 검색으로 코딩됩니다. 최고 점수를 가진 사람을 선택). 트리 검색은 이전에 본 위치 ( 조옮김 테이블 사용 )를 볼 때, 사전 정의 된 깊이 제한에 도달하거나 보드 상태가 매우 높지 않을 때 (예 : 6 "4"타일을 가져 와서 도달 한 경우) 종료됩니다 시작 위치에서 연속해서). 일반적인 검색 깊이는 4-8 이동입니다.

휴리스틱

최적화 알고리즘을 유리한 위치로 향하게하는 데 몇 가지 휴리스틱이 사용됩니다. 휴리스틱을 정확하게 선택하면 알고리즘 성능에 큰 영향을 미칩니다. 다양한 휴리스틱이 가중되고 위치 점수로 결합되어 주어진 보드 위치가 얼마나 "좋은"지를 결정합니다. 그런 다음 최적화 검색은 가능한 모든 보드 위치의 평균 점수를 최대화하는 것을 목표로합니다. 게임에서 볼 수 있듯이 실제 점수 는 보드 점수를 계산하는 데 사용 되지 않습니다. 타일 ​​병합을 선호하여 너무 가중되기 때문입니다 (지연된 병합이 큰 이점을 가져올 수있는 경우).

처음에는 매우 간단한 두 가지 휴리스틱을 사용하여 열린 사각형에 "보너스"를 부여하고 가장자리에 큰 값을 지정했습니다. 이러한 휴리스틱은 꽤 잘 수행되어 16384를 달성하지만 32768에 도달하지는 않습니다.

Petr Morávek (@xificurk)은 AI를 가져 와서 새로운 휴리스틱을 추가했습니다. 첫 휴리스틱은 순위가 증가함에 따라 비-단조 행과 열을 갖는 것에 대한 페널티로, 작은 숫자의 비-단조 행이 점수에 큰 영향을 미치지 않을 것이지만, 큰 숫자의 비-단조 행이 점수를 크게 손상시킵니다. 두 번째 휴리스틱은 열린 공간 외에도 잠재적 병합 수 (인접 동일한 값)를 계산했습니다. 이 두 휴리스틱은 알고리즘을 단조로운 보드 (병합이 더 쉬운) 및 많은 병합이있는 보드 위치로 푸시하는 데 도움이되었습니다 (가능한 경우 병합을보다 효과적으로 적용하도록 권장).

또한 Petr는 "메타 최적화"전략 ( CMA-ES 라는 알고리즘 사용)을 사용하여 휴리스틱 가중치를 최적화했습니다 . 여기서 가중치 자체는 가능한 가장 높은 평균 점수를 얻도록 조정되었습니다.

이러한 변경의 효과는 매우 중요합니다. 알고리즘은 16384 타일을 시간의 약 13 %에서 90 % 이상 달성하는 것으로 전환했으며, 알고리즘은 시간의 1/3에 걸쳐 32768을 달성하기 시작했습니다. .

휴리스틱에 여전히 개선의 여지가 있다고 생각합니다. 이 알고리즘은 아직 "최적화"되지는 않았지만 꽤 가까워지고 있다고 생각합니다.


AI가 게임의 1/3 이상에서 32768 타일을 달성한다는 것은 큰 이정표입니다. 공식 게임에서 인간 플레이어가 32768을 달성했다면 (즉, 저장 상태 나 실행 취소와 같은 도구를 사용하지 않고) 들었을 때 나는 놀랄 것입니다. 65536 타일이 도달 할 수 있다고 생각합니다!

AI를 직접 사용해 볼 수 있습니다. 코드는 https://github.com/nneonneo/2048-ai 에서 사용할 수 있습니다 .


12
@RobL : 2는 90 %의 시간으로 나타납니다. 4는 10 %의 시간으로 나타납니다. 그것은에서의 소스 코드 : var value = Math.random() < 0.9 ? 2 : 4;.
nneonneo

35
현재 Cuda로 포팅되어 GPU가 더 빠른 속도로 작동합니다!
nimsson

25
@nneonneo 나는 당신의 코드를 emscripten과 함께 javascript로 이식 했으며, 이제 브라우저에서 잘 작동합니다 ! 컴파일 할 필요없이 모든 것을 볼 수있는 멋진 기능 ... Firefox에서는 성능이 매우 뛰어납니다 ...
reverse_engineer

6
4x4 그리드의 이론적 한계는 실제로 65536이 아닌 131072입니다. 그러나 올바른 순간에 4를 가져와야합니다 (즉, 전체 보드는 한 번에 15 개의 필드를 한 번에 15 개씩 채워짐). 실제로 결합 할 수 있도록 순간.
Bodo Thiesen

5
: @nneonneo 당신은 60 게임 %에서 32K로 받고, 더 나은 것 같습니다 우리의 AI 확인 할 수 있습니다 github.com/aszczepanski/2048
코시

1253

저는이 글에서 다른 사람들이 언급 한 AI 프로그램의 저자입니다. AI를 실제로 보거나 소스를 읽을 수 있습니다 .

현재이 프로그램은 랩톱 브라우저에서 자바 스크립트로 실행하면 약 90 %의 승률을 달성하여 이동 당 약 100 밀리 초의 사고 시간을 제공하므로 완벽하지는 않지만 잘 수행됩니다.

이 게임은 체스와 체커와 같은 개별 상태 공간, 완벽한 정보, 턴 기반 게임이므로 알파 베타 전정을 사용한 미니 맥스 검색 과 같은 게임에서 작동하는 것으로 입증 된 동일한 방법을 사용했습니다 . 이미 그 알고리즘에 대한 많은 정보가 있기 때문에 정적 평가 함수 에서 사용 하고 다른 사람들이 여기에서 표현한 많은 직관을 공식화 하는 두 가지 주요 휴리스틱에 대해 이야기 하겠습니다.

단조

이 휴리스틱은 타일 값이 모두 왼쪽 / 오른쪽 및 위 / 아래 방향을 따라 증가 또는 감소하는지 확인하려고합니다. 이 휴리스틱만으로도 많은 다른 사람들이 언급 한 직관을 포착 할 수 있습니다. 일반적으로 값이 작은 타일이 고아가되는 것을 방지하고 작은 타일이 계단식으로 배열되어 큰 타일에 채워져 보드가 매우 체계적으로 유지됩니다.

다음은 완벽하게 단조로운 격자의 스크린 샷입니다. 나는 다른 휴리스틱을 무시하고 단 조성을 고려하기 위해 eval 함수로 알고리즘을 실행하여 이것을 얻었습니다.

완벽하게 단조로운 2048 보드

부드러움

위의 휴리스틱만으로도 인접한 타일의 가치가 감소하는 구조를 만드는 경향이 있지만 물론 병합하기 위해서는 인접한 타일의 값이 동일해야합니다. 따라서 매끄러움 휴리스틱은 인접한 타일 간의 값 차이를 측정하여이 수를 최소화하려고합니다.

해커 뉴스 (Hacker News)에 대한 논평자는 그래프 이론 측면에서이 아이디어 의 흥미로운 공식화 를 제시했습니다.

이 우수한 패러디 포크 에 의해 완벽하게 매끄러운 그리드의 스크린 샷 있습니다.

완벽하게 매끄러운 2048 보드

무료 타일

마지막으로, 게임 타일이 너무 좁아지면 옵션이 빨리 소진 될 수 있기 때문에 무료 타일이 너무 적 으면 패널티가 부과됩니다.

그리고 그게 다야! 이러한 기준을 최적화하면서 게임 공간을 검색하면 성능이 크게 향상됩니다. 명시 적으로 코딩 된 이동 전략 대신 이와 같은 일반화 된 접근 방식을 사용하는 한 가지 장점은 알고리즘이 종종 흥미롭고 예기치 않은 솔루션을 찾을 수 있다는 것입니다. 만약 당신이 그것을 달리는 것을보고 있다면, 벽이나 구석을 갑자기 바꾸는 것과 같이 놀랍지 만 효과적인 움직임을 보일 것입니다.

편집하다:

다음은이 접근법의 힘에 대한 데모입니다. 타일 ​​값을 풀었습니다 (그래서 2048에 도달 한 후에 계속 진행됨). 여덟 번의 시도 후에 가장 좋은 결과가 있습니다.

4096

그렇습니다. 2048과 함께 4096입니다. =) 같은 보드에서 2048 타일을 3 번 달성했습니다.


89
'2'및 '4'타일을 배치 한 컴퓨터를 '선택적'으로 취급 할 수 있습니다.
Wei Yen

29
@WeiYen Sure, 그러나 컴퓨터가 의도적으로 점수를 최소화하는 대신 특정 확률로 타일을 무작위로 배치하기 때문에 최소 논리 문제로 간주하는 것은 게임 논리에 충실하지 않습니다.
koo

57
AI가 타일을 무작위로 배치하더라도 목표는 잃지 않는 것입니다. 불행한 것은 상대방이 당신을 위해 최악의 움직임을 선택하는 것과 같습니다. "최소한"부분은 당신이 불행하게도 할 수있는 끔찍한 움직임이 없도록 보수적으로 게임을한다는 것을 의미합니다.
FryGuy

196
2048의 포크를 만드는 아이디어가 있었는데, 2와 4를 배치하는 대신 컴퓨터가 AI를 무작위로 사용하여 값을 넣을 위치를 결정합니다. 결과 : 순전히 불가능 함. 여기에서 시험해 볼 수 있습니다 : sztupy.github.io/2048-Hard
SztupY

30
@SztupY 와우, 이것은 악하다. qntm.org/hatetris Hatetris를 상기시켜 줍니다. 또한 Hatetris는 귀하의 상황을 가장 적게 개선 할 수있는 부분을 배치하려고합니다.
Patashu

145

나는 하드 코딩 된 인텔리전스 (휴리스틱, 스코어링 기능 등) 가 없는 이 게임의 AI 아이디어에 관심을 갖게되었습니다 . AI는 게임 규칙 만 "알고" 게임 플레이를 "피겨 내기" 해야합니다. 이것은 게임 플레이가 본질적으로 게임에 대한 인간의 이해를 나타내는 스코어링 기능에 의해 조종되는 무차별적인 힘 (이 스레드의 것과 같은)과 대부분의 AI와 대조적입니다.

AI 알고리즘

간단하지만 놀랍게도 좋은 재생 알고리즘을 발견했습니다. 주어진 보드의 다음 움직임을 결정하기 위해 AI는 게임 끝날 때까지 임의의 움직임을 사용하여 메모리에서 게임을 재생합니다 . 이것은 최종 게임 점수를 추적하면서 여러 번 수행됩니다. 그런 다음 시작 이동 당 평균 종료 점수 가 계산됩니다. 평균 점수가 가장 높은 시작 동작이 다음 동작으로 선택됩니다.

AI는 한 번에 100 번의 런 (메모리 게임에서)으로 2048 타일을 80 %, 4096 타일을 50 % 달성합니다. 10000 런을 사용하면 2048 타일 100 %, 4096 타일 70 %, 8192 타일 약 1 %를 얻습니다.

실제로보기

최고 점수는 다음과 같습니다.

최고 점수

이 알고리즘에 대한 흥미로운 사실은 랜덤 플레이 게임은 당연히 아주 나쁘지만 최상의 (또는 가장 나쁜) 움직임을 선택하면 매우 좋은 게임 플레이로 이어진다는 것입니다. 특정 위치의 메모리 내 랜덤 플레이 게임은 죽기 전에 약 40 회의 추가 이동으로 평균 340 개의 추가 점수를 얻습니다. AI를 실행하고 디버그 콘솔을 열어서 직접 확인할 수 있습니다.

이 그래프는이 점을 보여줍니다. 파란색 선은 각 이동 후 보드 점수를 나타냅니다. 빨간색 선은 해당 위치에서 알고리즘의 최고의 랜덤 런 엔드 게임 점수를 보여줍니다 . 본질적으로 빨간색 값은 알고리즘의 가장 좋은 추측이므로 파란색 값을 위쪽으로 "풀"합니다. 빨간색 선이 각 지점에서 파란색 선보다 약간 위에있는 것을 보는 것은 흥미롭지 만 파란색 선은 계속 증가하고 있습니다.

득점 그래프

나는 알고리즘이 그것을 생성하는 움직임을 선택하기 위해 실제로 좋은 게임 플레이를 예측할 필요가 없다는 것이 놀랍습니다.

나중에 검색하면이 알고리즘이 Pure Monte Carlo Tree Search 알고리즘 으로 분류 될 수 있음을 알았습니다 .

구현 및 링크

먼저 여기에서 작동 하는 JavaScript 버전을 만들었습니다 . 이 버전은 적절한 시간에 100 회의 실행을 수행 할 수 있습니다. 추가 정보를 보려면 콘솔을여십시오. ( 소스 )

나중에 더 많은 것을 즐기기 위해 @nneonneo 고도로 최적화 된 인프라를 사용하고 내 버전을 C ++로 구현했습니다. 이 버전을 사용하면 이동 당 최대 100000 회의 실행이 가능하며 인내심이있는 경우에는 1000000까지 실행할 수 있습니다. 제공된 건물 지침. 콘솔에서 실행되며 웹 버전을 재생할 수있는 리모컨도 있습니다. ( 소스 )

결과

놀랍게도, 런 수를 늘리더라도 게임 플레이가 크게 향상되지는 않습니다. 이 전략에는 4096 개의 타일과 8192 개의 타일을 달성하는 데 매우 가까운 모든 작은 타일이있는 약 80000 포인트에서 한계가있는 것 같습니다. 런 횟수를 100에서 100000으로 늘리면 이 점수 한도 (5 %에서 40 %)에 도달 할 가능성 이 높아지지만이를 통과하지는 않습니다.

10000을 실행하면 임계 위치 근처에서 일시적으로 1000000으로 증가하여 최대 129892와 8192 타일을 달성하는 시간의 1 % 미만으로이 장벽을 무너 뜨 렸습니다.

개량

이 알고리즘을 구현 한 후 min 또는 max 점수 또는 min, max 및 avg 조합을 사용하여 많은 개선을 시도했습니다. 또한 깊이를 사용하여 시도했습니다. 이동 당 K 런을 시도하는 대신 지정된 길이 (예 : "위, 위, 왼쪽")의 이동 목록 당 K 이동을 시도 하고 최고 점수 이동 목록의 첫 번째 이동을 선택했습니다.

나중에 주어진 이동 목록 다음에 이동을 할 수있는 조건부 확률을 고려한 스코어링 트리를 구현했습니다.

그러나 이러한 아이디어 중 어느 것도 간단한 첫 번째 아이디어에 비해 실질적인 이점을 보여주지 못했습니다. 이 아이디어에 대한 코드를 C ++ 코드에 주석 처리했습니다.

런 중 하나라도 실수로 다음 타일에 도달했을 때 런 수를 일시적으로 1000000으로 늘린 "딥 검색"메커니즘을 추가했습니다. 이것은 시간 향상을 제공했습니다.

인공 지능의 도메인 독립성을 유지하는 다른 개선 아이디어가 있다면 듣고 싶습니다.

2048 변종 및 클론

재미를 위해서 AI를 북마크릿으로 구현 하여 게임 컨트롤에 연결했습니다. 이것은 AI가 오리지널 게임과 그 변형 을 사용할 수있게합니다 .

이것은 도메인의 독립적 인 AI 특성으로 인해 가능합니다. 육각형 클론과 같은 일부 변형은 매우 다릅니다.


7
+1. AI 학생으로서 나는 이것이 정말로 흥미로웠다는 것을 알았습니다. 자유 시간에 이것을 더 잘 볼 것입니다.
Isaac Isaac

4
이것은 놀랍습니다! 나는 expectimax에 대한 좋은 휴리스틱 함수를 위해 가중치를 최적화하는 데 몇 시간을 보냈고 3 분 안에 이것을 구현했으며 이것이 완전히 스매시합니다.
Brendan Annable

8
몬테카를로 시뮬레이션을 잘 활용했습니다.
nneonneo 2016 년

5
이 연주를보고 깨달음을 요구하고 있습니다. 이것은 모든 휴리스틱을 날려 버렸지 만 작동합니다. 축하합니다 !
Stéphane Gourichon

4
지금까지 가장 흥미로운 솔루션입니다.
shebaw

126

편집 : 이것은 인간의 의식적 사고 과정을 모델링하는 순진한 알고리즘이며 AI보다 AI에 비해 매우 약한 결과를 얻습니다. 응답 일정 초기에 제출되었습니다.

알고리즘을 개선하고 게임을 이겼습니다! 끝이 가까워지면 운이 나빠져 실패 할 수 있습니다. 패턴을 깨 뜨리십시오), 그러나 기본적으로 고정 부분과 모바일 부분을 가지고 놀게됩니다. 이것이 당신의 목표입니다 :

완료 준비

이것이 기본적으로 선택한 모델입니다.

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

선택한 모서리는 임의적이며 기본적으로 하나의 키 (금지 된 이동)를 누르지 마십시오. 그렇다면 반대를 다시 누르고 수정하십시오. 이후 타일의 경우 모델은 항상 다음 임의 타일이 2가 될 것으로 예상하고 현재 모델의 반대쪽에 나타납니다 (첫 번째 행은 불완전한 반면 첫 번째 행이 완료되면 첫 번째 행이 완료되면 왼쪽 하단에 나타남) 모서리).

여기 알고리즘이 있습니다. 약 80 %의 승리 (항상 더 "전문적인"AI 기술로 승리하는 것이 가능할 것 같습니다.)

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

누락 된 단계에 대한 몇 가지 지침. 여기:모델 변경

모델이 예상 모델에 가까워짐에 따라 모델이 변경되었습니다. AI가 달성하려는 모델은

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

그리고 거기에 도달하는 사슬은 다음과 같습니다.

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

O금지 구역을 나타내는 ...

따라서 오른쪽을 누른 다음 다시 오른쪽을 누른 다음 (4가 생성 된 위치에 따라 오른쪽 또는 위쪽) 다음 체인이 완성 될 때까지 완료합니다.

체인 완성

이제 모델과 체인이 다시 돌아 왔습니다.

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

두 번째 포인터, 그것은 불운을 가지고 주요 지점이되었습니다. 실패 할 가능성이 있지만 여전히 달성 할 수 있습니다.

여기에 이미지 설명을 입력하십시오

모델과 체인은 다음과 같습니다.

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

128에 도달하면 전체 행을 다시 얻습니다.

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

execute move with best score가능한 다음 주 중에서 최고 점수를 어떻게 평가할 수 있습니까?
Khaled.K

휴리스틱은 evaluateResult기본적으로 최상의 시나리오에 가장 가깝게 시도합니다.
Daren

@Daren 나는 당신의 세부 사항을 기다리고 있습니다
ashu

@ashu 나는 노력하고 있습니다. 예기치 않은 상황으로 인해 끝내지 못했습니다. 한편 나는 알고리즘을 개선했으며 이제 75 %의 시간을 해결합니다.
Daren

13
이 전략에 대해 내가 정말 좋아하는 점은 게임을 수동으로 플레이 할 때 사용할 수 있다는 점입니다. 최대 37k 포인트를 얻었습니다.
Cephalopod

94

내 블로그게시물 내용을 여기에 복사합니다.


내가 제안하는 솔루션은 매우 간단하고 구현하기 쉽습니다. 131040의 점수에 도달했습니다. 알고리즘 성능에 대한 여러 벤치 마크가 제공됩니다.

점수

연산

휴리스틱 스코어링 알고리즘

내 알고리즘의 기반이되는 가정은 다소 간단합니다. 더 높은 점수를 얻으려면 보드를 최대한 깔끔하게 유지해야합니다. 특히, 최적의 설정은 타일 값의 선형 및 단조 감소 순서로 제공됩니다. 이 직관은 타일 값의 상한도 제공합니다. 에스여기서 n은 보드의 타일 수입니다.

(필요한 경우 4 타일이 2 타일 대신 무작위로 생성되면 131072 타일에 도달 할 가능성이 있습니다)

보드를 구성하는 두 가지 방법이 다음 이미지에 나와 있습니다.

여기에 이미지 설명을 입력하십시오

단조로운 내림차순으로 타일의 순서를 강제하기 위해, 점수 si는 보드상의 선형화 된 값의 합에 공통 비율 r <1을 갖는 기하학적 시퀀스의 값을 곱한 것으로 계산된다.

에스

에스

여러 선형 경로를 한 번에 평가할 수 있으며 최종 점수는 모든 경로의 최대 점수입니다.

결정 규칙

의사 결정 규칙이 현명하지는 않지만 파이썬 코드는 다음과 같습니다.

@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);

minmax 또는 Expectiminimax를 구현하면 알고리즘이 확실히 향상됩니다. 보다 정교한 의사 결정 규칙은 알고리즘 속도를 늦추고 구현하는 데 약간의 시간이 필요할 것입니다. (계속 지켜봐주십시오)

기준

  • T1-121 테스트-8 가지 경로-r = 0.125
  • T2-122 테스트-8 개의 다른 경로-r = 0.25
  • T3-132 테스트-8 개의 다른 경로-r = 0.5
  • T4-211 테스트-2 개의 다른 경로-r = 0.125
  • T5-274 테스트-2 개의 다른 경로-r = 0.25
  • T6-211 테스트-2 개의 다른 경로-r = 0.5

여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오

T2의 경우 10 번의 4 번의 테스트로 평균 점수가 에스42000 인 4096 타일이 생성됩니다.

암호

코드는 GiHub의 다음 링크에서 찾을 수 있습니다. https://github.com/Nicola17/term2048-AI term2048을 기반으로 하며 Python으로 작성되었습니다. 가능한 빨리 C ++에서보다 효율적인 버전을 구현할 것입니다.


나쁘지 않습니다. 귀하의 일러스트레이션은 병합 벡터를 평가에 사용하는 아이디어를 알려주었습니다
Khaled.K

안녕하세요. github 페이지에 제공된 지침이 프로젝트에 적용됩니까? 시도해보고 싶지만 AI 자동 실행이 아닌 원래의 재생 가능한 게임에 대한 지침 인 것 같습니다. 그것들을 업데이트 할 수 있습니까? 감사.
JD Gamboa

41

내 시도는 위의 다른 솔루션과 마찬가지로 expectimax를 사용하지만 비트 보드는 사용하지 않습니다. Nneonneo의 솔루션은 왼쪽에 6 개의 타일이 있고 4 개의 이동이 가능하며 (2 * 6 * 4) 4 정도의 깊이 인 약 4 백만 개의 이동을 확인할 수 있습니다 . 필자의 경우이 깊이는 탐색하는 데 너무 오래 걸립니다. 남은 사용 가능한 타일 수에 따라 expectimax 검색 깊이를 조정합니다.

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

보드의 점수는 다음과 같이 사용 가능한 타일 수의 제곱과 2D 그리드의 내적의 가중 합계로 계산됩니다.

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

왼쪽 상단 타일에서 일종의 뱀으로 타일을 내림차순으로 구성합니다.

아래 또는 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
왜 더 많은 투표가 없는지 잘 모르겠습니다. 간단하기 때문에 정말 효과적입니다.
David Greydanus

감사합니다, 늦게 대답 그것은 정말 잘하지 수행 (거의 항상 [1024, 8192]), 비용 / 통계 요구에 더 많은 작업 기능
caub

빈 공간의 무게를 어떻게 측정 했습니까?
David Greydanus

1
간단 cost=1x(number of empty tiles)²+1xdotproduct(snakeWeights,grid)하고 우리는이 비용을 극대화하려고 노력합니다
caub

@Robusto에게 감사합니다. 언젠가 코드를 개선해야합니다. 단순화 할 수 있습니다
caub

38

이 스레드에서 언급 한 다른 프로그램보다 점수가 높은 2048 컨트롤러의 저자입니다. 컨트롤러의 효율적인 구현은 github에서 가능합니다 . 에서 별도의 repo 또한 컨트롤러의 상태 평가 기능을 훈련에 사용되는 코드가있다. 훈련 방법은 논문에 설명되어 있습니다.

컨트롤러는 시간적 차이 학습 (강화 학습 기술) 의 변형에 의해 처음부터 (인간 2048 전문 지식없이) 배운 상태 평가 기능과 함께 expectimax 검색을 사용합니다 . 상태 값 함수 는 기본적으로 보드에서 관찰되는 패턴의 가중 선형 함수 인 n- 튜플 네트워크를 사용합니다 . 총 10 억 개 이상의 가중치 가 포함되었습니다.

공연

1 이동 / 초 : 609104 (평균 100 게임)

10 이동 / 초 : 589355 (평균 300 게임)

3 회 (약 1500 회 이동 / 초) : 511759 (평균 1000 게임)

10 이동 / 초의 타일 통계는 다음과 같습니다.

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

(마지막 줄은 보드에 주어진 타일을 동시에 가지고 있음을 의미합니다).

3 겹의 경우 :

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

그러나 65536 타일을 얻는 것을 본 적이 없습니다.


4
꽤 인상적인 결과. 그러나 프로그램이 어떻게 이것을 달성하는지 설명하기 위해 대답을 업데이트 할 수 있습니까 (대략 간단하게 말하면 ... 자세한 내용을 게시하기에는 너무 길 것입니다)? 학습 알고리즘의 작동 방식에 대한 대략적인 설명과 같이?
Cedric Mamo

27

필자는 10000 점 이상의 점수에 도달하는 개인 알고리즘이 16000 정도가되는 알고리즘을 발견했다고 생각합니다. 내 솔루션은 가장 큰 숫자를 모퉁이에 두지 않고 최상위 행에 두는 것을 목표로합니다.

아래 코드를 참조하십시오 :

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
나는 사소한 주기적 전략 "위, 오른쪽, 위, 왼쪽, ..."(필요한 경우 아래로)에 대해 이것을 테스트하는 100,000 게임을 실행했습니다. 순환 전략은의 "평균 타일 점수"를 마치는 770.6동안이 전략은 단지을 얻었습니다 396.7. 왜 그런지 추측 할 수 있습니까? 왼쪽이나 오른쪽이 더 많이 병합 될 때조차도 너무 많이 일어났다 고 생각합니다.
Thomas Ahle

1
타일이 여러 방향으로 움직이지 않으면 호환되지 않는 방식으로 쌓이는 경향이 있습니다. 일반적으로 주기적 전략을 사용하면 중앙에 타일이 더 커져 조작이 훨씬 비좁아집니다.
bcdan

25

이 게임에 대한 AI 구현이 이미 있습니다 . README에서 발췌 :

이 알고리즘은 반복 심도 우선 알파-베타 검색입니다. 평가 기능은 그리드의 타일 수를 최소화하면서 행과 열을 단조롭게 (모두 감소 또는 증가) 유지하려고합니다.

또한 유용한 알고리즘에 대한 해커 뉴스 에 대한 토론도 있습니다.


4
이것은 최고의 답변이되어야하지만 구현에 대한 자세한 내용을 추가하는 것이 좋을 것입니다. 예를 들어 게임 보드를 모델링하는 방법 (그래프), 최적화 (사용 된 타일의 최대 차) 등
Alceu Costa

1
미래의 독자들을 위해 : 이것은 두 번째 최상위 답변 에서 저자 (진화)가 설명한 동일한 프로그램 입니다. 이 토론과이 토론에서 ovolve의 프로그램에 대한 다른 언급은 ovolve가 자신의 알고리즘이 어떻게 작동했는지를 나타나고 기록하도록 자극했습니다. 대답은 지금 (1200)의 점수를 가지고
MultiplyByZer0

23

연산

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

    choose the maximum evaluation
}

평가

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)

평가 내용

128 (Constant)

이것은 기준선으로 사용되며 테스트와 같은 다른 용도로 사용되는 상수입니다.

+ (Number of Spaces x 128)

더 많은 공간은 상태를 더 유연하게 만듭니다. 128 개의면으로 채워진 그리드는 최적의 불가능한 상태이므로 128 (중앙값)을 곱합니다.

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

여기서는 타일 2를 평가하면 타일 2는 값 2048이되고 타일 2048은 2를 평가하여 병합 할 수있는면을 평가합니다.

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

여기서도 여전히 누적 값을 확인해야하지만 유연성 매개 변수를 방해하지 않는 더 적은 방법으로 {x in [4,44]}의 합을 갖습니다.

+ (Number of possible next moves x 256)

가능한 전환의 자유가 더 많으면 상태가 더 유연합니다.

+ (Number of aligned values x 2)

이것은 미리 보지 않고 해당 상태 내에서 병합 될 가능성에 대한 간단한 점검입니다.

참고 : 상수를 조정할 수 있습니다.


2
라이브 코드 @ nitish712
Khaled.K

9
이 알고리즘의 win %는 얼마입니까?
cegprakash

왜 필요한 constant가요? 당신이하고있는 모든 것이 점수를 비교하는 것이라면, 그것은 그 비교의 결과에 어떤 영향을 미칩니 까?
bcdan 2016 년

(비교 점수 일명) 휴리스틱을 @bcdan 미래 상태의 기대 값을 비교에 비슷한에 따라 달라 체스 추론 작업, 우리는 최고의 다음 N의 움직임을 알고 나무를 구축하지 않기 때문에이 선형 추론이다 제외
Kled. K

12

이것은 OP의 질문에 대한 직접적인 대답이 아닙니다. 이것은 지금까지 동일한 문제를 해결하기 위해 시도한 몇 가지 결과 (결과)를 얻었으며 공유하고 싶은 관찰 결과를 얻었습니다. 이것으로부터 추가 통찰력.

방금 3과 5에서 검색 트리 깊이 컷오프를 사용하여 알파-베타 가지 치기를 사용하여 미니 맥스 구현을 시도했습니다 . AI) .

나는 주로 직관과 위에서 논의 된 것들로부터 몇 가지 휴리스틱 평가 기능의 볼록한 조합 (다른 휴리스틱 가중치를 시도)을 적용했습니다.

  1. 단조
  2. 사용 가능한 여유 공간

필자의 경우 컴퓨터 플레이어는 완전히 임의적이지만 여전히 적대적인 설정을 가정하고 AI 플레이어 에이전트를 최대 플레이어로 구현했습니다.

게임을하기위한 4x4 그리드가 있습니다.

관측:

첫 번째 휴리스틱 함수 또는 두 번째 휴리스틱 함수에 너무 많은 가중치를 할당하면 AI 플레이어가 얻는 점수가 모두 낮습니다. 휴리스틱 함수에 가능한 많은 가중치를 부여하고 볼록한 조합을 취했지만 AI 플레이어가 2048을 기록 할 수있는 경우는 거의 없습니다. 대부분 1024 또는 512에서 멈 춥니 다.

또한 코너 휴리스틱을 시도했지만 어떤 이유로 인해 결과가 더 나빠집니다. 직관적 인 이유는 무엇입니까?

또한 검색 깊이 컷오프를 3에서 5로 늘리려 고했지만 (정리로도 공간이 허용 시간을 초과하므로 검색을 더 늘릴 수 없습니다) 인접한 타일의 값을보고 제공하는 휴리스틱을 추가했습니다. 병합 가능하면 더 많은 포인트를 얻지 만 여전히 2048을 얻을 수 없습니다.

minimax 대신 Expectimax를 사용하는 것이 더 낫다고 생각하지만, minimax로만이 문제를 해결하고 2048 또는 4096과 같은 높은 점수를 얻고 싶습니다. 내가 빠진 것이 있는지 확실하지 않습니다.

아래 애니메이션은 AI 에이전트가 컴퓨터 플레이어와 함께 한 게임의 마지막 몇 단계를 보여줍니다.

여기에 이미지 설명을 입력하십시오

모든 통찰력은 사전에 정말 도움이 될 것입니다. (이 기사에 대한 내 블로그 게시물 링크 : https://sandipanweb.wordpress.com/2017/03/06/using-minimax-with-alpha-beta-pruning-and-heuristic-evaluation-to-solve -2048-game-with-computer / 및 YouTube 동영상 : https://www.youtube.com/watch?v=VnVFilfZ0r4 )

다음 애니메이션은 AI 플레이어 에이전트가 2048 점을 얻을 수있는 게임의 마지막 몇 단계를 보여줍니다. 이번에는 절대 값 휴리스틱도 추가합니다.

여기에 이미지 설명을 입력하십시오

다음 그림 은 플레이어 AI 에이전트가 컴퓨터를 단 한 단계의 적이라고 가정 한 게임 트리를 보여줍니다 .

여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오


9

나는 주로이 언어를 배우기 때문에 Haskell에서 2048 솔버를 썼습니다.

새 타일은 항상 '2'(90 % 2 및 10 % 4가 아니라)라는 점에서 게임 구현이 실제 게임과 약간 다릅니다. 그리고 새로운 타일은 무작위가 아니지만 항상 왼쪽 상단에서 사용 가능한 첫 번째 타일입니다. 이 변형은 Det 2048 로도 알려져 있습니다 .

결과적으로이 솔버는 결정적입니다.

나는 빈 타일을 선호하는 철저한 알고리즘을 사용했습니다. 깊이 1-4에서는 꽤 빨리 수행되지만 깊이 5에서는 이동 당 약 1 초에 다소 느려집니다.

다음은 해결 알고리즘을 구현하는 코드입니다. 격자는 16 길이의 정수 배열로 표시됩니다. 그리고 빈 사각형의 수를 세어 간단히 점수를 매 깁니다.

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] ]

나는 그것이 간단하기 때문에 꽤 성공적이라고 생각합니다. 빈 그리드로 시작하여 깊이 5에서 풀 때 도달하는 결과는 다음과 같습니다.

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

Game Over

소스 코드는 여기에서 찾을 수 있습니다 : https://github.com/popovitsj/2048-haskell


실제 규칙으로 확장하십시오. 하스켈의 랜덤 제너레이터에 대해 배우는 것은 좋은 도전입니다!
Thomas Ahle

나는 Haskell이 그렇게하려고 매우 좌절했지만, 아마 두 번째 시도를 할 것입니다! 나는 무작위 화없이 게임이 훨씬 쉬워진다는 것을 알았습니다.
wvdz

무작위 화가 없으면 항상 16k 또는 32k를 얻는 방법을 찾을 수 있다고 확신합니다. 그러나 Haskell의 무작위 배정은 그렇게 나쁘지 않습니다.`시드 '를 통과하는 방법이 필요합니다. 명시 적으로 또는 임의 모나드로 수행하십시오.
Thomas Ahle

비 랜덤 게임에서 항상 16k / 32k에 도달하도록 알고리즘을 수정하는 것은 또 다른 흥미로운 도전이 될 수 있습니다.
wvdz

네 말이 맞아 생각보다 힘들어 나는 항상이 게임에서이기는 [UP, LEFT, LEFT, UP, LEFT, DOWN, LEFT] 시퀀스를 찾았지만 2048을 넘지 않습니다. (법적 이동이 없으면 사이클 알고리즘은 단지 시계 방향으로 다음 것)
Thomas Ahle

6

이 알고리즘은 게임에서 승리하기에 최적은 아니지만 성능 및 필요한 코드 양 측면에서 상당히 최적입니다.

  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
random from (right, right, right, down, down, up) 모든 움직임이 동일한 확률을 갖는 것은 아니라는 것이 더 효과적 입니다. :)
Daren

3
사실, 게임에 완전히 익숙하지 않다면 기본적으로이 알고리즘의 기능에 따라 3 개의 키만 사용하면 도움이됩니다. 첫눈에 보이는 것만 큼 나쁘지 않습니다.
Digits

5
예, 그것은 게임에 대한 나의 관찰에 근거합니다. 당신이 4 방향을 사용해야 할 때까지 게임은 어떤 종류의 관찰없이 실제로 스스로를 해결할 것입니다. 이 "AI"는 블록의 정확한 값을 확인하지 않고 512/1024에 도달 할 수 있어야합니다.
API-Beast

3
적절한 AI는 모든 비용으로 한 방향으로 만 이동할 수있는 상태가되지 않도록 노력합니다.
API-Beast

3
실제로 3 방향 만 사용하는 것은 매우 적절한 전략입니다! 2048 년에 거의 수동으로 게임을하게되었습니다. 이것을 3 가지 남은 동작 중에서 결정하기위한 다른 전략과 결합하면 매우 강력 할 수 있습니다. 선택을 3으로 줄이면 성능에 막대한 영향을 미칩니다.
wvdz

4

다른 많은 답변들은 AI를 사용하여 미래, 휴리스틱, 학습 등의 계산적으로 비싼 검색을 수행합니다. 이것들은 인상적이고 아마도 올바른 길이지만, 나는 또 다른 아이디어를 제공하고자합니다.

게임의 훌륭한 플레이어가 사용하는 전략을 모델링하십시오.

예를 들면 다음과 같습니다.

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

다음 제곱 값이 현재 제곱보다 클 때까지 위에 표시된 순서대로 제곱을 읽습니다. 이것은 같은 값의 다른 타일을이 사각형에 병합하려고 할 때 발생하는 문제를 나타냅니다.

이 문제를 해결하기 위해 두 가지 방법으로 남거나 나 빠지지 않고 두 가지 가능성을 모두 검사하면 더 많은 문제가 즉시 드러날 수 있습니다. 이는 종속성 목록을 형성하며, 각 문제는 먼저 다른 문제를 해결해야합니다. 나는 다음 체인을 결정할 때, 특히 갇혀있을 때이 체인을 가지고 있거나 어떤 경우에는 내부적으로 의존성 트리를 가지고 있다고 생각합니다.


타일을 이웃과 병합해야하지만 너무 작습니다. 다른 이웃을이 타일과 병합하십시오.

방해가 큰 타일 : 주변 타일이 작을수록 가치가 높아집니다.

기타...


전체 접근 방식은 이보다 더 복잡 할 수 있지만 훨씬 더 복잡하지는 않습니다. 점수, 무게, 뉴런 및 가능성에 대한 깊은 검색이 부족하다고 느끼는 것이이 기계 일 수 있습니다. 가능성의 나무는 아무래도 분기가 전혀 필요하지 않을 정도로 커야합니다.


5
휴리스틱으로 지역 검색을 설명하고 있습니다. 그것은 당신을 붙잡을 것이다, 그래서 당신은 다음 움직임을 위해 미리 계획해야한다. 결과적으로 솔루션을 검색하고 점수를 매 깁니다 (결정하기 위해). 따라서 이것은 실제로 제시된 다른 솔루션과 다르지 않습니다.
runDOSrun
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.