Qual è l'algoritmo ottimale per il gioco 2048?


1920

Di recente mi sono imbattuto nel gioco del 2048 . Unisci tessere simili spostandole in una delle quattro direzioni per creare tessere "più grandi". Dopo ogni mossa, una nuova tessera appare in una posizione vuota casuale con un valore di 2o 4. Il gioco termina quando tutte le caselle sono riempite e non ci sono mosse in grado di unire le tessere o si crea una tessera con un valore di 2048.

Uno, devo seguire una strategia ben definita per raggiungere l'obiettivo. Quindi, ho pensato di scrivere un programma per questo.

Il mio attuale algoritmo:

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
}

Quello che sto facendo è in ogni momento, cercherò di unire le tessere con i valori 2e 4, cioè, cercherò di avere 2e 4tessere, il minimo possibile. Se lo provo in questo modo, tutte le altre tessere si uniscono automaticamente e la strategia sembra buona.

Ma quando uso effettivamente questo algoritmo, ottengo solo circa 4000 punti prima che il gioco termini. Punti massimi AFAIK è leggermente più di 20.000 punti che è molto più grande del mio punteggio attuale. Esiste un algoritmo migliore di quanto sopra?


84
Questo potrebbe aiutare! ov3y.github.io/2048-AI
cegprakash

5
@ nitish712 a proposito, il tuo algoritmo è avido poiché hai choose the move with large number of mergesrapidamente portato a optima locale
Khaled.K

21
@ 500-InternalServerError: se dovessi implementare un'intelligenza artificiale con potatura dell'albero dei giochi alfa-beta, si presumerebbe che i nuovi blocchi siano posizionati in modo contraddittorio. È un'ipotesi nel caso peggiore, ma potrebbe essere utile.
Charles

6
Una distrazione divertente quando non hai il tempo di puntare a un punteggio alto: cerca di ottenere il punteggio più basso possibile. In teoria si alternano 2 e 4 secondi.
Mark Hurd,

7
La discussione sulla legittimità di questa domanda è disponibile su meta: meta.stackexchange.com/questions/227266/…
Jeroen Vannevel

Risposte:


1266

Ho sviluppato un'intelligenza artificiale 2048 utilizzando l' ottimizzazione di aspettimax , invece della ricerca minimax utilizzata dall'algoritmo di @ ovolve. L'intelligenza artificiale esegue semplicemente la massimizzazione su tutte le mosse possibili, seguita dall'aspettativa su tutte le possibili spawn delle tessere (ponderata dalla probabilità delle tessere, ovvero il 10% per un 4 e il 90% per un 2). Per quanto ne so, non è possibile eliminare l'ottimizzazione di aspettativa (tranne che per rimuovere rami che sono estremamente improbabili), quindi l'algoritmo utilizzato è una ricerca della forza bruta accuratamente ottimizzata.

Prestazione

L'intelligenza artificiale nella sua configurazione predefinita (profondità di ricerca massima di 8) impiega da 10ms a 200ms per eseguire una mossa, a seconda della complessità della posizione della scheda. Durante i test, l'IA raggiunge una velocità di movimento media di 5-10 mosse al secondo nel corso di un'intera partita. Se la profondità di ricerca è limitata a 6 mosse, l'intelligenza artificiale può facilmente eseguire oltre 20 mosse al secondo, il che rende interessante la visione .

Per valutare le prestazioni del punteggio dell'IA, ho eseguito l'IA 100 volte (collegato al browser tramite telecomando). Per ogni tessera, ecco le proporzioni dei giochi in cui quella tessera è stata raggiunta almeno una volta:

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

Il punteggio minimo su tutte le corse è stato 124024; il punteggio massimo raggiunto era 794076. Il punteggio mediano è 387222. L'intelligenza artificiale non è mai riuscita a ottenere la tessera 2048 (quindi non ha mai perso la partita nemmeno una volta su 100 partite); infatti, ha raggiunto la tessera 8192 almeno una volta in ogni corsa!

Ecco lo screenshot della migliore corsa:

Piastrella 32768, punteggio 794076

Questo gioco ha richiesto 27830 mosse in 96 minuti o una media di 4,8 mosse al secondo.

Implementazione

Il mio approccio codifica l'intera scheda (16 voci) come un singolo numero intero a 64 bit (in cui i riquadri sono i nybbles, ovvero i blocchi a 4 bit). Su una macchina a 64 bit, ciò consente al pannello intero di essere passato in un unico registro macchina.

Le operazioni di spostamento dei bit vengono utilizzate per estrarre singole righe e colonne. Una singola riga o colonna è una quantità di 16 bit, quindi una tabella delle dimensioni 65536 può codificare trasformazioni che operano su una singola riga o colonna. Ad esempio, le mosse vengono implementate come 4 ricerche in una "tabella degli effetti di movimento" precompilata che descrive come ogni spostamento influisce su una singola riga o colonna (ad esempio, la tabella "sposta a destra" contiene la voce "1122 -> 0023" che descrive come la riga [2,2,4,4] diventa la riga [0,0,4,8] quando viene spostata a destra).

Il punteggio viene eseguito anche tramite la ricerca della tabella. Le tabelle contengono punteggi euristici calcolati su tutte le possibili righe / colonne e il punteggio risultante per una scheda è semplicemente la somma dei valori della tabella in ogni riga e colonna.

