Qual è il modo migliore / più efficace per creare un'IA semi-intelligente per un gioco di tris?

3

in pratica sto tentando di creare un gioco C efficiente / piccolo di Tic-Tac-Toe. Finora ho implementato tutto il resto dell'IA per il computer. i miei quadrati sono fondamentalmente strutture in un array con un valore assegnato basato sul quadrato. Ad esempio

s[1].value = 1;

quindi è un x , e quindi un valore di 3 sarebbe un o . La mia domanda è qual è il modo migliore per creare un gioco semi-decente che giochi all'IA per il mio gioco tic-tac-toe? Non voglio davvero usare minimax, dal momento che non è quello di cui ho bisogno. Quindi, come evitare un sacco di se le statistiche e renderlo più efficiente.

Ecco il resto del mio codice:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

struct state{ // defined
    int state; // 0 is tie, 1 is user loss, 2 is user win, 3 is ongoing game
    int moves;
};

struct square{ // one square of the board
    int value; // 1 is x, 3 is o
    char sign; // no space used
};

struct square s[9]; //set up the struct
struct state gamestate = {0,0}; //nothing

void setUpGame(){ // setup the game
    int i = 0;
    for(i = 0; i < 9; i++){
        s[i].value = 0;
        s[i].sign = ' ';
    }
    gamestate.moves=0;
    printf("\nHi user! You're \"x\"! I'm \"o\"! Good Luck :)\n");
}

void displayBoard(){// displays the game board
    printf("\n %c | %c | %c\n", s[6].sign, s[7].sign, s[8].sign);
    printf("-----------\n");
    printf(" %c | %c | %c\n", s[3].sign, s[4].sign, s[5].sign);
    printf("-----------\n");
    printf(" %c | %c | %c\n\n", s[0].sign, s[1].sign, s[2].sign);
}

void getHumanMove(){ // get move from human
    int i;
    while(1){
        printf(">>:");
        char line[255]; // input the move to play
        fgets(line, sizeof(line), stdin);
            while(sscanf(line, "%d", &i) != 1) { //1 match of defined specifier on input line
            printf("Sorry, that's not a valid move!\n");
            fgets(line, sizeof(line), stdin);
        }
        if(s[i-1].value != 0){printf("Sorry, That moves already been taken!\n\n");continue;}
        break;
    }
    s[i-1].value = 1;
    s[i-1].sign = 'x';
    gamestate.moves++;
}

int sum(int x, int y, int z){return(x*y*z);}

void getCompMove(){ // get the move from the computer

}

void checkWinner(){ // check the winner
    int i;
    for(i = 6; i < 9; i++){ // check cols
        if((sum(s[i].value,s[i-3].value,s[i-6].value)) == 8){printf("The Winner is o!\n");gamestate.state=1;}
        if((sum(s[i].value,s[i-3].value,s[i-6].value)) == 1){printf("The Winner is x!\n");gamestate.state=2;}   
    }
    for(i = 0; i < 7; i+=3){ // check rows
        if((sum(s[i].value,s[i+1].value,s[i+2].value)) == 8){printf("The Winner is o!\n");gamestate.state=1;}
        if((sum(s[i].value,s[i+1].value,s[i+2].value)) == 1){printf("The Winner is x!\n");gamestate.state=2;}
    }
    if((sum(s[0].value,s[4].value,s[8].value)) == 8){printf("The Winner is o!\n");gamestate.state=1;}
    if((sum(s[0].value,s[4].value,s[8].value)) == 1){printf("The Winner is x!\n");gamestate.state=2;}
    if((sum(s[2].value,s[4].value,s[6].value)) == 8){printf("The Winner is o!\n");gamestate.state=1;}
    if((sum(s[2].value,s[4].value,s[6].value)) == 1){printf("The Winner is x!\n");gamestate.state=2;}
}

