sabato 30 marzo 2013

Tetris

Ecco a vuoi un giochino programmato in C. Si tratta del famosissimo tetris!
Questa è la seconda versione che ho scritto del tetris. La prima era solo per sperimentare il disegno su console con un model e la gestione dell'input. Apprese queste conoscenze ed altre ho scritto una versione seria di questo gioco.

Penso non sia necessario spiegare che cosa sia il tetris, quindi passiamo subito al funzionamento.
Il tetris che ho programmato è una versione Multiplayer su console. I due giocatori giocano su una stessa tastiera premendo i caratteri per spostare il proprio tetramino. Chi fa più punti vince.

Nelle versioni precedenti dei giochi su console che ho programmato si manifestava un problema: ad ogni aggiornamento della grafica del gioco (ad ogni ciclo del while) tutto lo schermo veniva cancellato e riscritto. Questa operazione ha dei tempi non lunghissimi ma non invisibili. Per evitare questo problema ho pensato di utilizzare uno stack ed un buffer per gestire la console. Lo schermo viene quasi mai riscritto completamente, quando si deve fare una modifica allo schermo viene chiamata la funzione editBuffer che, come spiegato nei commenti, scrive nello stack la modifica da fare; quando si finisce di modificare il buffer si chiama flush che si occupa di disegnare solo le modifiche fatte nello stack. Gran parte dello schermo quindi non viene quasi mai modificata. Però un approccio del genere darebbe seri problemi a gestire le collisioni del blocco che scende. Per il blocco in caduta ho usato un approccio un po' diverso: memorizzo la posizione del blocco, il suo id e la sua forma (in una matrice 4x4). Per disegnare questo blocco non uso il buffer ma una funzione indipendente. Questa funzione stampa semplicemente nello schermo il tetramino. Non è sufficiente stamparlo così come ci capita, bisogna anche fare delle considerazioni: non si devono stampare gli spazi, infatti, andrebbero a sostituire dei caratteri che non vanno modificati; per questioni di sicurezza non scrive nulla se nel carattere di destinazione c'è già qualcosa. Per questioni puramente estetiche ho aggiunto anche la possibilità di cambiare il colore di ciò che viene disegnato con questo metodo. Se si deve spostare il blocco (ad esempio un po' più in basso) è necessario cancellare il blocco attuale per poi ridisegnare quello nuovo. Per eseguire ciò ho scritto anche un'altra funzione che fa proprio questo, riceve in input un blocco e cancella dallo schermo i caratteri che nel blocco sono diversi dallo spazio. Gli spazi nel blocco potrebbero infatti non essere spazi nello schermo...
Sinteticamente questo è il funzionamento della grafica del gioco...

Passiamo ora all'analisi del main. L'inizializzazione del gioco è abbastanza semplice. Pulisce il buffer dello schermo e ci inserisce la board. Non sarebbe necessario cancellare il buffer perché viene comunque sostituito dal file letto. Non è garantito però che il file sia perfetto, quindi per sicurezza è meglio spendere qualche millisecondo in più... ;)
Ora che il gioco è pronto bisogna iniziare la partita: si crea un nuovo blocco per A e per B.
Inizia quindi il ciclo del gioco, il quale continua finché almeno un giocatore sta giocando. Il ciclo è strutturato per funzionare anche se un giocatore ha perso. Non spendo troppo tempo a spiegarlo qui dato che è commentato bene ed è abbastanza semplice.

Un altro passo fondamentale per capire questo programma è l'analisi del metodo che gestisce le collisioni. I commenti ci sono ma credo sia meglio spendere un paio di parole in più su ciò. 
La funzione riceva da input due interi: dX e dY (delta-x e delta-y). Con ciò intendo lo spostamento che avrebbero i blocchi. Restituisce un valore che è zero se non ci sono collisioni, uno altrimenti. Per verificare ciò è necessario scorrere tutte le caselle del blocco. Nel codice questo è rappresentato da un ciclo for annidato in un altro. Viene quindi dichiarato un carattere (a) che rappresenta il carattere letto dal blocco ed un carattere (b) che rappresenta il corrispondente carattere del buffer dello schermo. Se il carattere del blocco e/o il carattere dello schermo sono spazi allora non c'è collisione. Infatti la collisizone avviene solo se entrambi i caratteri non sono degli spazi.

La gestione dell'input non è difficile da capire, fatta eccezione per il meccanismo di rotazione del blocco. Il metodo che ho usato non è forse il migliore ma il più semplice che mi è venuto in mente... per prima cosa salva il blocco attuale in una variabile di backup nel caso si dovesse ripristinare. Effettua la rotazione del blocco cambiano il suo id e il suo buffer. Ora controlla se nella nuova posizione avviene una collisione, se dovesse esserci allora ripristina il backup che aveva fatto, annullando quindi la modifica. Per lo spostamento a destra e a sinistra il funzionamento è più semplice: controlla la collisione si si spostasse di dX, sposta il blocco solo ne non si sovrapporrebbe a nulla.

Il prossimo passo è comprendere come funziona il metodo update. Non è difficile: se abbassando il blocco non c'è collisione allora è possibile farlo scendere. Se dovesse sovrapporsi allora il blocco è arrivato a fine corsa ed è necessario generarne un'altro. Il metodo update però ha anche un'altra funzione: quella di controllare se si ha riempito una riga e quindi di cancellarla. Per fare ciò controlla tutte le righe partendo dall'ultima e se ne trova una completa sposta tutto quello sopra di lui verso il basso di uno. E' necessario ripetere la verifica un'altra volta su questa riga dato che se non si facesse, il controllo verrebbe effettuato una riga sopra, la quale però è stata spostata verso il basso, quindi verrebbe saltata un'intera riga...

Per generare un nuovo blocco è necessario stampare nel model quello vecchio, per fare ciò ho usato lo stack del buffer dello schermo che avevo scritto. Scorre tutte le caselle del blocco e stampa nel buffer solo quelle diverse dallo spazio. Chiamando la funzione flush vengono rese definitive le modifiche. Per generare a caso il blocco ho usato la funzione rand per la x e per il tipo di blocco, per la y il valore è costante (4). Questo perché l'altezza di caduta dei blocchi è sempre la stessa. La chiave di questa funzione stà nell'eseguire il controllo di collisione: se nella posizione generato, il blocco generato dovesse collidere allora il gioco per quel giocatore finisce.

La funzione per la stampa dei punteggi è abbastanza semplice. Con degli if stabilisce quale giocatore stà vincendo ed imposta il colore del punteggio in base a ciò.

Per il comparto grafico vi do qualche cenno. Il file board.txt contiene lo schermo per tutta la durata del gioco. Viene infatti disegnata la board e le scritte in base a quel file.
Lo stack e le funzioni pop e push sono semplici e ben commentate e non richiedono particolare spiegazione. La funzione writeWithoutBuf e molte altre funzioni utilizzano gotoxy(int x, int y) la quale è definita in conio2.h. Questa libreria però non è presente in quelle del mio compilaotre quindi ho cercato una versione da mettere nel programma senza includere questo file. Come per setColor si basa su delle API della console di Windows, quindi non sono compatibili con altri sistemi operativi. Il programma l'ho testato sotto windows 7 e windows 8 a 64 bit. Compilato con il DevC++ versione 4.9.9.2 (portatile).
Normalmente uso la versione 5.4.0 di questo compilatore, ma per qualche strana ragione l'output con questa versione mi pesa quasi due megabyte mentre con quella precedente solo una ventina di kilobyte...

Download:
Dropbox - Google Drive

Screen:


Nessun commento:

Posta un commento