Questa rappresentazione del tabellone, insieme all'approccio di ricerca del tavolo per il movimento e il punteggio, consente all'AI di cercare un numero enorme di stati di gioco in un breve periodo di tempo (oltre 10.000.000 di stati di gioco al secondo su un core del mio laptop a metà 2011).

La stessa ricerca aspettativa è codificata come una ricerca ricorsiva che si alterna tra i passaggi di "aspettativa" (test di tutte le possibili posizioni e valori di spawn delle piastrelle e ponderazione dei loro punteggi ottimizzati in base alla probabilità di ciascuna possibilità) e passaggi di "massimizzazione" (test di tutte le possibili mosse e selezionando quello con il punteggio migliore). La ricerca dell'albero termina quando vede una posizione precedentemente vista (usando una tabella di trasposizione ), quando raggiunge un limite di profondità predefinito, o quando raggiunge uno stato della scheda che è altamente improbabile (ad esempio è stato raggiunto ottenendo 6 "4" tessere di fila dalla posizione iniziale). La profondità di ricerca tipica è di 4-8 mosse.

Euristico

Diverse euristiche vengono utilizzate per indirizzare l'algoritmo di ottimizzazione verso posizioni favorevoli. La scelta precisa dell'euristica ha un enorme effetto sulle prestazioni dell'algoritmo. Le varie euristiche vengono ponderate e combinate in un punteggio posizionale, che determina quanto sia "buona" una determinata posizione della scacchiera. La ricerca di ottimizzazione mirerà quindi a massimizzare il punteggio medio di tutte le possibili posizioni del board. Il punteggio effettivo, come mostrato dal gioco, non viene utilizzato per calcolare il punteggio del tabellone, poiché è troppo pesantemente ponderato a favore della fusione delle tessere (quando la fusione ritardata potrebbe produrre un grande vantaggio).

Inizialmente, ho usato due euristiche molto semplici, garantendo "bonus" per i quadrati aperti e per avere grandi valori al limite. Queste euristiche hanno funzionato abbastanza bene, raggiungendo frequentemente il 16384 ma non arrivando mai a 32768.

Petr Morávek (@xificurk) ha preso la mia IA e ha aggiunto due nuove euristiche. La prima euristica fu una penalità per avere file e colonne non monotoniche che aumentavano con l'aumentare dei ranghi, assicurando che le file non monotoniche di piccoli numeri non influenzassero fortemente il punteggio, ma le file non monotoniche di grandi numeri danneggiarono sostanzialmente il punteggio. La seconda euristica ha contato il numero di potenziali fusioni (valori uguali adiacenti) oltre agli spazi aperti. Queste due euristiche servivano a spingere l'algoritmo verso le schede monotoniche (che sono più facili da unire) e verso le posizioni delle schede con molte fusioni (incoraggiandolo ad allineare le fusioni ove possibile per un maggiore effetto).

Inoltre, Petr ha anche ottimizzato i pesi euristici utilizzando una strategia di "meta-ottimizzazione" (utilizzando un algoritmo chiamato CMA-ES ), in cui i pesi stessi sono stati regolati per ottenere il punteggio medio più alto possibile.

L'effetto di questi cambiamenti è estremamente significativo. L'algoritmo è passato dal raggiungimento della tessera 16384 circa il 13% delle volte a raggiungerla nel 90% delle volte, e l'algoritmo ha iniziato a raggiungere 32768 in 1/3 del tempo (mentre la vecchia euristica non ha mai prodotto una tessera 32768) .

Credo che ci sia ancora spazio per migliorare l'euristica. Questo algoritmo sicuramente non è ancora "ottimale", ma sento che si sta avvicinando molto.


Che l'IA raggiunga la tessera 32768 in oltre un terzo dei suoi giochi è una pietra miliare enorme; Sarò sorpreso di sapere se qualche giocatore umano ha raggiunto 32768 nel gioco ufficiale (cioè senza usare strumenti come savestate o annulla). Penso che la tessera 65536 sia a portata di mano!

Puoi provare l'IA per te stesso. Il codice è disponibile su https://github.com/nneonneo/2048-ai .


12
@RobL: i 2 appaiono il 90% delle volte; I 4 appaiono il 10% delle volte. E 'nel codice sorgente : var value = Math.random() < 0.9 ? 2 : 4;.
nneonneo,

35
Attualmente sta eseguendo il porting su Cuda, quindi la GPU lavora per velocità ancora migliori!
Nimsson,

25
@nneonneo Ho portato il tuo codice con emscripten su javascript e ora funziona abbastanza bene nel browser ! Bello da guardare, senza la necessità di compilare e tutto il resto ... In Firefox, le prestazioni sono abbastanza buone ...
reverse_engineer