void playGame(){ // start playing the game
    gamestate.state = 3; //set-up the gamestate
    srand(time(NULL));
    int temp = (rand()%2) + 1;
    if(temp == 2){ // if two comp goes first
        temp = (rand()%2) + 1;
        if(temp == 2){
            s[4].value = 2; s[4].sign = 'o';
            gamestate.moves++;
        }else{
            s[2].value = 2; s[2].sign = 'o';
            gamestate.moves++;
        }
    } 
    displayBoard();
    while(gamestate.state == 3){
        if(gamestate.moves<10);
            getHumanMove();
        if(gamestate.moves<10);
            getCompMove();
        checkWinner();
        if(gamestate.state == 3 && gamestate.moves==9){
            printf("The game is a tie :p\n");
            break;
        }
        displayBoard();
    }
}

int main(int argc, const char *argv[]){
    printf("Welcome to Tic Tac Toe\nby The Elite Noob\nEnter 1-9 To play a move, standard numpad\n1 is bottom-left, 9 is top-right\n");
    while(1){ // while game is being played
        printf("\nPress 1 to play a new game, or any other number to exit;\n>>:");
        char line[255]; // input whether or not to play the game
        fgets(line, sizeof(line), stdin);
        int choice; // user's choice about playing or not   
        while(sscanf(line, "%d", &choice) != 1) { //1 match of defined specifier on input line
            printf("Sorry, that's not a valid option!\n");
            fgets(line, sizeof(line), stdin);
        }
        if(choice == 1){
            setUpGame(); // set's up the game
            playGame(); // Play a Game
        }else {break;} // exit the application
    }
    printf("\nThank's For playing!\nHave a good Day!\n");
    return 0;
}
    
posta Rivasa 07.03.2012 - 01:09
fonte

4 risposte

9

Quindi vuoi che l'umano abbia la possibilità di vincere?

Quindi gioca la mossa che blocca una linea vincente, o se non ce n'è una, gioca una mossa a caso. Forse pesi il centro per 3 e gli angoli per 2, con i bordi a 1, ma se quello che vuoi è un avversario che puoi battere, non vuoi essere intelligente.

Ovviamente, l'IA dovrebbe giocare una mossa vincente se ce n'è una.

    
risposta data 07.03.2012 - 01:29
fonte
3

