lunedì 19 settembre 2011

Tutorial: scriviamo un gioco in C++... Tris! (parte 6)

Nell'ultima puntata, risalente ormai a quasi due mesi fa (vergogna!), eravamo quasi arrivati al gioco completo. Quasi...

Il nostro gioco infatti funziona, ma non è molto interessante poichè il computer non ragiona sulle proprie mosse, ma si limita a posizionare simboli a caso nelle celle vuote. Vincere contro di lui è fin troppo facile.

Quel che ci manca è di fornire il computer di una sorta di intelligenza, o almeno di quella che basta per capire come controbattere alle nostre mosse e magari metterci in difficoltà.

Nel nostro caso specifico non si tratta di una gran difficoltà, e con un po’ di ragionamento si può giungere ad una soluzione casereccia o addirittura scomodare qualche algoritmo famoso come il minimax.

Il punto focale di questo post non è però il come scrivere la logica, ma il come organizzarne la gestione. Quel che faremo sarà creare un meccanismo per cui un tipo di intelligenza artificiale tra molte disponibili venga prelevato ed utilizzato ad ogni partita. Ognuno implementerà la propria logica per gestire le mosse.

Il come scrivere queste logiche dipende dal programmatore, dal suo intuito, il suo gusto, la sua formazione. It's up to you, come dicono quelli là.

Utilizzeremo due design pattern: Strategy e Factory. Strategy ci servirà per implementare le diverse AI (che nel nostro caso saranno due), mentre una factory ci fornirà una implementazione concreta ad ogni partita, secondo una logica random.

Più facile a vedersi che a dirsi. Iniziamo quindi con la classe base astratta StrategyAI.

#ifndef STRATEGYAI_H
#define STRATEGYAI_H

enum GridSymbols
{
EGS_X, EGS_O, EGS_NONE
};

class StrategyAI
{
public:
StrategyAI();
virtual ~StrategyAI();

virtual void PlaceSymbol(GridSymbols grid[3][3]) = 0;

protected:
private:
};

#endif // STRATEGYAI_H

Come vedete è occorso un po’ di refactor: abbiamo spostato qui la definizione dell’enumerativo dei simboli presente nella classe PlayGameState. Per il resto si tratta di una classettina molto semplice; contiene infatti un solo metodo virtuale puro atto a posizionare la mossa del computer all’interno della griglia. Le classi concrete che implementeranno StrategyAI dovranno implementare proprio questo metodo.

Una prima classe di esempio è DummyStrategyAI, che altro non fa che simulare quel che già avevamo, ovvero un riempimento casuale della griglia. Ecco la sua implementazione (ne ometto la dichiarazione)

#include "../include/DummyStrategyAI.h"
#include <cstdlib>

//-----------------------------------------------------------------------------
DummyStrategyAI::DummyStrategyAI() : StrategyAI()
{
//ctor
}

//-----------------------------------------------------------------------------
DummyStrategyAI::~DummyStrategyAI()
{
//dtor
}

//-----------------------------------------------------------------------------
void
DummyStrategyAI::PlaceSymbol(GridSymbols grid[3][3])
{
int gridx, gridy;

bool ok = false;
while(!ok)
{
gridx = rand()%3;
gridy = rand()%3;

if(grid[gridy][gridx] == EGS_NONE)
{
ok = true;
grid[gridy][gridx] = EGS_O;
}
}
}

Assieme alla DummyStrategyAI ho implementato anche una DefenderStrategyAI, una semplice AI che tende a bloccare i tentativi di tris del giocatore umano, pur non giocando d’attacco. Non riporto il codice di questa classe perchè non introduce nulla di nuovo.

Molto più interessante è vedere invece come queste classi vengono usate dalla PlayGameState.

Il metodo Reset è ora diventato così:

//-----------------------------------------------------------------------------
void
PlayGameState::Reset()
{
m_SymbolsPlacedCount = 0;

m_PlayerWins = m_ComputerWins = false;

if(m_pWinText)
{
SDL_FreeSurface(m_pWinText);
}
m_pWinText = 0;

for(int r=0; r<3; ++r)
{
for(int c=0; c<3; ++c)
{
m_Grid[r][c] = EGS_NONE;
}
}

if(m_pAI)
{
delete m_pAI;
m_pAI = 0;
}

StrategyAIFactory factory;
m_pAI = factory.CreateStrategyAI();
}

Come potete vedere c’è ora un nuovo membro, m_AI, che altro non è che un puntatore a StrategyAI. Tramite la factory otteniamo una istanza concreta di StrategyAI ad ogni chiamata della Reset, ovvero ad ogni inizio gioco. Il suo utilizzo poi è molto semplice: nel metodo HandleMouseInput, dove prima eseguivamo la logica random, ora abbiamo una sola riga:

m_pAI->PlaceSymbol(m_Grid);

Piuttosto intuitivo, no?

Non ci resta che dare un’occhiata alla StrategyFactory. La sua definizione:

#ifndef STRATEGYAIFACTORY_H
#define STRATEGYAIFACTORY_H

#include "StrategyAI.h"

class StrategyAIFactory
{
public:
StrategyAIFactory();
virtual ~StrategyAIFactory();

StrategyAI* CreateStrategyAI();

protected:
private:
};

#endif // STRATEGYAIFACTORY_H

… e la relativa implementazione:

#include <cstdlib>
#include "../include/StrategyAIFactory.h"
#include "../include/DummyStrategyAI.h"
#include "../include/DefenderStrategyAI.h"

//-----------------------------------------------------------------------------
StrategyAIFactory::StrategyAIFactory()
{
//ctor
}

//-----------------------------------------------------------------------------
StrategyAIFactory::~StrategyAIFactory()
{
//dtor
}

//-----------------------------------------------------------------------------
StrategyAI*
StrategyAIFactory::CreateStrategyAI()
{
int prob = rand() % 100;

if(prob < 50) return new DummyStrategyAI();
else return new DefenderStrategyAI();
}

Banalmente andiamo a scegliere con una probabilità del 50% tra le nostre due StrategyAI implementate. Ovviamente possiamo scrivere tutte le StrategyAI che vogliamo, arricchendo così il gioco di sfumature diverse e dando l’idea che il computer non si comporti seguendo sempre uno schema predefinito. Volendo, si potrebbe assegnare una faccina ad ogni Strategy e mostrarla nella schermata di gioco, umanizzando ulteriormente il computer e creando una certa aspettativa: il player inizierà a conoscere e distinguerei giocatori “forti” da quelli “scarsi”!

Con questo abbiamo finalmente concluso. TrisLick vuole essere un piccolo esempio di come realizzare qualcosa di piccolo ma concreto, lasciando aperte eventuali possibilità di sviluppo grazie ad una architettura flessibile.

Coraggio dunque, aprite il vostro editor, scaldate il compilatore e iniziate a scrivere il vostro prossimo gioco!

Buon coding!

Nessun commento:

Posta un commento