7
Il limite teorico in una griglia 4x4 è in realtà 131072 non 65536. Tuttavia ciò richiede di ottenere un 4 nel momento giusto (cioè l'intera scheda riempita con 4 .. 65536 una volta ogni volta - 15 campi occupati) e la scheda deve essere impostata in quel momento in modo che tu possa davvero combinare.
Bodo Thiesen

5
@nneonneo Potresti voler controllare la nostra IA, che sembra ancora migliore, arrivando a 32k nel 60% dei giochi: github.com/aszczepanski/2048
cauchy

1253

Sono l'autore del programma AI che altri hanno citato in questa discussione. Puoi visualizzare l'IA in azione o leggere la fonte .

Attualmente, il programma raggiunge una percentuale di vincita del 90% in esecuzione in javascript nel browser del mio laptop, dato circa 100 millisecondi di tempo di riflessione per mossa, quindi sebbene non perfetto (ancora!) Si comporta abbastanza bene.

Poiché il gioco è uno spazio di stato discreto, informazioni perfette, gioco a turni come scacchi e pedine, ho usato gli stessi metodi che hanno dimostrato di funzionare su quei giochi, vale a dire la ricerca minimax con potatura alfa-beta . Dato che ci sono già molte informazioni su quell'algoritmo là fuori, parlerò solo delle due principali euristiche che utilizzo nella funzione di valutazione statica e che formalizzano molte delle intuizioni che altre persone hanno espresso qui.

Monotonicità

Questo euristico cerca di garantire che i valori delle tessere siano tutti in aumento o in diminuzione lungo entrambe le direzioni sinistra / destra e su / giù. Questo euristico da solo cattura l'intuizione che molti altri hanno menzionato, che le piastrelle di valore più alto dovrebbero essere raggruppate in un angolo. In genere eviterà di rimanere orfane tessere di valore inferiore e manterrà la scacchiera molto organizzata, con tessere più piccole che scorrono a cascata e si riempiono in quelle più grandi.

Ecco uno screenshot di una griglia perfettamente monotona. L'ho ottenuto eseguendo l'algoritmo con la funzione eval impostata per ignorare le altre euristiche e considerare solo la monotonicità.

Una scheda 2048 perfettamente monotona

levigatezza

Il solo euristico di cui sopra tende a creare strutture in cui le tessere adiacenti stanno diminuendo di valore, ma ovviamente per unire le tessere adiacenti devono avere lo stesso valore. Pertanto, l'euristica della levigatezza misura solo la differenza di valore tra le tessere vicine, cercando di minimizzare questo conteggio.

Un commentatore di Hacker News ha fornito un'interessante formalizzazione di questa idea in termini di teoria dei grafi.

Ecco uno screenshot di una griglia perfettamente liscia, per gentile concessione di questa eccellente forcella per parodia .

Una tavola 2048 perfettamente liscia

Piastrelle gratuite

E infine, c'è una penalità per avere troppe tessere gratuite, poiché le opzioni possono esaurirsi rapidamente quando il tabellone di gioco è troppo stretto.

E questo è tutto! La ricerca nello spazio di gioco e l'ottimizzazione di questi criteri producono prestazioni straordinariamente buone. Un vantaggio nell'utilizzare un approccio generalizzato come questo piuttosto che una strategia di spostamento esplicitamente codificata è che l'algoritmo può spesso trovare soluzioni interessanti e inaspettate. Se lo guardi correre, spesso farà mosse sorprendenti ma efficaci, come cambiare improvvisamente il muro o l'angolo contro cui si sta costruendo.

Modificare:

Ecco una dimostrazione del potere di questo approccio. Ho sbloccato i valori delle tessere (quindi è andato avanti dopo aver raggiunto il 2048) ed ecco il miglior risultato dopo otto prove.

4096

Sì, è un 4096 insieme a un 2048. =) Ciò significa che ha raggiunto la sfuggente tessera 2048 tre volte sulla stessa tavola.


89
Puoi considerare il computer che posiziona le tessere "2" e "4" come "avversario".
Wei Yen,

29
@WeiYen Certo, ma considerarlo come un problema di minmax non è fedele alla logica del gioco, perché il computer posiziona casualmente le tessere con determinate probabilità, piuttosto che minimizzare intenzionalmente il punteggio.
koo,

57
Anche se l'IA posiziona casualmente le tessere, l'obiettivo non è perdere. Essere sfortunati è la stessa cosa dell'avversario che sceglie la mossa peggiore per te. La parte "min" significa che cerchi di giocare in modo conservativo in modo che non ci siano mosse terribili che potresti essere sfortunato.
FryGuy

196
Ho avuto l'idea di creare un fork di 2048, in cui il computer invece di posizionare i 2 e i 4 utilizza casualmente la tua IA per determinare dove inserire i valori. Il risultato: pura impossibilità. Può essere provato qui: sztupy.github.io/2048-Hard
SztupY

30
@SztupY Wow, questo è male. Mi ricorda qntm.org/hatetris Hatetris, che cerca anche di posizionare il pezzo che migliorerà la tua situazione.
Patashu,

145

Mi sono interessato all'idea di un'intelligenza artificiale per questo gioco che non contiene intelligenza codificata (ad es. Euristica, funzioni di punteggio ecc.). L'intelligenza artificiale dovrebbe "conoscere" solo le regole del gioco e "capire" il gioco. Ciò è in contrasto con la maggior parte degli AI (come quelli in questo thread) in cui il gioco è essenzialmente una forza bruta guidata da una funzione di punteggio che rappresenta la comprensione umana del gioco.

Algoritmo AI

Ho trovato un algoritmo di gioco semplice ma sorprendentemente buono: per determinare la mossa successiva per una determinata tavola, l'IA gioca il gioco in memoria usando mosse casuali fino alla fine del gioco. Questo viene fatto più volte tenendo traccia del punteggio finale del gioco. Quindi il punteggio medio finale per mossa iniziale viene calcolato il . La mossa iniziale con il punteggio finale medio più alto viene scelta come mossa successiva.

