Proiectul constă în implementarea bine-cunoscutului joc 2048 pe un ecran monocrom Nokia 5110 84×48 pixeli, adăugând diverse elemente de quality of life precum:
Scopul acestui proiect este de a pune în practică toate noțiunile prezentate până acum în cadrul laboratoarelor de PM.
La pornirea jocului se va alege între a începe un joc nou și între a vedea un top cu cele mai mari scoruri de până acum. Când jucătorul alege să pornească un joc nou, pe ecran va fi încărcată scena jocului în care va putea să își vadă scorul curent și va putea să miște elementele stânga/dreapta sau sus/jos folosind un modul joystick PS2. La sfârșitul jocului, în cazul în care a câștigat, jucătorul va putea să își salveze scorul dacă acesta se încadrează în top-ul vechi.
Utilizatorul va avea câteva butoane tactile dedicate pentru pause, quit, continue. Jocul conține, de asemenea, un led RGB decorativ care luminează în culori diferite în funcție de acțiunile utilizatorului, precum și un buzzer care să redea sunetele. Utilizatorul va putea seta luminozitatea ecranului, precum și volumul buzzerului folosind potențiometre.
Denumire piesă | Preț piesă | Furnizor | Cantitate comandată |
---|---|---|---|
Arduino Uno R3 | 25 lei | cleste.ro | 1 |
Modul joysticks PS2 | 8 lei | cleste.ro | 1 |
Ecran Nokia 5110 85×48 | 19 lei | cleste.ro | 1 |
Buton Tactil 6x6x5mm | 1 leu | cleste.ro | 5 |
LED de 5mm diverse culori | 0.3 lei | cleste.ro | 50 |
LED RGB 5mm 4 pini catod comun | 2 lei | cleste.ro | 5 |
Breadboard 830 puncte | 15 lei | cleste.ro | 1 |
Fire Dupont tata-tata | 4 lei | cleste.ro | 30 |
Fire Dupont mama-tata | 4 lei | cleste.ro | 30 |
Potentiometru liniar 10K | 3 lei | ardushop.ro | 2 |
Buzzer pasiv | 3 lei | ardushop.ro | 5 |
Rezistor 220R | 0.4 lei | ardushop.ro | 20 |
Rezistor 1.2K | 0.4 lei | ardushop.ro | 20 |
Cablu USB A-B | 4 lei | ardushop.ro | 1 |
Mai jos se găsesc poze cu montajul hardware realizat și cu câteva imagini din joc:
În funcția de setup se pregătește configurația inițială a jocului:
În funcția de loop se apelează doar DISPLAY_GameLogic care se va ocupa de toată logica jocului. În funcția de GameLogic se apelează diverse alte funcții după valoarea variabilei g_state. La începutul programului variabila are valoarea quit_state care indică faptul că fie ne aflăm la începutul rulării, fie că s-a apăsat butonul quit, iar jocul a fost resetat. În această stare se citește cu ARDUINO_MyRead valoarea de pe axa Oy a joystick-ului. Dacă scade/crește față de pragul de repaus, atunci se schimbă valoarea variabilei g_option fie în scores (joystick-ul a fost mutat în jos și a fost selectat Highscores), fie în start (joystick-ul a fost mutat în sus și a fost selectat Start Game). În această stare se printează meniul principal cu selecția conform g_option, se redă un sunet specific operației de select și se schimbă culoarea led-ului.
Cât timp jocul se află în starea de quit_state singurele butoane care pot schimba starea sunt quit (care duce la resetarea jocului și, implicit, la întoarcerea în quit_state cu g_option setat pe start) sau continue care va schimba starea fie în score_state, fie în game_state.
În cazul score_state se apelează funcția DISPLAY_PrintScores care va afișa primele 5 cele mai mari scoruri obținute în joc sub forma X. [][][] - SCORE unde X reprezintă un număr de la 1 la 5, [] reprezintă un caracter de la A la Z iar SCORE reprezintă scorul jucătorului identificat de cele 3 caractere. Aceste date sunt preluate din memoria EEPROM a plăcuței Arduino, iar fiecare intrare reprezintă 5 bytes (3 pentru numele jucătorului și 2 pentru scor - int este reprezentat pe 2 bytes). Din această stare se poate reveni doar la starea quit_state apăsând quit.
În cazul game_state se citesc atât valoarea pe Oy cât și valoarea pe Ox a joystick-ului pentru a ști ce funcție trebuie apelată în continuare. Este posibil să se apeleze o singură funcție la un moment dat dintre cele 4 funcții DISPLAY_MoveUp/Down/Left/Right în funcție de valorile citite pe x și pe y, apoi se va apela funcția DISPLAY_PrintBoard care va afișa scorul și piesele curente de pe tabla de joc. PrintBoard apelează DISPLAY_PrintPiece pentru a afișa centrat orice piesă indiferent de valoare folosind calcule care țin cont de valorile posibile ale pieselor și de numărul de pixeli ocupați pe axa Ox a display-ului (3 pixeli + 1 pixel spațiu).
Funcțiile MoveXYZ folosesc același algoritm cu modificări în funcție de direcție, pentru simplitate voi explica ce se întâmplă în cazul MoveUp, pentru celelalte cazuri ideea este asemănătoare. MoveUp iterează prin fiecare linie, coloană cu coloană căutând o piesă validă (valoarea != 0). Când întâlnește o astfel de piesă începe să o „tragă” în sus până la o linie de stop; aici există două cazuri:
În cazul 2. se verifică dacă prin combinarea celor 2 piese s-a obținut 2048 apelând DISPLAY_CheckIfWon. La final după ce toate piesele au fost mutate în locul corespunzător se redă un sunet specific acțiunii de move, led-ul își schimbă culoarea și se generează o noua piesă apelând DISPLAY_GeneratePiece.
GeneratePiece iterează prin vectorul g_empty_tiles selectând doar spațiile goale (valoarea == empty) din tabla de joc și apoi selectează aleatoriu un spațiu din cele goale în care să pună un 2 sau un 4 (90% șansă pentru 2 și 10% pentru 4). Dacă în urma generării unei piese noi nu mai există niciun spațiu liber pe tabla de joc se verifică dacă jocul a fost sau nu pierdut apelând DISPLAY_CheckIfLost. CheckIfLost verifică dacă se poate realiza cel puțin o combinație mutând sus/jos/stânga/dreapta, caz în care jocul nu a fost încă pierdut, altfel jocul s-a terminat și jucătorul a pierdut.
CheckIfWon este o funcție simplă în care se verifică dacă jucătorul a obținut 2048, caz în care starea curentă se schimbă în win_state. Alte stări posibile din game_state sunt pause_state (a fost apăsat butonul pause), quit_state (a fost apăsat butonul quit) sau, conform celor menționate mai sus, în stare lose_state.
Starea pause_state afișează pe ecran un mesaj corespunzător (GAME PAUSED!) și setează g_pause pe 1, oprind scăderea scorului cu 10 puncte care se întâmplă în timpul stării game_state. Această stare ascunde tabla de joc pentru a nu oferi jucătorului timp de gândire fără penalizarea scorului. Din pause_state se poate reveni în game_state apăsând pe butonul continue care va afișa tabla de joc de la momentul de timp anterior apăsării butonului de pause.
În starea lose_state se afișează un mesaj corespunzător, se redă o melodie scurtă specifică înfrângerii și se setează led-ul pe roșu. În final se resetează jocul apelând DISPLAY_ResetGame care zeroizează tabla de joc, resetează starea la quit_state, precum și celelalte variabile la valorile inițiale.
Starea win_state este mai specială (funcția DISPLAY_PrintWin). Inițial, se urmează pașii de la lose_state afișând un mesaj corespunzător, melodia din g_win_melody
și schimbând culorile led-ului; după ce se termină această etapă, se realizează citirea valorilor de pe Ox și Oy ale joystick-ului pentru a itera
prin 3 caractere 'AA
A' care pot fi schimbate în intervalul A-Z. Când jucătorul și-a ales numele, acesta va apăsa pe butonul continue
pentru a începe operația de înregistrare a scorului său în top. Dacă se apasă pe quit în loc de continue, atunci se va reseta jocul, iar
scorul nu va fi înregistrat.
Înregistrarea scorului presupune obținerea datelor din memoria EEPROM și identificarea primului scor mai mic decât cel obținut de jucător. Dacă
nu există niciun scor mai mic decât cel curent, atunci nu se înregistrează, în caz contrar, scorul curent îl înlocuiește pe cel din top, iar restul
scorurilor mai mici ca acesta sunt coborâte cu o poziție (inclusiv cel înlocuit).
După ce jucătorul a câștigat/pierdut jocul, se realizează resetarea acestuia și întoarcerea în meniul principal. Acum acesta poate porni un joc nou sau poate vedea noul top.
Proiectul a ieșit în mare parte exact cum mi-am propus de la început. Din păcate am rămas foarte rapid fără pini liberi pe plăcuța Arduino și a trebuit să renunț la ideea originală în care foloseam mai multe led-uri și buzzere pentru quality of life, dar am reușit să mă încadrez în pinii puși la dispoziție de plăcuță. În afară de acest mic inconvenient am realizat exact tot ceea ce am dorit să fac pentru acest proiect. Personal, consider că rezultatul final este mult mai bun față de imaginea pe care o aveam la început .
Când am început lucrul efectiv pentru proiect am avut multe momente când nu știam cu siguranță dacă ceea ce mi-am propus era în totalitate posibil de realizat, majoritatea problemelor venind din partea lucrului cu LCD-ul; am trecut prin multe iterații ale mesajelor afișate și ale reprezentării pieselor pe ecran. Am căutat foarte multe biblioteci care să ofere suport pentru un font mai mic astfel încât să încapă 4 numere pe 4 cifre pe o linie. Din păcate, majoritatea bibliotecilor disponibile pentru lucrul cu Nokia 5110 nu aveau nici măcar pe aproape la fel de multe funcționalități precum biblioteca din partea ADAFRUIT. Pe această problemă am pierdut cel mai mult timp, în final alegând să rămân la ADAFRUIT și găsind după foarte multe căutări o bibliotecă de fonturi compatibile cu Adafruit_GFX.
Un alt challenge, din nefericire, a venit încă de la început când am primit piesele. Ecranul Nokia 5110 nu a venit lipit (nu mai există modelul gata lipit) și a trebuit să realizez lipirea ecranului pe coloana de pini . Rezultatul putea fi mai bun, dar cel puțin funcționează cum trebuie .
Cu toate acestea, am reușit să duc proiectul la bun sfârșit și consider că am reușit să aprofundez mai mult și mai bine noțiunile prezentate la laborator. Proiectul a fost într-adevăr o provocare foarte bună care a venit ca un suport pentru ce ne-a fost prezentat atât la curs cât și în cadrul laboratoarelor.
De-a lungul procesului de realizare a proiectului am obținut motivația de a face și alte astfel de proiecte orientate pe partea de hardware. Concluzionând, consider că proiectul a fost un challenge bine-venit și sunt foarte satisfăcut de rezultatul final .