Code Blocks

Tetris

Tutorial originale

  1. Avviare un nuovo progetto: chiamarlo giocoTetris.

  2. Sul frame principale inserire un wxBoxSizer;

  3. Nel sizer inserire un wxPanel;

  4. Sul Pannello inserire un altro wxBoxSizer;

  5. Nel BoxSizer inserire due componenti wxStaticText sovrapposti (applicare l'orientamento verticale al box sizer).
    Modificare la proprietà Label del primo in: "Istruzioni". Nel secondo componente wxStaticText scrivere le istruzioni (spazio: caduta veloce, P: pausa ecc..)

  6. Aprire l'editor del menu ed aggiungere la voce: Nuova partita nel menu File.

  7. File → New Class … → [class name:] Forma. → Create.
    Rispondere si alla richiesta di aggiungere la classe al progetto e accettare la creazione della versione di Debug e di Release.

Completare o sostituire il file header della classe Forma:

forma.h

#ifndef FORMA_H
#define FORMA_H

enum Tetramini { 
               Vuoto, FormaZeta, FormaEsse, FormaLinea,
               FormaT, FormaQuadra, FormaElle, FormaElleRiflessa 
               };

Tetramini è un nuovo tipo del quale vengono forniti i possibili valori che una variabile di questo tipo può assumere.

La classe Forma Possiede le proprietà private:

e i metodi:


class Forma
{
    private:
      int coords[4][2]; // un tetramino è contenuto in una griglia di 4x2 quadrati
      Tetramini nrForma;
      void SetX(int riga, int valore) {
        coords[riga][0] = valore;
      }
      void SetY(int riga, int valore) {
        coords[riga][1] = valore;
      }

L'interfaccia pubblica della classe contiene il costruttore, il distruttore e i metodi per usare una forma:


  public:
    Forma() { // il costruttore imposta un tetramino vuoto
      impostaForma(Vuoto);
    }
    void impostaForma(Tetramini IDTetramino);
    void generaForma();
    Tetramini leggiForma() const { return nrForma; }
/* il metodo x riceve una coordinata di riga e restituisce il valore della prima colonna dell'array coords */
    int x(int riga) const { return coords[riga][0]; }
/* il metodo y riceve una coordinata di riga e restituisce il valore della seconda colonna dell'array coords */
    int y(int riga) const { return coords[riga][1]; }
    int MinX() const;
    int MaxX() const;
    int MinY() const;
    int MaxY() const;
    Forma RuotaAntiOrario() const;
    Forma RuotaOrario() const;
    virtual ~Forma(); // il distruttore.
    protected:
};
#endif // FORMA_H

Forme dei tetramini:

Per disegnare un Tetramino (una forma composta da 4 quadrati), si sceglie un quadrato di riferimento, cioè gli si assegna la posizione (0, 0).
Le posizioni degli altri quadrati vengono indicate con due coordinate che rappresentano il numero di righe e il numero di colonne che distano dalla casella di riferimento.

Le forme scelte sono indicate con le seguenti coordinate:

 

a cui corrispondono i seguenti quadrati:

{0, -1}, {0, 0}, {-1, 0}, {-1, 1}         -1, 0 -1, 1      
FormaZeta   0, -1 0, 0    
{0, -1}, {0, 0}, {1, 0}, {1, 1}   0, -1 0, 0    
FormaEsse     1, 0 1, 1  
{0, -1}, {0, 0}, {0, 1}, {0, 2}   0, -1 0, 0 0, 1 0, 2
FormaLinea          
{-1, 0}, {0, 0}, {1, 0}, {0, 1}     -1, 0    
FormaT     0, 0 0, 1  
      1, 0    
{0, 0}, {1, 0}, {0, 1}, {1, 1}     0, 0 0, 1  
FormaQuadra     1, 0 1, 1  
{-1, -1}, {0, -1}, {0, 0}, {0, 1}     -1, -1    
FormaElle     0, -1 0, 0 0, 1
{1, -1}, {0, -1}, {0, 0}, {0, 1}     0, -1 0, 0 0, 1
FormaElleRiflessa     1, -1    

Forma.cpp

Completare il file di implementazione della classe:

  #include <stdlib.h>
  #include "../include/Forma.h"
  using namespace std;

Il metodo impostaForma riceve, come parametro, uno dei possibili valori di tipo Tetramini.
Dichiara una matrice statica, cioè ne esiste una sola copia che viene usata da tutte le istanze della classe Forma.
Questa matrice indica, per ogni riga, la forma di uno dei 6 tetramini.
Usa il parametro ricevuto per accedere alla riga della matrice statica e copia i valori nella proprietà coords.
Infine memorizza nella proprietà nrForma il numero della riga della matrice statica da cui ha prelevato le coordinate.

  void Forma::impostaForma(Tetramini IDTetramino) {
    static const int coordRelative[8][4][2] = {
        { { 0, 0 },   { 0, 0 },   { 0, 0 },   { 0, 0 } },
        { { 0, -1 },  { 0, 0 },   { -1, 0 },  { -1, 1 } },
        { { 0, -1 },  { 0, 0 },   { 1, 0 },   { 1, 1 } },
        { { 0, -1 },  { 0, 0 },   { 0, 1 },   { 0, 2 } },
        { { -1, 0 },  { 0, 0 },   { 1, 0 },   { 0, 1 } },
        { { 0, 0 },   { 1, 0 },   { 0, 1 },   { 1, 1 } },
        { { -1, -1 }, { 0, -1 },  { 0, 0 },   { 0, 1 } },
        { { 1, -1 },  { 0, -1 },  { 0, 0 },   { 0, 1 } }
    };
    
    for (int i = 0; i < 4 ; i++) {
        for (int j = 0; j < 2; ++j)
            coords[i][j] = coordRelative[IDTetramino][i][j];
    }
    nrForma = IDTetramino; // memorizza la riga contenente le coordinate.
  } 

Il metodo generaForma genera un numero casuale, compreso tra 1 e 7, che usa come indice per acquisire il corrispondente valore del tipo enumerato Tetramini. Tale valore viene passato al metodo impostaForma.

  void Forma::generaForma() {
    int x = rand() % 7 + 1; // escludi la prima riga.
    impostaForma(Tetramini(x));
  }

Il metodo MinX cerca, tra i quadrati del tetramino, quello che ha la coordinata X più a sinistra

  int Forma::MinX() const {
    int m = coords[0][0]; // assume che la posizione X del primo quadrato del tetramino sia la minore tra le 4.
    for (int i=0; i<4; i++)
      m = min(m, coords[i][0]); // se ne trova una più piccola aggiorna il minimo
    return m;
  }

Il metodo MaxX cerca, tra i quadrati del tetramino, quello che ha la coordinata X più a destra

  int Forma::MaxX() const {
    int m = coords[0][0];//assume che la posizione X del primo quadrato del tetramino sia la maggiore tra le 4.
    for (int i=0; i<4; i++)
      m = max(m, coords[i][0]); // se ne trova una più grande aggiorna il massimo
    return m;
  }

Il metodo MinY cerca, tra i quadrati del tetramino, quello che ha la coordinata Y più bassa

  int Forma::MinY() const {
    int m = coords[0][1]; //assume che la posizione Y del primo quadrato del tetramino sia la minore tra le 4.
    for (int i=0; i<4; i++)
      m = min(m, coords[i][1]); // se ne trova una più piccola aggiorna il minimo
    return m;
  }

Il metodo MaxY cerca, tra i quadrati del tetramino, quello che ha la coordinata Y più alta

  int Forma::MaxY() const {
    int m = coords[0][1]; //assume che la posizione Y del primo quadrato del tetramino sia la maggiore.
    for (int i=0; i<4; i++)
      m = max(m, coords[i][1]); // se ne trova una più grande aggiorna il massimo
    return m;
  }

I metodi RuotaAntiOrario e RuotaOrario restituiscono la stessa forma del tetramino se quello corrente è quadrato. Altrimenti creano un'istanza di classe Forma, a cui assegnano la stessa proprietà nrForma, e poi scambiano le coordinate. Infine restituiscono la forma con cui appare il tetramino nel nuovo orientamento.

  Forma Forma::RuotaOrario() const {
    if (nrForma == FormaQuadra) return *this;
    Forma tmpForma; // crea un'istanza di Tetramino
    tmpForma.nrForma = nrForma; // la inizializza con la forma corrente
    for (int i = 0; i < 4; ++i) {
/* rispetto alla casella di riferimento, le coordinate Y delle caselle vengono assegnate alle coordinate x*/
      tmpForma.SetX(i, y(i));
        
/* rispetto alla casella di riferimento, le coordinate x delle caselle vengono ribaltate e 
   assegnate alle coordinate y, quelle di sinistra passano in alto e quelle di destra passano in basso.*/
      tmpForma.SetY(i, -x(i));
    }
    return tmpForma;
  }

Esempio

Rotazione di un tetramino in senso orario:
  1. Coordinate e posizione del
    tetramino prima della rotazione
       
  1. Coordinate e posizione del tetramino
    dopo la Rotazione in senso orario
  (x) (y)                   x y                     
0 -1 0 -1, 0 SetX(0,y(0)) 0 1 SetY(0, -x(0))
1 0 0 0, 0 0, 1 SetX(1,y(1)) 0 0 SetY(1,-x(1)) 0, -1 0, 0  0, 1 
2 1 0 1, 0   SetX(2,y(2)) 0 -1 SetY(2,-x(2)) 1, 0
3 0 1     SetX(3,y(3)) 1 0 SetY(3,-x(3))
  Forma Forma::RuotaAntiOrario() const {
    if (nrForma == FormaQuadra) return *this;
    Forma tmpForma;
    tmpForma.nrForma = nrForma;
    for (int i = 0; i < 4; ++i) {
      tmpForma.SetX(i, -y(i));
      tmpForma.SetY(i, x(i));
    }
    return tmpForma;
  }

Esempio

Rotazione di un tetramino in senso antiorario:
  1. Coordinate e posizione del
    tetramino prima della rotazione
     
  1. Coordinate e posizione del tetramino
    dopo la Rotazione in senso antiorario
  (x) (y)                   x y                     
0 -1 0 -1, 0 SetX(0,-y(0)) 0 -1 SetY(0,x(0)) -1, 0
1 0 0 0, 0 0, 1 SetX(1,-y(1)) 0 0 SetY(1,x(1)) 0, -1 0, 0  0, 1 
2 1 0 1, 0   SetX(2,-y(2)) 0 1 SetY(2,x(2))
3 0 1     SetX(3,-y(3)) -1 0 SetY(3,x(3))
  Forma::~Forma()
  {     //distruttore
  }

Costruzione del campo di gioco

Per disegnare nell'area client dell'applicazione si inserisce un pannello all'interno di un frame.

campoGioco.h

#ifndef CAMPOGIOCO_H
#define CAMPOGIOCO_H
#include "include/Forma.h"
class campoGioco : public wxPanel {
public:
    campoGioco(wxWindow* parent,wxWindowID id=wxID_ANY,const wxPoint& pos=wxDefaultPosition,const wxSize& dimensioni=wxDefaultSize);
    void Start();
    void Pausa();
    virtual ~campoGioco();
    void lineeEliminate(int nrLinee);
private:
/* Numero di righe (10) e numero di colonne (22) del campo di gioco.*/
    enum { nrColonne = 10, nrRighe = 22 };
/*legge una casella della griglia di gioco e restituisce la forma contenuta */
    Tetramini& FormaIn(int x, int y) {return griglia[(y * nrColonne) + x];}
/* calcolo delle dimensioni di un quadrato che compone un tetramino */
    int Lato() { return GetClientSize().GetWidth() / nrColonne; }
    int Altezza() { return GetClientSize().GetHeight() / nrRighe; }

    void cancellaGriglia();
    void Caduta();
    void riduciUnaLinea();
    void FormaCaduta();
    void rimuoviLineeComplete();
    void nuovoPezzo();
    bool verificaMossa(const Forma& newPiece, int newX, int newY);
    void disegnaQuadrato(wxPaintDC &dc, int x, int y, Tetramini IDTetramino);

    bool avviato;
    bool inPausa;
    bool fineCaduta;
    Forma formaCorrente;
    int xForma;
    int yForma;
    int numLineeRimosse;
    Tetramini griglia[nrColonne * nrRighe];
    wxStatusBar *m_stsbar;
};
#endif
void campoGioco::OnPaint(wxPaintEvent& event) {
/* crea un device context  */
    wxPaintDC dc(this);
/* ottiene le dimensioni dell'area client della finestra */
    wxSize dimensioni = GetClientSize();
/* decide la quota da cui cadranno i tetramini  */
    int inizioCaduta = dimensioni.GetHeight() - nrRighe * Altezza();
/* per tutte le posizioni della griglia di gioco */
    for (int i = 0; i < nrRighe; ++i) {
        for (int j = 0; j < nrColonne; ++j) {
/* legge il codice del tetramino */
            Tetramini IDTetramino = FormaIn(j, nrRighe - i - 1);
            if (IDTetramino != Vuoto)
                disegnaQuadrato(dc, 0 + j * Lato(),
                           inizioCaduta + i * Altezza(), IDTetramino);
        }
    }
/* dopo aver ridisegnato i tetramini caduti, disegna il tetramino in corso */
    if (formaCorrente.leggiForma() != Vuoto) {
        for (int i = 0; i < 4; ++i) {
            int x = xForma + formaCorrente.x(i);
            int y = yForma - formaCorrente.y(i);
            disegnaQuadrato(dc, 0 + x * Lato(),
                       inizioCaduta + (nrRighe - y - 1) * Altezza(),
                       formaCorrente.leggiForma());
        }
    }
}
void campoGioco::disegnaQuadrato(wxPaintDC& dc, int x, int y, Tetramini IDTetramino) {
    static wxColour colori[] = { wxColour(0, 0, 0), wxColour(204, 102, 102),
             wxColour(102, 204, 102), wxColour(102, 102, 204),
             wxColour(204, 204, 102), wxColour(204, 102, 204),
             wxColour(102, 204, 204), wxColour(218, 170, 0) };
    static wxColour luce[] = { wxColour(0, 0, 0), wxColour(248, 159, 171),
             wxColour(121, 252, 121), wxColour(121, 121, 252),
             wxColour(252, 252, 121), wxColour(252, 121, 252),
             wxColour(121, 252, 252), wxColour(252, 198, 0) };
    static wxColour ombra[] = { wxColour(0, 0, 0), wxColour(128, 59, 59),
             wxColour(59, 128, 59), wxColour(59, 59, 128),
             wxColour(128, 128, 59), wxColour(128, 59, 128),
             wxColour(59, 128, 128), wxColour(128, 98, 0) };
    wxPen penna(luce[int(IDTetramino)]);
    penna.SetCap(wxCAP_PROJECTING);
    dc.SetPen(penna);
    dc.DrawLine(x, y + Altezza() - 1, x, y);
    dc.DrawLine(x, y, x + Lato() - 1, y);
    wxPen pennaScura(ombra[int(IDTetramino)]);
    pennaScura.SetCap(wxCAP_PROJECTING);
    dc.SetPen(pennaScura);
    dc.DrawLine(x + 1, y + Altezza() - 1,
        x + Lato() - 1, y + Altezza() - 1);
    dc.DrawLine(x + Lato() - 1,
        y + Altezza() - 1, x + Lato() - 1, y + 1);
    dc.SetPen(*wxTRANSPARENT_PEN);
    dc.SetBrush(wxBrush(colori[int(IDTetramino)]));
    dc.DrawRectangle(x + 1, y + 1, Lato() - 2,
        Altezza() - 2);
}

Mentre la scheda campoGioco.wxs è selezionata inserire il gestore dell'evento EVT_KEY_DOWN:

void campoGioco::OnKeyDown(wxKeyEvent& event) {
    if (!avviato || formaCorrente.leggiForma() == Vuoto) {
        event.Skip();
        return;
    }
    int codiceTasto = event.GetKeyCode();
    if (codiceTasto == 'p' || codiceTasto == 'P') {
      Pausa();
      return;
    }
    if (inPausa)
        return;
    switch (codiceTasto) {
    case WXK_LEFT:
        verificaMossa(formaCorrente, xForma - 1, yForma);
        break;
    case WXK_RIGHT:
        verificaMossa(formaCorrente, xForma + 1, yForma);
        break;
    case WXK_DOWN:
        verificaMossa(formaCorrente.RuotaOrario(), xForma, yForma);
        break;
    case WXK_UP:
        verificaMossa(formaCorrente.RuotaAntiOrario(), xForma, yForma);
        break;
    case WXK_SPACE:
        Caduta();
        break;
    case 'd':
        riduciUnaLinea();
        break;
    case 'D':
        riduciUnaLinea();
        break;
    default:
        event.Skip();
    }
}
void campoGioco::OnTimer(wxCommandEvent& event) {
    if (fineCaduta) {
        fineCaduta = false;
        nuovoPezzo();
    } else 
        riduciUnaLinea();
}
campoGioco.cpp

aggiungere le seguenti righe alla fine del costruttore:

  fineCaduta = false;
  avviato = false;
  inPausa = false;
  numLineeRimosse = 0;
  xForma = 0;
  yForma = 0;
  cancellaGriglia();
} // fine costruttore