Con solo 100 corse (cioè nei giochi di memoria) per mossa, l'IA raggiunge la tessera 2048 l'80% delle volte e la tessera 4096 il 50% delle volte. Utilizzando 10000 esecuzioni si ottiene la tessera 2048 al 100%, 70% per la tessera 4096 e circa l'1% per la tessera 8192.

Guardalo in azione

Il punteggio migliore ottenuto è mostrato qui:

miglior punteggio

Un fatto interessante di questo algoritmo è che mentre i giochi casuali sono sorprendentemente abbastanza cattivi, la scelta della mossa migliore (o meno cattiva) porta a un ottimo gioco: un tipico gioco di intelligenza artificiale può raggiungere 70000 punti e le ultime 3000 mosse, ma il i giochi casuali in memoria da una determinata posizione generano in media 340 punti aggiuntivi in ​​circa 40 mosse extra prima di morire. (Puoi vederlo da solo eseguendo l'IA e aprendo la console di debug.)

Questo grafico illustra questo punto: la linea blu mostra il punteggio del tabellone dopo ogni mossa. La linea rossa mostra il miglior punteggio di fine corsa dell'algoritmo da quella posizione. In sostanza, i valori rossi "tirano" verso l'alto i valori blu verso di loro, in quanto sono la migliore ipotesi dell'algoritmo. È interessante vedere che la linea rossa è solo un po 'sopra la linea blu in ogni punto, ma la linea blu continua ad aumentare sempre di più.

grafico del punteggio

Trovo abbastanza sorprendente che l'algoritmo non debba effettivamente prevedere un buon gioco per scegliere le mosse che lo producono.

Effettuando la ricerca più tardi ho scoperto che questo algoritmo potrebbe essere classificato come un algoritmo di ricerca dell'albero di Monte Carlo puro .

Implementazione e collegamenti

Per prima cosa ho creato una versione JavaScript che può essere vista in azione qui . Questa versione può eseguire centinaia di corse in tempo decente. Apri la console per ulteriori informazioni. ( fonte )

Successivamente, per giocare ancora un po ', ho usato l'infrastruttura altamente ottimizzata di @nneonneo e ho implementato la mia versione in C ++. Questa versione consente fino a 100000 corse per mossa e persino 1000000 se hai la pazienza. Istruzioni per la costruzione fornite. Funziona nella console e ha anche un telecomando per riprodurre la versione web. ( fonte )

risultati

Sorprendentemente, aumentare il numero di run non migliora drasticamente il gioco. Sembra esserci un limite a questa strategia a circa 80000 punti con la tessera 4096 e tutte quelle più piccole, molto vicine al raggiungimento della tessera 8192. Aumentare il numero di tiri da 100 a 100000 aumenta le probabilità di arrivare a questo limite di punteggio (dal 5% al ​​40%) ma non superarlo.

L'esecuzione di 10000 corse con un aumento temporaneo a 1000000 vicino a posizioni critiche è riuscita a superare questa barriera meno dell'1% delle volte raggiungendo un punteggio massimo di 129892 e la tessera 8192.

miglioramenti

Dopo aver implementato questo algoritmo ho provato molti miglioramenti tra cui l'utilizzo dei punteggi minimo o massimo o una combinazione di minimo, massimo e medio. Ho anche provato a usare la profondità: invece di provare K run per mossa, ho provato K mosse per lista mosse di una data lunghezza ("su, su, sinistra" per esempio) e selezionando la prima mossa della lista mosse con il miglior punteggio.

Successivamente ho implementato un albero di punteggio che ha tenuto conto della probabilità condizionata di poter giocare una mossa dopo una determinata lista di mosse.

Tuttavia, nessuna di queste idee ha mostrato alcun vantaggio reale rispetto alla prima idea semplice. Ho lasciato il codice per queste idee commentate nel codice C ++.

Ho aggiunto un meccanismo di "Ricerca approfondita" che ha aumentato temporaneamente il numero di corse a 1000000 quando una delle piste è riuscita a raggiungere accidentalmente la tessera più alta successiva. Ciò ha offerto un miglioramento del tempo.

Sarei interessato a sapere se qualcuno ha altre idee di miglioramento che mantengono l'indipendenza del dominio dell'IA.

2048 Varianti e cloni

Solo per divertimento, ho anche implementato l'IA come bookmarklet , collegandomi ai controlli del gioco. Ciò consente all'intelligenza artificiale di funzionare con il gioco originale e molte delle sue varianti .

Ciò è possibile a causa della natura indipendente dal dominio dell'IA. Alcune varianti sono abbastanza distinte, come il clone esagonale.


7
+1. Come studente di AI l'ho trovato davvero interessante. Vedremo meglio questo nel tempo libero.
Isacco,

4
Questo è fantastico! Ho appena trascorso ore a ottimizzare i pesi per una buona funzione euristica per aspettasix e lo implemento in 3 minuti e questo lo distrugge completamente.
Brendan Annable,

8
Bel uso della simulazione Monte Carlo.
nneonneo,

5
Guardare questo suonare richiede un'illuminazione. Questo soffia tutta l'euristica e tuttavia funziona. Complimenti!
Stéphane Gourichon,

4
Di gran lunga la soluzione più interessante qui.
Shebaw,

126

EDIT: questo è un algoritmo ingenuo, che modella il processo di pensiero consapevole umano e ottiene risultati molto deboli rispetto all'intelligenza artificiale che cerca tutte le possibilità poiché guarda solo una tessera davanti. È stato presentato all'inizio della sequenza temporale di risposta.

Ho perfezionato l'algoritmo e battuto il gioco! Potrebbe fallire a causa della semplice sfortuna vicino alla fine (sei costretto a spostarti verso il basso, cosa che non dovresti mai fare, e una tessera appare dove dovrebbe essere la tua più alta. Cerca solo di riempire la fila superiore, quindi spostarti a sinistra non rompere il modello), ma fondamentalmente si finisce per avere una parte fissa e una parte mobile con cui giocare. Questo è il tuo obiettivo:

Pronto a finire

Questo è il modello che ho scelto di default.

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

L'angolo scelto è arbitrario, praticamente non si preme mai un tasto (la mossa proibita), e se lo si fa, si preme di nuovo il contrario e si tenta di risolverlo. Per le tessere future il modello si aspetta sempre che la tessera casuale successiva sia un 2 e appaia sul lato opposto al modello corrente (mentre la prima riga è incompleta, nell'angolo in basso a destra, una volta completata la prima riga, in basso a sinistra angolo).

