Spânzurătoarea (I)

Astăzi vom face un joc. Cel din titlul articolului... Presupun că îl știți, dar îl voi prezenta pe scurt...

Descrierea jocului

Un jucător se gândește la un cuvânt, scrie prima și ultima literă și în locul celorlalte scrie liniuțe (dacă vreuna dintre literele din interior este la fel ca prima sau ca ultima, este scrisă și ea). Celălalt jucător încearcă să ghicească la ce cuvânt s-a gândit primul. El spune litere; de fiecare dată când ghicește o literă care apare în cuvânt, liniuța sau liniuțele corespunzătoare sunt înlocuite cu litera respectivă; dacă litera propusă de al doilea jucător nu se află în cuvânt, primul desenează sub o spânzurătoare o parte a corpului; se începe cu capul și se continuă cu trunchiul, mâna stângă, mâna dreaptă, piciorul stâng și piciorul drept. Jocul se termină în cazul în care este ghicit întregul cuvânt sau este desenat întregul corp.

Pe parcursul jocului, spânzurătoarea ar arăta, succesiv, ca în imaginile următoare (preluate de pe Wikipedia).

Hangman-0 Hangman-1Hangman-2 Hangman-3 Hangman-4 Hangman-5Hangman-7

Numele jocului în engleză este Hangman; și cum programatorii fac multe chestii în engleză, vom face și noi. În primul rând vom exemplifica ghicirea literelor folosind cuvântul hangman. La început ar trebui scrisă prima literă (H), cinci liniuțe și ultima literă (N). Dar a treia literă este și ea un N, deci liniuța corespunzătoare este înlocuită. Așadar, inițial cuvântul nostru ar arăta așa:

H_N___N

Dacă următoarea literă ghicită ar fi G, atunci linuța corespunzătoare ar fi înlocuită și cuvântul ar deveni:

H_NG__N

Următoarea literă ar putea fi A; ea apare de două ori în cuvânt; ambele liniuțe sunt înlocuite:

HANG_AN

Ultima literă ghicită ar fi M și întregul cuvânt ar fi descoperit:

HANGMAN

Acum știm cam ce ar trebui să facem... Considerăm că primul jucător este calculatorul. El trebuie să aleagă ce cuvânt trebuie să ghicim, să deseneze spânzurătoarea pe parcursul jocului și să afișeze cuvântul cu literele neghicite încă înlocuite cu liniuțe.

Proiectarea

Să vedem acum și cum facem... Vom folosi ceva numit model-view-controller (sau măcar vom încerca). Dacă nu știți ce e, să știți că e o chestie șmecheră, formată dintr-un model, un view și un controller. Nu v-ați fi așteptat, așa-i?!

Pe scurt, acest MVC (prescurtarea de la Model-View-Controller) funcționează cam așa: utilizatorul folosește controller-ul, care manipulează modelul; acesta actualizează view-ul care este văzut de utilizator.  Cam abstract, nu?

Să încercăm să clarificăm puțin... Partea centrală este modelul. Acesta se ocupă de datele care reprezintă starea curentă a aplicației. În cazul nostru, modelul ar trebui să știe care este cuvântul ales de calculator, care litere sunt vizibile și câte ori a fost aleasă o literă care nu face parte din cuvânt.

Controller-ul este cel care manipulează modelul. Utilizatorul va introduce litere; acestea vor fi primite de controller care va trimite modelului o comandă prin care îi cere să se actualizeze. De fiecare dată când primește o literă, modelul verifică dacă ea apare în cuvânt; dacă nu, crește numărul literelor alese greșit; dacă da, liniuțele corespunzătoare sunt înlocuite. După actualizarea modelului, controller-ul cere view-ului să afișeze noua situație: fie noi litere în cuvânt, fie încă o parte a corpului în spânzurătoare. Modelul trebuie să verifice și dacă jocul s-a încheiat.

View-ul trebuie doar să afișeze starea curentă a spânzurătorii și a cuvântului, folosind informații furnizate de model.

Până acum am discutat teoretic; totuși, trebuie să ajungem și la practică. Trebuie să decidem cum va arăta interacțiunea cu utilizatorul. Pentru prima versiune a jocului vom avea o interfață text: jucătorul va introduce litere de la tastatură și va vedea o reprezentare în mod text a spânzurătorii și a cuvântului. Vom avea mai multe versiuni...

Așadar, ca programatori inteligenți, vom lucra cu interfețe. Vom defini câte una pentru fiecare componentă și vom realiza o primă implementare pentru controller și pentru view care să lucreze cu intrarea și ieșirea standard. Ar fi frumos ca modelul să nu depindă deloc de interacțiunea cu utilizatorul, deci să sperăm că va rămâne nemodificat de la o versiune la alta. Totuși, vom crea o interfață și pentru el; poate decidem să-l implementăm altfel la un moment dat.

Mai trebuie să alegem limbajul: Java.

Implementarea

Am ales limbajul, avem o idee despre cum va fi structurat codul nostru și putem trece la implementare. Să definim interfețele:

Interfețele

Controller-ul trebuie doar să primească date de la utilizator. Așadar, va avea o metodă prin care va primi comezile și va cere efectuarea operațiilor corespunzătoare.

