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).
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.
1 2 3 |
public interface HangmanControllerInterface { public void processCommands(); } |
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.
1 2 3 4 5 6 7 8 9 |
public interface HangmanModelInterface { public boolean addLetter(char c); public void addBodyPart(); public boolean isGameOver(); public boolean isHumanPlayerWinner(); public void reset(); public int getBodyPartsCount(); public String getWord(); } |
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.
1 2 3 4 5 |
public interface HangmanViewInterface { public void displayHangman(); public void displayWord(); public void displayMessage(); } |
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ă:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import java.util.Scanner; public class HangmanTextController implements HangmanControllerInterface{ private HangmanModelInterface model; private HangmanViewInterface view; private Scanner scanner; public HangmanTextController(HangmanModelInterface model, HangmanViewInterface view) { this.model = model; this.view = view; scanner = new Scanner(System.in); } @Override public void processCommands() { boolean playAgain; do { model.reset(); view.displayHangman(); view.displayWord(); while (!model.isGameOver()) { view.displayMessage("Please enter a letter: "); String line = scanner.nextLine().toUpperCase(); if (line.length() != 1) { view.displayMessage("ERROR: Only letters are accepted!\n"); } else { char c = line.charAt(0); if (c < 'A' || c > 'Z') { view.displayMessage("ERROR: Only letters are accepted!\n"); } else { if (!model.addLetter(c)) { model.addBodyPart(); } view.displayHangman(); view.displayWord(); } } } if (model.isHumanPlayerWinner()) { view.displayMessage("CONGRATULATIONS!!!\n"); } else { view.displayMessage("YOU'VE BEEN HANGED...\n"); } view.displayMessage("Would you like to play again? "); playAgain = scanner.nextLine().toUpperCase().startsWith("Y"); } while (playAgain); } } |
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...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
import java.io.*; import java.util.ArrayList; import java.util.List; import java.util.Random; public class HangmanModel implements HangmanModelInterface { private List<String> words; private int bodyParts; private String word; private char[] guessedWord; public HangmanModel() throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader( new FileInputStream("words.txt"))); String word = br.readLine(); words = new ArrayList<String>(); while (word != null) { words.add(word.toUpperCase()); word = br.readLine(); } } @Override public boolean addLetter(char c) { boolean found = false; for (int i = 0; i < word.length(); i++) { if (word.charAt(i) == c) { guessedWord[i] = c; found = true; } } return found; } @Override public void addBodyPart() { bodyParts++; } @Override public boolean isGameOver() { return bodyParts == 6 || getWord().indexOf('_') == -1; } @Override public boolean isHumanPlayerWinner() { return getWord().indexOf('_') == -1; } @Override public void reset() { Random random = new Random(); word = words.get(random.nextInt(words.size())); int length = word.length(); guessedWord = new char[length]; for (int i = 0; i < length; i++) { guessedWord[i] = '_'; } addLetter(word.charAt(0)); addLetter(word.charAt(length - 1)); bodyParts = 0; } @Override public int getBodyPartsCount() { return bodyParts; } @Override public String getWord() { return String.valueOf(guessedWord); } } |
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?
1 2 3 4 5 6 7 8 |
+---+ | | O | /|\ | | | / \ | | ========= |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
import java.util.Arrays; public class HangmanTextView implements HangmanViewInterface { private HangmanModelInterface model; private static char[][] matrix = { " +---+ ".toCharArray(), " | | ".toCharArray(), " O | ".toCharArray(), " /|\\ | ".toCharArray(), " | | ".toCharArray(), " / \\ | ".toCharArray(), " | ".toCharArray(), "==============".toCharArray() }; public HangmanTextView(HangmanModelInterface model) { this.model = model; } @Override public void displayHangman() { int bodyParts = model.getBodyPartsCount(); char tmp[][] = new char[matrix.length][matrix[0].length]; for (int i = 0; i < matrix.length; i++) { tmp[i] = Arrays.copyOf(matrix[i], matrix[i].length); } switch (bodyParts) { case 0: tmp[2][7] = ' '; case 1: tmp[3][7] = tmp[4][7] = ' '; case 2: tmp[3][6] = ' '; case 3: tmp[3][8] = ' '; case 4: tmp[5][6] = ' '; case 5: tmp[5][8] = ' '; } for (char[] line : tmp) { System.out.println(String.valueOf(line)); } } @Override public void displayWord() { System.out.println(model.getWord()); } @Override public void displayMessage(String message) { System.out.print(message); } } |
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:
1 2 3 4 5 6 7 8 9 10 |
import java.io.IOException; public class Hangman { public static void main(String[] args) throws IOException { HangmanModelInterface model = new HangmanModel(); HangmanViewInterface view = new HangmanTextView(model); HangmanControllerInterface controller = new HangmanTextController(model, view); controller.processCommands(); } } |
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.