Ecco qui l'algoritmo. Circa l'80% delle vittorie (sembra che sia sempre possibile vincere con tecniche di intelligenza artificiale più "professionali", non ne sono sicuro, però).

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

Alcuni suggerimenti sui passaggi mancanti. Qui:cambio di modello

Il modello è cambiato a causa della fortuna di essere più vicino al modello previsto. Il modello che l'IA sta cercando di raggiungere è

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

E la catena per arrivarci è diventata:

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

Gli Ospazi proibiti rappresentano ...

Quindi premi a destra, poi di nuovo a destra, quindi (a destra o in alto a seconda di dove ha creato il 4) quindi procederà a completare la catena fino a quando non ottiene:

Catena completata

Quindi ora il modello e la catena sono tornati a:

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

Secondo puntatore, ha avuto sfortuna e il suo punto principale è stato preso. È probabile che fallirà, ma può comunque raggiungerlo:

Inserisci qui la descrizione dell'immagine

Qui il modello e la catena sono:

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

Quando riesce a raggiungere il 128 guadagna un'intera riga di nuovo:

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

execute move with best scorecome puoi valutare il miglior punteggio tra i possibili stati successivi?
Khaled.K,

l'euristica è definita in evaluateResultte sostanzialmente cerca di avvicinarti al miglior scenario possibile.
Daren,

@Daren, aspetto i tuoi dettagli dettagliati
ashu,

@ashu Ci sto lavorando, circostanze inaspettate mi hanno lasciato senza tempo per finirlo. Nel frattempo ho migliorato l'algoritmo e ora lo risolve il 75% delle volte.
Daren,

13
Quello che mi piace davvero di questa strategia è che sono in grado di usarlo quando gioco manualmente, mi ha portato a 37k punti.
Cefalopode

94

Copio qui il contenuto di un post sul mio blog


La soluzione che propongo è molto semplice e facile da implementare. Tuttavia, ha raggiunto il punteggio di 131040. Vengono presentati diversi parametri di riferimento delle prestazioni dell'algoritmo.

Punto

Algoritmo

Algoritmo di punteggio euristico

L'ipotesi su cui si basa il mio algoritmo è piuttosto semplice: se si desidera ottenere un punteggio più alto, la scheda deve essere mantenuta il più ordinata possibile. In particolare, l'impostazione ottimale è data da un ordine decrescente lineare e monotonico dei valori delle piastrelle. Questa intuizione ti darà anche il limite superiore per un valore di tessera: Sdove n è il numero di tessere sul tabellone.