Modelul primește comenzi de la controller; vom avea o comandă care va adăuga o literă și o alta pentru a adăuga o parte o corpului în spânzurătoare. Cum controller-ul trebuie să știe dacă litera se află sau nu în cuvânt, prima comandă va trebui să returneze un anumit rezultat (vom folosi o valoare logică; vom returna true dacă litera se află în cuvânt și false în caz contrar). De asemenea, trebuie să putem verifica dacă jocul s-a încheiat și să determinăm cine este câștigătorul, deci avem două metode noi. Mai adăugăm o metodă pentru resetare (începerea unui nou joc). Mai trebuie ca modelul să poată furniza informații view-ului; vor exista două metode care să furnizeze starea curentă a spânzurătorii (numărul părților corpului afișate) și starea curentă a cuvântului.

View-ul trebuie să afișeze spânzurătoarea și cuvântul (cu literele descoperite); de asemenea, ar putea avea o metodă pentru a tipări un mesaj.

Implementarea controller-ului

Controller-ul interacționează cu modelul și cu view-ul. Va trebui să conțină două properietăți prin intermediul cărora să le acceseze. Vom adăuga și un Scanner pentru a citi datele introduse de utilizator.

Metoda care procesează comenzile va controla jocul. Pentru fiecare joc, va începe cu o resetare. Apoi, va cere succesiv comenzi până când jocul se va încheia. Va manipula modelul pe baza acestor comenzi și va cere view-ului să afișeze rezultatele. La sfârșit, va afișa câștigătorul și va întreba dacă se dorește începerea unui nou joc.

Implementarea nu este foarte complicată:

Am tratat câteva cazuri de eroare, dar nu foarte riguros (liniile 25 - 30). Am convertit în majuscule datele introduse de utilizator pentru a simplifica verificările (liniile 24 și 46).

Pentru unele mesaje am folosit marcaje de sfârșit de linie; pentru cele care preced informații introduse de utilizator nu am folosit așa ceva.

Implementarea modelului

Modelul trebuie să păstreze starea jocului și orice altă informație relevantă. Pentru starea jocului avem un întreg bodyParts care conține numărul părților corpului din spânzurătoare și un șir de caractere guessedWord care conține cuvântul (un caracter va fi liniuță dacă litera corespunzătoare nu a fost ghicită încă). Nu am folosit un String pentru a simplifica manipularea datelor (schimbarea unei liniuțe în litera corespunzătoare ar fi fost mai complicată pentru un String).

În constructor va trebui să încărcăm de undeva cuvintele disponibile. Le vom citi dintr-un fișier numit words.txt.

Pentru a adăuga o literă vom parcurge cuvântul original și, dacă litera curentă este egală cu cea care se vrea adăugată, vom actualiza elementul corespunzător din șirul guessedWord.

Pentru a adăuga o nouă parte a corpului, este suficient să incrementăm valoarea variabilei bodyParts.

Pentru a verifica dacă jocul s-a încheiat, trebuie să vedem dacă sunt afișate deja șase părți ale corpului (valoarea variabilei bodyParts este 6) sau dacă nu mai este nicio liniuță în șirul guessedWord.

Pentru a verifica dacă utilizatorul a câștigat este suficient să facem doar una dintre cele două verificări de la metoda anterioară (am ales-o pe a doua; când avem de ales, de obicei este mai bine să facem o verificare pozitivă; prima verificare ar fi trebuit să fie negată).

Pentru a reseta jocul, vom alege aleator unul dintre cuvintele disponibile, vom inițializa cu zero numărul părților corpului, cu liniuțe șirul guessedWord și vom considera că prima și ultima literă au fost deja ghicite (vom efectua două apeluri ale metodei addLetter; astfel ne asigurăm că, în cazul literele apar și în interior, liniuțele corespunzătoare sunt înlocuite).

Pentru a furniza numărul părților corpului din spânzurătoare este suficient să returnăm valoarea variabilei bodyParts.

Pentru a furniza starea actuală a cuvântului care trebuie ghicit, vom transforma într-un String șirul guessedWord.

Putem vedea că avem un cod mult mai simplu decât ne-am fi aștetat...

Se observă destul de ușor că nu am realizat o implementare eficientă. Performanța poate fi îmbunătățită, dar am preferat să avem un cod cât mai lizibil. De exemplu, liniile 56 - 60 pot fi rescrise astfel âncât să avem o singură parcurgere, nu trei.

De obicei performanța nu trebuie ignorată, dar în unele situații putem să renunțăm la optimalitate dacă acest lucru nu este vizibil pentru vizitator. În cazul nostru, o fracțiune de secundă de întârziere este acceptabilă.

Implementarea view-ului

Mai avem de prezentat informațiile pe ecran. Pentru cuvânt și pentru mesaje totul este destul de simplu. Să vedem ce facem cu spânzurătoarea. Nu putem pune elementele grafice de la începtul articolului, dar putem realiza ceva care să semene cât de cât. Ce ziceți de imaginea următoare?

Avem opt linii: vom avea o matrice de caractere pe care o vom afișa. În funcție de numărul părților corpului care trebuie afișate, anumite caractere vor fi înlocuite cu spații.

Din nou, codul propriu zis nu este foarte complicat.

Din nou, performanța poate fi mult îmbunătățită...

Programul principal

Avem deja toate componentele, mai trebuie doar să le "legăm" unele de altele și să pornim controller-ul. Programul principal este foarte simplu:

Va urma

Am văzut că după ce am proiectat totul, codul devine relativ simplu. Puteți încerca o implementare mai puțin complicată. Destul de probabil veți ajunge la un cod mai greu de înțeles...

Acum, putem înlocui view-ul și controller-ul pentru a avea o interfață grafică. Vom face acest lucru într-un articol viitor.