3x3 Tic Tac Toe è un gioco abbastanza piccolo da poter enumerare in modo relativamente semplice tutte le possibili posizioni della tavola e identificare la prossima mossa corretta per ciascuna. Se si voleva affrontare qualcosa di un po 'più grande come 4x4x4, che consente al primo giocatore di forzare una vittoria ma non sempre facilmente, un buon approccio sarebbe quello di combinare l'euristica con un algoritmo min-max. Un'euristica ragionevolmente buona potrebbe, per ciascuna delle possibili linee vincenti, identificarla come vuota, come bloccata, o come con un punteggio di 1-4 giocatori da parte della persona che ha appena spostato, o 1-3 punti dall'altro giocatore. A ognuna di queste nove possibilità verrà assegnato un valore di punteggio (4 punti dalla persona che ha appena spostato il movimento essendo un valore di punteggio veramente enorme, e 3 punti dalla persona che sta per muoversi essendo un valore negativo non abbastanza grande (quindi se una mossa creerebbe un four-in-a-row, ciò lascerebbe una buona posizione di bordo per la persona che si è appena mossa anche se l'altro giocatore ha avuto un tre sbloccato, ma altrimenti lasciare un tre sbloccato dall'avversario sarebbe molto cattivo Un semplice approccio basato sul punteggio funzionerebbe testando ogni mossa possibile, calcolando quanto favorevole sarebbe la posizione della tavola dopo, e selezionando la mossa che produce la posizione più favorevole.Un approccio di questo tipo funzionerebbe in modo decente, ma non avrebbe un funzionamento ottimale.

Per migliorare la riproduzione del computer, utilizza quello che viene chiamato un algoritmo min-max. Per trovare il punteggio di "livello 0" per una possibile mossa, basta segnare la tabella che risulterebbe dopo lo spostamento. Per trovare il punteggio "livello n" per una possibile mossa, determinare quale possibile mossa successiva da parte dell'avversario produrrebbe il miglior punteggio "livello n-1" (per l'avversario). Il punteggio "livello n" è il miglior punteggio "livello n-1", con il segno invertito.

Gli algoritmi min-max possono funzionare molto bene per molti tipi di giochi a turni. Il più grande problema con loro è che possono essere molto lenti a meno che non vengano prese alcune misure per garantire l'efficienza. Tra l'altro, semplici algoritmi finiscono spesso per esaminare le posizioni della tavola molte decine di volte, se non centinaia. Trascorreranno inoltre molto tempo a valutare in profondità le contromosse possibili per le mosse che dovrebbero essere riconosciute come prive di merito. Inoltre, algoritmi semplici spesso assegnano il punteggio a ciascuna posizione di bordo "da zero". Fare un gioco giocabile richiede di affrontare tutti e tre i problemi: tenere traccia di quali posizioni sono state analizzate e quali punteggi hanno ricevuto; piuttosto che usare una ricerca in profondità per valutare ogni possibile mossa ad es. livello 5, usa una croce tra una ricerca di ampiezza e una coda di priorità, in modo che le mosse più belle vengano valutate per prime. Infine, utilizzare una struttura dati "incrementale" che consenta un punteggio rapido. Per esempio, oltre a tenere i 64 quadrati del tabellone, tieni i segnalini per ognuna delle possibili 16 + 16 + 16 + 24 + 8 possibili linee vincenti che contano quanti segnalini di ciascun giocatore sono sulla linea. L'aggiunta o la rimozione di un pezzo richiederebbe quindi l'incremento o il decremento dei conteggi delle linee vincenti su cui il pezzo appare, piuttosto che doverli ricalcolare tutti.

    
risposta data 06.10.2012 - 19:36
fonte
2

Se uno rappresenta X come +1 e O come -1 su una matrice 3x3, uno spostamento necessario è uno dove il valore assoluto della somma dei valori sul la linea è 2. Un gioco non necessario è nel terzo punto in cui il valore è 0.

Quindi, prende lo stato della scheda ed esamina ogni riga e piazza i punti vuoti su quella linea in uno dei tre elenchi:

  1. necessario
  2. OK
  3. inutili

Quindi l'algoritmo seleziona una riproduzione casuale da uno degli elenchi in ordine di necessità. Un miglioramento per il gioco più intelligente leggermente sarebbe quello di forzare questa selezione in base al numero di volte in cui il gioco esiste nell'elenco (questo farà sì che tenti di giocare al centro (4 linee possibili) e angoli (3 linee possibili) oltre i bordi (2 linee possibili).

O | X |
--+---+--
  | X |
--+---+--
  |   |

Diventerebbe:

    1  2  0  1
    |  |  | /
T  -1| 1 |  - 0
   --+---+--
C    | 1 |  - 1
   --+---+--
B    |   |  - 0
    L  c  R \ 0

In questa situazione, il punto vuoto nella colonna centrale verrebbe riprodotto perché il suo valore è 2. Poiché questo è O da riprodurre, questo sarebbe un blocco.

L'insieme completo di mosse e raggruppamenti per questo stato della scheda sarebbe:

Necessary:
  Bc (1)
Ok:
  TR (1)
  CL (2)
  CR (1)
  BL (2)
Unnecessary:
  TR (2)
  CR (1)
  BL (1)
  Bc (1)
  BR (3)

Si noti che una singola cella può benissimo cadere in più raggruppamenti (Centro inferiore Bc è sia necessario da riprodurre per vincere / bloccare, ma è anche non necessario giocare nella riga inferiore in quanto non contribuirà a una partita vincente o di blocco su quella riga).

La chiave qui è che identifica le giocate ovvie ma non guarda avanti per quale posizione della tavola è più strong in seguito (non ha alcuna preferenza per giocare le strategie conosciute per vincere con X o forzare un'estrazione con O ). Cercherà di bloccare o vincere impostazioni ovvie - se non lo blocchi quando è piazzato due su una linea, tenterà di vincere (a meno che tu non abbia anche una vittoria possibile, nel qual caso potrebbe decidere di bloccare piuttosto che vincere) .

Si noti che questo è quasi un sottoprodotto di rilevamento delle condizioni vincenti per la scheda, in quanto avrebbe una riga, una colonna o una diagonale che ha un valore assoluto dei suoi valori di 3.

    
risposta data 06.11.2013 - 21:08
fonte
0

Trivial, Basic, Simple concept of AI per Tic-tac-toe, che è stato il mio primo test per il mio prototipo di IA anni fa.

Gioco

  • N x N celle. N = 3 = > 9 celle
  • Ogni cella può avere 3 stati (Vuoto, 0, 1). Il gioco ha 39 = 19683 stati
  • Il giocatore deve scegliere 1 cella

Input AI: stato del gioco (1 di 81 stati)

  • 15 bit per le combinazioni di stato (log2 (39))
  • Not: utilizzando 2 bit per stato cella, 9x2 è 18 bit

Output AI: cella (sposta)

  • Usa 3 bit (usa 1 di 8 celle libere) - si 9th cell non verrà scelta, ma abbiamo 9 celle libere solo in prima iterazione e solo se iniziamo e in questo momento ha lo stesso significato di cell [0]
  • Not: utilizzando 9 bit (1 bit per cella)
  • Not: utilizzando 4 bit (indice a 4 bit della cella)

Feedback AI : partita vinta o persa (1 bit)

AI interno

  • int fbs [MAX_STATES] [MAX_MOVES]; // feedback
  • void Feedback (fb) {fbs [lastState] [lastMove] + = f; }
  • U32 Output (stato) {
  • prova a spostare casualmente ogni N_RAND'a volta (rand ()% N_RAND == 0)
    • return lastMove = rand ()% MAX_MOVES;
  • Trova max (fbs [lastState] [m = 0..MAX_MOVES])
    • se trovato, restituisce m; // rischi con questo per provare ad altre mosse avversarie.
    • else, forza casuale
  • lastState = state; lastMove = m;

Note: (puoi saltarle)

  • Poiché le rotazioni hanno lo stesso significato, ci sono stati meno significativi e potrebbero usare meno bit per quelli. Ma potremmo desiderare che l'IA "trovi" quelle "significatività" da sé e si ottimizzi successivamente
  • Output: indice di celle libere (altri output sono impossibili)
    • nella seconda iterazione possiamo avere fino a 5 opzioni (log (5) > 2bits), quindi l'output 3b è buono.
    • Ci sono degli output meno significativi (alla 1a iterazione: center, corner, side) - potrebbe avere output come indice di output significativi
  • Le mosse impossibili e non significative saranno ignorate dopo "allenamento" pesandole automaticamente (come mosse sbagliate) automaticamente.
  • Potrebbe utilizzare un feedback a 4 bit per (perso / vinto in N passaggi)
  • Se stato non trovato - potrebbe interpolare da stati simili invece di provare a caso
  • [Stato] [Sposta] combinati sono 15 + 3 18 b o meno. Quindi hai 157464 o meno combinazioni di spostamento di stato.
  • Usando la risoluzione 1B / precisione per il feedback si usa 157KB o RAM (se si passa a giochi più complessi, l'ottimizzazione dell'uso della memoria diventa il lavoro principale). Sta perdendo solo se vuoi ordinare ~ 5 mosse possibili dal loro fb, quindi 3b sarebbe sufficiente, ma con "codifica aritmetica" è ancora un grosso spreco di 3 ° bit. Nel modello allenato e ottimizzato solo 1 bit è sufficiente in quanto si desiderano solo le mosse migliori (1) e il feedback delle mosse peggiori dovrebbe andare su (0) (RAM: 20KB), o semplicemente scrivere le migliori mosse 3b per lo stato 19683 ( 7.4KB), può essere compresso meglio ...
  • Per renderlo più intelligente, puoi aggiungere l'invecchiamento ai feedback, in modo che se le regole del gioco cambiano, (o dovremmo chiamarla lei) inizierebbe a vincere prima.
  • Wiki: "138 posizioni della morsettiera"
    • Può fare solo 4-5 mosse al massimo prima di terminare in 1 di quelle 138 posizioni, significa che abbiamo meno di 5x138 decisioni da prendere, significa che le risposte migliori sono < 259B di dati (possibilmente intorno a 100B). Una buona intelligenza artificiale ottimizzerebbe l'utilizzo della RAM da 157 KB a 0,1 KB. La migliore intelligenza artificiale dovrebbe comprimere queste informazioni probabilmente a < 30 byte e utilizzare 157 KB di RAM per problemi molto più grandi.
risposta data 21.10.2015 - 11:47
fonte

Leggi altre domande sui tag