(C'è la possibilità di raggiungere la tessera 131072 se la tessera 4 viene generata casualmente invece della tessera 2 quando necessario)

Nelle seguenti immagini sono mostrati due modi possibili di organizzare la scheda:

inserisci qui la descrizione dell'immagine

Per imporre l'ordinazione delle tessere in ordine decrescente monotonico, il punteggio viene calcolato come la somma dei valori linearizzati sul tabellone moltiplicati per i valori di una sequenza geometrica con rapporto comune r <1.

S

S

Diversi percorsi lineari possono essere valutati contemporaneamente, il punteggio finale sarà il punteggio massimo di qualsiasi percorso.

Regola decisionale

La regola decisionale implementata non è abbastanza intelligente, il codice in Python è presentato qui:

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

Un'implementazione di minmax o Expectiminimax migliorerà sicuramente l'algoritmo. Ovviamente una regola di decisione più sofisticata rallenterà l'algoritmo e richiederà un po 'di tempo per essere implementata. Proverò un'implementazione minimax nel prossimo futuro. (rimanete sintonizzati)

Prova delle prestazioni

  • T1 - 121 prove - 8 percorsi diversi - r = 0.125
  • T2 - 122 test - 8 percorsi diversi - r = 0,25
  • T3 - 132 test - 8 percorsi diversi - r = 0,5
  • T4 - 211 test - 2 percorsi diversi - r = 0.125
  • T5 - 274 test - 2 percorsi diversi - r = 0,25
  • T6 - 211 test - 2 percorsi diversi - r = 0,5

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

Nel caso di T2, quattro prove su dieci generano la tessera 4096 con un punteggio medio di S42000

Codice

Il codice è disponibile su GiHub al seguente link: https://github.com/Nicola17/term2048-AI Si basa su term2048 ed è scritto in Python. Implementerò una versione più efficiente in C ++ il prima possibile.


Non male, la tua illustrazione mi ha dato un'idea, di prendere in considerazione i vettori di unione
Khaled.K

Ciao. Sei sicuro che le istruzioni fornite nella pagina github si applichino al tuo progetto? Voglio provarlo, ma quelle sembrano essere le istruzioni per il gioco giocabile originale e non per l'autorun AI. Potresti aggiornarli? Grazie.
JD Gamboa,

41

Il mio tentativo usa un aspetto simile ad altre soluzioni precedenti, ma senza bitboard. La soluzione di Nneonneo può controllare 10 milioni di mosse che è approssimativamente una profondità di 4 con 6 tessere rimaste e 4 mosse possibili (2 * 6 * 4) 4 . Nel mio caso, questa profondità richiede troppo tempo per essere esplorata, aggiusto la profondità della ricerca di waitimax in base al numero di tessere libere rimaste:

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

I punteggi delle schede vengono calcolati con la somma ponderata del quadrato del numero di tessere libere e il prodotto punto della griglia 2D con questo:

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

che costringe a organizzare le tessere in modo discendente in una sorta di serpente dalla tessera in alto a sinistra.

codice sotto o su 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
Non sono sicuro del perché questo non abbia più voti. È davvero efficace per la sua semplicità.
David Greydanus,

Grazie, risposta in ritardo e non funziona molto bene (quasi sempre in [1024, 8192]), la funzione costo / statistiche richiede più lavoro
caub

Come hai pesato gli spazi vuoti?
David Greydanus,

1
È semplicemente cost=1x(number of empty tiles)²+1xdotproduct(snakeWeights,grid)e cerchiamo di massimizzare questo costo
caub

grazie @Robusto, un giorno dovrei migliorare il codice, può essere semplificato
caub

38

Sono l'autore di un controller 2048 che ha un punteggio migliore di qualsiasi altro programma menzionato in questo thread. Un'implementazione efficiente del controller è disponibile su github . In un repository separato c'è anche il codice utilizzato per l'addestramento della funzione di valutazione dello stato del controller. Il metodo di allenamento è descritto nel documento .

Il controllore utilizza la ricerca di waitimax con una funzione di valutazione dello stato appresa da zero (senza esperienza umana 2048) da una variante dell'apprendimento delle differenze temporali (una tecnica di apprendimento di rinforzo). La funzione valore-stato utilizza una rete n-tupla , che è fondamentalmente una funzione lineare ponderata dei modelli osservati sulla scheda. Ha coinvolto oltre 1 miliardo di pesi , in totale.

Prestazione

A 1 mossa / e: 609104 (100 partite in media)

A 10 mosse / e: 589355 (300 partite in media)

A 3 strati (circa 1500 mosse / s): 511759 (1000 partite in media)

Le statistiche delle tessere per 10 mosse / s sono le seguenti:

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

(L'ultima riga significa avere le tessere date contemporaneamente sul tabellone).

Per 3 strati:

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

Tuttavia, non l'ho mai osservato ottenere la tessera 65536.


4
Risultato piuttosto impressionante. Tuttavia potresti forse aggiornare la risposta per spiegare (approssimativamente, in termini semplici ... Sono sicuro che i dettagli completi sarebbero troppo lunghi per pubblicare qui) come il tuo programma raggiunge questo? Come in una spiegazione approssimativa di come funziona l'algoritmo di apprendimento?
Cedric Mamo,

27

Penso di aver trovato un algoritmo che funziona abbastanza bene, poiché raggiungo spesso punteggi superiori a 10000, il mio migliore personale è di circa 16000. La mia soluzione non mira a mantenere i numeri più grandi in un angolo, ma a mantenerlo nella riga superiore.

Si prega di consultare il codice qui sotto:

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
Ho corso 100.000 giochi testando questo contro la banale strategia ciclica "su, destra, su, sinistra, ..." (e giù se necessario). La strategia ciclica ha concluso un "punteggio medio delle tessere" di 770.6, mentre questa ha ottenuto giusto 396.7. Hai indovinare perché potrebbe essere? Sto pensando che faccia troppi aumenti, anche quando sinistra o destra si fonderebbero molto di più.
Thomas Ahle,

1
Le tessere tendono ad impilarsi in modi incompatibili se non vengono spostate in più direzioni. In generale, l'uso di una strategia ciclica comporterà le tessere più grandi al centro, che rendono le manovre molto più anguste.
bcdan,

25

C'è già un'implementazione AI per questo gioco qui . Estratto da README:

L'algoritmo è la profondità di approfondimento iterativo prima ricerca alfa-beta. La funzione di valutazione cerca di mantenere monotoniche le righe e le colonne (tutte in diminuzione o in aumento) riducendo al minimo il numero di riquadri sulla griglia.

C'è anche una discussione su Hacker News su questo algoritmo che potresti trovare utile.


4
Questa dovrebbe essere la risposta migliore, ma sarebbe utile aggiungere ulteriori dettagli sull'implementazione: ad es. Come viene modellato il tabellone (come grafico), l'ottimizzazione impiegata (min-max la differenza tra le tessere) ecc.
Alceu Costa

1
Per i futuri lettori: questo è lo stesso programma spiegato dal suo autore (ovolve) nella seconda risposta più in alto qui. Questa risposta, e altre menzioni del programma di Ovolve in questa discussione, hanno spinto Ovolve a comparire e scrivere come funzionava il suo algoritmo; quella risposta ora ha un punteggio di 1200.
MoltiplicaByZer0

23

Algoritmo

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

    choose the maximum evaluation
}

Valutazione

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)

