Penso che potrei generare tutti gli stati possibili per un tick di gioco, ma con quattro giocatori e 5 azioni di base (4 mosse e luogo della bomba) dà 5 ^ 4 stati al primo livello dell'albero del gioco.
Corretta! Devi cercare tutte le azioni 5 ^ 4 (o anche 6 ^ 4, dato che puoi camminare in 4 direzioni, fermarti e "mettere una bomba"?) Per ogni tick di gioco. MA, quando un giocatore ha già deciso di muovere, ci vuole del tempo prima che la mossa venga eseguita (ad es. 10 tick di gioco). Durante questo periodo il numero di possibilità si riduce.
Tale valore aumenterà in modo esponenziale con ogni livello successivo. Mi sto perdendo qualcosa? Ci sono modi per implementarlo o dovrei usare un algoritmo totalmente diverso?
È possibile utilizzare una tabella hash per calcolare lo stesso "sottotree" dello stesso stato di gioco solo una volta. Immagina che il giocatore A cammini su e giù, mentre tutti gli altri giocatori "aspettano", finisci nello stesso stato di gioco. È lo stesso di "sinistra-destra" o "destra-sinistra". Anche lo spostamento di "su-poi-a sinistra" e "a sinistra-poi-su" porta allo stesso stato. Utilizzando una tabella hash è possibile "riutilizzare" il punteggio calcolato per uno stato di gioco che è già stato valutato. Ciò riduce parecchio la velocità di crescita. Matematicamente, riduce la base della tua funzione di crescita esponenziale. Per avere un'idea di quanto riduce la complessità, esaminiamo le mosse possibili per un solo giocatore rispetto alle posizioni raggiungibili sulla mappa (= diversi stati di gioco) se il giocatore può semplicemente spostarsi su / giù / a sinistra / a destra / a fine .
profondità 1: 5 mosse, 5 stati diversi, 5 stati aggiuntivi per questa ricorsione
profondità 2: 25 mosse, 13 stati diversi, 8 stati aggiuntivi per questa ricorsione
profondità 3: 6125 mosse, 25 stati diversi, 12 stati aggiuntivi per questa ricorsione
Per visualizzarlo, rispondi a te stesso: quali campi sulla mappa possono essere raggiunti con una mossa, due mosse, tre mosse. La risposta è: tutti i campi con una distanza massima = 1, 2 o 3 dalla posizione iniziale.
Quando usi una HashTable devi solo valutare ogni stato di gioco raggiungibile (nel nostro esempio 25 alla profondità 3) una volta. Considerando che senza una tabella hash è necessario valutarli più volte, il che significherebbe 6125 valutazioni anziché 25 a livello di profondità 3. Il migliore: una volta calcolata una voce di HashTable, è possibile riutilizzarla in fasi temporali successive ...
Puoi anche utilizzare approfondimenti incrementali e sottotitoli di potatura alfa-beta "tagliati" che non vale la pena cercare in modo più approfondito. Per gli scacchi ciò riduce il numero di nodi cercati a circa l'1%. Una breve introduzione alla potatura alfa-beta può essere trovata come video qui: http://www.teachingtree.co/cs/watch?concept_name=Alpha-beta+Pruning
Un buon inizio per ulteriori studi è http://chessprogramming.wikispaces.com/Search . La pagina è legata agli scacchi, ma gli algoritmi di ricerca e ottimizzazione sono abbastanza gli stessi.
Un altro (ma complesso) algoritmo AI - che sarebbe più adatto al gioco - è l'apprendimento delle differenze temporali.
Saluti
Stefan
PS: se riduci il numero di possibili stati di gioco (ad es. Dimensioni molto ridotte della mappa, solo una bomba per giocatore, nient'altro), c'è la possibilità di pre-calcolare una valutazione per tutti gli stati di gioco.
--modificare--
È inoltre possibile utilizzare i risultati calcolati offline dei calcoli minimax per addestrare una rete neuronale. Oppure potresti usarli per valutare / confrontare strategie implementate a mano. Ad esempio, è possibile implementare alcune delle "personalità" suggerite e alcune euristiche che rilevano, in quali situazioni la strategia è buona. Pertanto, dovresti "classificare" le situazioni (ad es. Gli stati di gioco). Questo potrebbe anche essere gestito da una rete neuronale: formare una rete neuronale per prevedere quale delle strategie codificate a mano sta giocando meglio nella situazione attuale ed eseguirla. Ciò dovrebbe produrre decisioni in tempo reale estremamente buone per un gioco reale. Molto meglio di una ricerca con limite di profondità basso che può essere realizzata altrimenti, poiché non importa quanto tempo impiegano i calcoli offline (sono prima del gioco).
- modifica # 2 -
Se ricalcoli le mosse migliori solo ogni 1 secondo, potresti anche provare a eseguire una pianificazione di livello superiore. Cosa intendo con questo? Sai quante mosse puoi fare in 1 secondo. In questo modo puoi creare un elenco di posizioni raggiungibili (ad es. Se si trattasse di 3 mosse in 1 secondo, avresti 25 posizioni raggiungibili). Quindi potresti pianificare come: vai in "posizione xe piazza una bomba". Come alcuni altri hanno suggerito, è possibile creare una mappa di "pericolo", che viene utilizzata per l'algoritmo di routing (come andare in posizione x? Quale percorso dovrebbe essere preferito [ci sono alcune variazioni possibili nella maggior parte dei casi]). Ciò richiede meno memoria rispetto a un'enorme HashTable, ma produce risultati meno ottimali. Ma poiché utilizza meno memoria, potrebbe essere più veloce a causa degli effetti di memorizzazione nella cache (uso migliore delle cache di memoria L1 / L2).
INOLTRE: è possibile effettuare pre-ricerche che contengono solo mosse per un giocatore ciascuna per risolvere le variazioni che risultano perdere. Quindi togli tutti gli altri giocatori dal gioco ... Memorizza le combinazioni che ogni giocatore può scegliere senza perdere. Se ci sono solo mosse perdenti, cerca le combinazioni di mosse in cui il giocatore rimane in vita il più a lungo possibile. Per archiviare / elaborare questo tipo di strutture ad albero è necessario utilizzare un array con puntatori indice come questo:
class Gamestate {
int value;
int bestmove;
int moves[5];
};
#define MAX 1000000
Gamestate[MAX] tree;
int rootindex = 0;
int nextfree = 1;
Ogni stato ha un "valore" di valutazione e si collega ai Gamestates successivi quando si sposta (0 = stop, 1 = su, 2 = destra, 3 = giù, 4 = sinistra) memorizzando l'indice di matrice all'interno di "albero" nelle mosse [0 ] per spostare [4]. Costruire il tuo albero in modo ricorsivo potrebbe apparire così:
const int dx[5] = { 0, 0, 1, 0, -1 };
const int dy[5] = { 0, -1, 0, 1, 0 };
int search(int x, int y, int current_state, int depth_left) {
// TODO: simulate bombs here...
if (died) return RESULT_DEAD;
if (depth_left == 0) {
return estimate_result();
}
int bestresult = RESULT_DEAD;
for(int m=0; m<5; ++m) {
int nx = x + dx[m];
int ny = y + dy[m];
if (m == 0 || is_map_free(nx,ny)) {
int newstateindex = nextfree;
tree[current_state].move[m] = newstateindex ;
++nextfree;
if (newstateindex >= MAX) {
// ERROR-MESSAGE!!!
}
do_move(m, &undodata);
int result = search(nx, ny, newstateindex, depth_left-1);
undo_move(undodata);
if (result == RESULT_DEAD) {
tree[current_state].move[m] = -1; // cut subtree...
}
if (result > bestresult) {
bestresult = result;
tree[current_state].bestmove = m;
}
}
}
return bestresult;
}
Questo tipo di struttura ad albero è molto più veloce, poiché l'allocazione dinamica della memoria è davvero molto lenta! Ma anche memorizzare l'albero di ricerca è piuttosto lento ... Quindi questa è più una fonte d'ispirazione.