Il metodo Start() verifica che il gioco non sia in pausa ed assegna i valori iniziali alle proprietà della classe.

void campoGioco::Start() {
    if (inPausa) return;
    avviato = true;
    fineCaduta = false;
    numLineeRimosse = 0;
    cancellaGriglia();
    nuovoPezzo();
    timer1->Start(300);
}

il metodo Pausa() verifica che il gioco sia avviato, commuta lo stato booleano della proprietà inPausa e, a seconda dello stato, ferma o riavvia il timer. In ogni caso mostra un messaggio nella barra di stato.

void campoGioco::Pausa() {
    if (!avviato) return;
    inPausa = !inPausa;
    if (inPausa) {
        timer1->Stop();
        m_stsbar->SetStatusText(wxT("pausa"));
    } else {
        timer1->Start(300);
        wxString str;
        str.Printf(wxT("%d"), numLineeRimosse);
        m_stsbar->SetStatusText(str);
    }
    Refresh();
}
void campoGioco::cancellaGriglia() {
    for (int i = 0; i < nrRighe * nrColonne; ++i)
        griglia[i] = Vuoto;
}
void campoGioco::Caduta() {
    int newY = yForma;
    while (newY > 0) {
        if (!verificaMossa(formaCorrente, xForma, newY - 1)) break;
        --newY;
    }
    FormaCaduta();
}
void campoGioco::riduciUnaLinea() {
    if (!verificaMossa(formaCorrente, xForma, yForma - 1)) FormaCaduta();
}
void campoGioco::FormaCaduta() {
    for (int i = 0; i < 4; ++i) {
        int x = xForma + formaCorrente.x(i);
        int y = yForma - formaCorrente.y(i);
        FormaIn(x, y) = formaCorrente.leggiForma();
    }
    rimuoviLineeComplete();
    if (!fineCaduta) nuovoPezzo();
}
void campoGioco::rimuoviLineeComplete() {
     int numFullLines = 0;
     for (int i = nrRighe - 1; i >= 0; --i) {
         bool lineaPiena = true;
         for (int j = 0; j < nrColonne; ++j) {
             if (FormaIn(j, i) == Vuoto) {
                 lineaPiena = false;
                 break;
             }
         }
         if (lineaPiena) {
             ++nrLineePiene;
             for (int k = i; k < nrRighe - 1; ++k) {
                 for (int j = 0; j < nrColonne; ++j)
                     FormaIn(j, k) = FormaIn(j, k + 1);
             }
         }
     }
     if (nrLineePiene > 0) {
         numLineeRimosse += nrLineePiene;
         wxString str;
         str.Printf(wxT("%d"), numLineeRimosse);
         m_stsbar->SetStatusText(str);
         fineCaduta = true;
         formaCorrente.impostaForma(Vuoto);
         Refresh();
     }
 }