Dettagli di valutazione

128 (Constant)

Questa è una costante, usata come linea di base e per altri usi come i test.

+ (Number of Spaces x 128)

Più spazi rendono lo stato più flessibile, moltipliciamo per 128 (che è la mediana) poiché una griglia riempita con 128 facce è uno stato ottimale impossibile.

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

Qui valutiamo i volti che hanno la possibilità di fondersi, valutandoli all'indietro, la tessera 2 diventa di valore 2048, mentre la tessera 2048 viene valutata 2.

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

Qui abbiamo ancora bisogno di controllare i valori in pila, ma in modo minore che non interrompe i parametri di flessibilità, quindi abbiamo la somma di {x in [4,44]}.

+ (Number of possible next moves x 256)

Uno stato è più flessibile se ha più libertà di possibili transizioni.

+ (Number of aligned values x 2)

Questo è un controllo semplificato della possibilità di avere fusioni all'interno di quello stato, senza guardare avanti.

Nota: le costanti possono essere modificate.


2
Lo modificherò più tardi, per aggiungere un codice live @ nitish712
Khaled.K

9
Qual è la percentuale di vincita di questo algoritmo?
Cegprakash,

Perché hai bisogno di un constant? Se tutto ciò che stai facendo è confrontare i punteggi, in che modo influisce sul risultato di tali confronti?
bcdan,

@bcdan l'euristico (noto anche come punteggio di confronto) dipende dal confronto del valore atteso dello stato futuro, simile a come funziona l'euristica degli scacchi, tranne per il fatto che è euristica lineare, poiché non costruiamo un albero per conoscere le migliori mosse successive
Khaled.K,

12

Questa non è una risposta diretta alla domanda di OP, questo è più roba (esperimenti) che ho provato finora per risolvere lo stesso problema e ottenuto alcuni risultati e alcune osservazioni che voglio condividere, sono curioso di sapere se possiamo avere ulteriori approfondimenti da questo.

Ho appena provato la mia implementazione minimax con potatura alfa-beta con taglio di profondità dell'albero di ricerca a 3 e 5. Stavo cercando di risolvere lo stesso problema per una griglia 4x4 come assegnazione di un progetto per il corso edX ColumbiaX: CSMM.101x Artificial Intelligence ( AI) .

Ho applicato una combinazione convessa (provato diversi pesi euristici) di un paio di funzioni di valutazione euristica, principalmente dall'intuizione e da quelle discusse sopra:

  1. Monotonicità
  2. Spazio libero disponibile

Nel mio caso, il lettore del computer è completamente casuale, ma ho comunque assunto le impostazioni del contraddittorio e implementato l'agente del giocatore AI come massimo giocatore.

Ho una griglia 4x4 per giocare.

Osservazione:

Se assegno troppi pesi alla prima funzione euristica o alla seconda funzione euristica, entrambi i casi i punteggi ottenuti dal giocatore AI sono bassi. Ho giocato con molte possibili assegnazioni di peso alle funzioni euristiche e ho preso una combinazione convessa, ma molto raramente il giocatore AI è in grado di segnare 2048. Il più delle volte si ferma a 1024 o 512.

Ho anche provato l'angolo euristico, ma per qualche ragione peggiora i risultati, qualche intuizione perché?

Inoltre, ho cercato di aumentare l'interruzione della profondità di ricerca da 3 a 5 (non riesco ad aumentarla ulteriormente poiché la ricerca che lo spazio supera il tempo consentito anche con la potatura) e ho aggiunto un altro euristico che esamina i valori delle tessere adiacenti e fornisce più punti se sono in grado di unire, ma non riesco ancora a ottenere il 2048.

Penso che sarà meglio usare Expectimax invece di minimax, ma voglio comunque risolvere questo problema solo con minimax e ottenere punteggi alti come 2048 o 4096. Non sono sicuro che mi manchi qualcosa.

L'animazione di seguito mostra gli ultimi passaggi del gioco svolto dall'agente AI con il lettore del computer:

inserisci qui la descrizione dell'immagine

Eventuali approfondimenti saranno davvero molto utili, grazie in anticipo. (Questo è il link del mio post sul blog per l'articolo: https://sandipanweb.wordpress.com/2017/03/06/using-minimax-with-alpha-beta-pruning-and-heuristic-evaluation-to-solve -2048-game-with-computer / e il video di YouTube: https://www.youtube.com/watch?v=VnVFilfZ0r4 )

La seguente animazione mostra gli ultimi passaggi del gioco in cui l'agente del giocatore AI può ottenere 2048 punteggi, questa volta aggiungendo anche il valore assoluto euristico:

inserisci qui la descrizione dell'immagine

Le seguenti figure mostrano l' albero di gioco esplorato dall'agente AI del giocatore assumendo il computer come avversario per un solo passo:

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine


9

Ho scritto un risolutore 2048 in Haskell, principalmente perché sto imparando questa lingua in questo momento.

La mia implementazione del gioco differisce leggermente dal gioco reale, in quanto una nuova tessera è sempre un '2' (piuttosto che 90% 2 e 10% 4). E che la nuova tessera non è casuale, ma sempre la prima disponibile in alto a sinistra. Questa variante è anche nota come Det 2048 .

Di conseguenza, questo risolutore è deterministico.

Ho usato un algoritmo esaustivo che favorisce le tessere vuote. Si esibisce abbastanza rapidamente per profondità 1-4, ma a profondità 5 diventa piuttosto lento a circa 1 secondo per mossa.

Di seguito è riportato il codice che implementa l'algoritmo di risoluzione. La griglia è rappresentata come una matrice di numeri interi di 16 lunghezze. E il punteggio viene fatto semplicemente contando il numero di quadrati vuoti.

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

Penso che abbia abbastanza successo per la sua semplicità. Il risultato che raggiunge quando si inizia con una griglia vuota e si risolve a profondità 5 è:

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

Game Over

Il codice sorgente può essere trovato qui: https://github.com/popovitsj/2048-haskell


Prova ad estenderlo con le regole effettive. È una bella sfida nell'apprendere il generatore casuale di Haskell!
Thomas Ahle,

Sono molto frustrato dal fatto che Haskell stia provando a farlo, ma probabilmente ci proverò di nuovo! Ho scoperto che il gioco diventa notevolmente più semplice senza la randomizzazione.
wvdz,

Senza randomizzazione sono abbastanza sicuro che potresti trovare un modo per ottenere sempre 16k o 32k. Comunque la randomizzazione in Haskell non è poi così male, hai solo bisogno di un modo per passare attorno al `seme '. O esplicitamente, o con la monade casuale.
Thomas Ahle,

Affinare l'algoritmo in modo che raggiunga sempre 16k / 32k per un gioco non casuale potrebbe essere un'altra sfida interessante ...
wvdz,

Hai ragione, è più difficile di quanto pensassi. Sono riuscito a trovare questa sequenza: [SU, SINISTRA, SINISTRA, SU, SINISTRA, GIÙ, SINISTRA] che vince sempre il gioco, ma non supera il 2048. (In caso di mossa legale, l'algoritmo del ciclo sceglie semplicemente il prossimo in senso orario)
Thomas Ahle

6

Questo algoritmo non è ottimale per vincere il gioco, ma è abbastanza ottimale in termini di prestazioni e quantità di codice necessario:

  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
funziona meglio se dici che random from (right, right, right, down, down, up) non tutte le mosse hanno la stessa probabilità. :)
Daren,

3
In realtà, se sei completamente nuovo al gioco, ti aiuta davvero a usare solo 3 chiavi, sostanzialmente ciò che fa questo algoritmo. Quindi non così male come sembra a prima vista.
Cifre

5
Sì, si basa sulla mia osservazione con il gioco. Fino a quando non dovrai usare la 4a direzione, il gioco praticamente si risolverà da solo senza alcun tipo di osservazione. Questo "AI" dovrebbe essere in grado di arrivare a 512/1024 senza controllare il valore esatto di alcun blocco.
API-Bestia

3
Un'intelligenza artificiale corretta cercherebbe di evitare di raggiungere uno stato in cui può muoversi in una sola direzione a tutti i costi.
API-Bestia

3
Usare solo 3 direzioni è in realtà una strategia molto decente! Mi ha portato quasi al 2048 a giocare manualmente. Se lo combini con altre strategie per decidere tra le 3 mosse rimanenti, potrebbe essere molto potente. Per non parlare del fatto che ridurre la scelta a 3 ha un impatto enorme sulle prestazioni.
wvdz,

4

Molte delle altre risposte usano l'IA con una ricerca computazionalmente costosa di possibili futuri, euristica, apprendimento e così via. Sono impressionanti e probabilmente la via giusta da seguire, ma desidero contribuire con un'altra idea.

Modella il tipo di strategia che usano i buoni giocatori del gioco.

Per esempio:

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

Leggi i quadrati nell'ordine mostrato sopra fino a quando il valore dei quadrati successivi è maggiore di quello attuale. Questo presenta il problema di provare a unire un'altra tessera dello stesso valore in questo quadrato.

Per risolvere questo problema, ci sono 2 modi per spostarsi che non sono lasciati o peggio ancora ed esaminare entrambe le possibilità può rivelare immediatamente più problemi, questo forma un elenco di dipendenze, ogni problema che richiede un altro problema da risolvere per primo. Penso di avere questa catena o, in alcuni casi, l'albero delle dipendenze internamente al momento di decidere la mia prossima mossa, in particolare quando bloccato.


Le tessere devono fondersi con il vicino ma sono troppo piccole: unisci un altro vicino con questo.

Piastrella più grande in mezzo: aumenta il valore di una tessera circostante più piccola.

eccetera...


L'intero approccio sarà probabilmente più complicato di così, ma non molto più complicato. Potrebbe essere questa sensazione meccanica priva di punteggi, pesi, neuroni e ricerche approfondite di possibilità. L'albero delle possibilità deve anche essere raramente abbastanza grande da aver bisogno di qualsiasi ramificazione.


5
Stai descrivendo una ricerca locale con euristica. Questo ti bloccherà, quindi devi pianificare in anticipo per le prossime mosse. Ciò a sua volta porta anche a una ricerca e valutazione delle soluzioni (per decidere). Quindi questo non è davvero diverso da qualsiasi altra soluzione presentata.
runDOSrun,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.