void campoGioco::nuovoPezzo() {
    formaCorrente.generaForma();
    xForma = nrColonne / 2 + 1;
    yForma = nrRighe - 1 + formaCorrente.MinY();
    if (!verificaMossa(formaCorrente, xForma, yForma)) {
        formaCorrente.impostaForma(Vuoto);
        timer1->Stop();
        avviato = false;
        m_stsbar->SetStatusText(wxT("game over"));
    }
}
bool campoGioco::verificaMossa(const Forma& newPiece, int newX, int newY) {
    for (int i = 0; i < 4; ++i) {
        int x = newX + newPiece.x(i);
        int y = newY - newPiece.y(i);
        if (x < 0 || x >= nrColonne || y < 0 || y >= nrRighe)
            return false;
        if (FormaIn(x, y) != Vuoto)
            return false;
    }
    formaCorrente = newPiece;
    xForma = newX;
    yForma = newY;
    Refresh();
    return true;
}

Associare il gestore di evento onNuovaPartita alla voce di menu nuovaPartita:

  srand(time(NULL));
  Tetris *tetris = new Tetris(this);
  campoGioco *griglia = new campoGioco(tetris);
  tetris->Centre();
  tetris->Show(true);
  griglia->SetFocus();
  griglia->Start();

Nella sezione di intestazione del file aperto inserire:

#include "campogioco.h"
#include "Tetris.h"