Tractament modular de dades. El joc de combats a l'arena

Els principis de la modularitat també són aplicables per resoldre problemes en els quals es duen a terme operacions amb fitxers. De fet, normalment dins de les aplicacions se solen generar mòduls clarament diferenciats vinculats a la gestió de totes les dades persistents.

En aquest apartat es planteja un exemple d’aplicació modular on es duu a terme el tractament de dades emmagatzemades en fitxers. Concretament, s’estudia com ampliar una aplicació ja existent, generada usant els principis de modularitat, de manera que aquests principis se segueixin mantenint en la nova versió amb noves funcionalitats.

Descripció del problema

El programa que serveix com a punt de partida és un joc on l’usuari, el jugador, es va enfrontant amb successius adversaris en una arena de combat. Tant el jugador com els seus adversaris són definits per un seguit de valors, que anomenaran els seus atributs, mitjançant els quals s’indica la seva perícia lluitant: resistència, nivell de poder, capacitat d’atacar o defensar, etc.

Cada combat es divideix en rondes, a l’inici de les quals el jugador i el seu adversari trien secretament una estratègia a seguir. En cada ronda es pot seguir una estratègia diferent. Segons les estratègies triades per cadascú, el combat s’anirà resolent més favorablement cap a un o cap a l’altre, fins que finalment es consideri que un dels dos ha estat derrotat. Si es derrota l’adversari, s’atorga una puntuació al jugador. Si el jugador és derrotat, acaba la partida. L’objectiu final del jugador és sobreviure deu combats, assolint la màxima puntuació possible en el procés.

A la secció “Annexos” del web disposeu del codi font complet de l’aplicació sobre la qual es treballa en aquest apartat. Estudieu-lo atentament.

Divisió del problema

Si bé el procés de resoldre les rondes de combat segons els atributs dels lluitadors té les seves particularitats, per plantejar quin és el funcionament de l’aplicació, es deixarà en aquesta descripció general, sense entrar en més detall. De totes maneres, si voleu fer la idea més aclaridora, tot seguit s’exposa la descomposició del problema mitjançant disseny descendent:

  1. Generar els atributs del nou jugador.
  2. Anunciar inici del combat.
    1. Mostrar estat del jugador.
  3. Triar l’adversari.
  4. Combatre.
    1. Mostrar estat dels lluitadors.
      1. Mostrar estat del jugador.
      2. Mostrar estat de l’adversari.
    2. Triar estratègia del jugador.
    3. Triar estratègia de l’adversari.
    4. Resoldre resultats d’estratègies.
      1. Llançar monedes.
      2. Penalitzar lluitador.
      3. Danyar lluitador.
      4. Guarir lluitador.
    5. Restaurar lluitador.
  5. Resoldre resultat del combat.
    1. Atorgar puntuació.
    2. Pujar de nivell.
    3. Finalització del joc.

Mòduls del programa

Partint de la divisió en subproblemes del joc de combats de l’arena, s’ha fet la divisió en mòduls següent. D’una banda, en el package joc.arena.regles hi haurà les classes:

  • Monedes: per a les tasques vinculades al llançament de monedes per resoldre una ronda.
  • Lluitador: per a les tasques vinculades a la manipulació de les dades d’un lluitador (danyar, guarir, etc.).
  • Bestiari: per a les tasques vinculades a la generació d’adversaris i el jugador.
  • Combat: per a les tasques vinculades a la resolució d’estratègies enfrontades.

D’altra banda, en el package joc.arena.interficie es decideix dividir les classes que tracten la pantalla i el teclat, de manera que hi haurà:

  • EntradaTeclat: s’encarrega de les tasques importants que són donades pel que escriu l’usuari usant el teclat.
  • SortidaPantalla: com l’anterior, però per mostrar informació en pantalla.

La classe principal, JocArena és al package que engloba els anteriors, joc.arena, donada la jerarquia de noms.

Partint d’aquesta descripció, es volen afegir dues funcionalitats noves:

  • Mantenir un rànquing amb les deu millor puntuacions obtingudes. Aquestes es desen en un fitxer, de manera que es mantenen entre partides diferents. En finalitzar el joc, si un jugador ha obtingut una puntuació que mereix estar entre les deu primeres, pot indicar quines són les seves inicials (tres lletres), perquè hi constin associades a la puntuació.
  • Ara mateix, els atributs dels adversaris estan escrits en el codi font del programa (a la classe Bestiari). Això impedeix afegir nous adversaris fàcilment. Caldria modificar el seu codi font i compilar de nou el programa. Estaria bé que les dades dels adversaris s’obtinguin d’un fitxer, de manera que, només modificant aquest fitxer, el programa ja incorpora sempre automàticament els adversaris continguts.

Criteris d'elecció de tipus de fitxer

Una de les primeres decisions que cal prendre quan cal tractar fitxers és establir com s’hi emmagatzemaran les dades (en format caràcter o byte) i l’accés que es durà a terme (seqüencial o relatiu). En molts casos, sigui quin sigui el sistema triat, el problema es podrà resoldre igualment. L’única diferència serà la complexitat de l’algorisme que cal implementar i el nombre de vegades que s’ha de llegir el fitxer. En qualsevol cas, usar accés relatiu sempre implicarà que el fitxer serà orientat a byte.

A la taula es resumeixen alguns pros i contres de cada tipus, perquè els tingueu en compte.

Taula Avantatges i desavantatges segons el tipus de fitxers
Tipus de fitxer Avantatges Desavantatges
Orientat a caràcter Són fàcils de crear i editar amb eines externes (un editor de text simple).
Són fàcils de depurar (veure si el seu contingut és correcte.
En llegir-los, és fàcil comprovar mitjançant codi de tipus d’una dada dades abans de llegir-la (mètodes hasNex…).
La mida que ocupa cada valor pot ser molt variable.
Cal controlar el nombre de valor en el fitxer, o alguna marca de finalització
Només permeten tractament seqüencial.
No es poden sobreescriure fragments concrets. Cal reescriure tot el fitxer al complet.
Orientat a byte La mida que ocupa cada valor es sempre la mateixa.
Lligat al punt anterior, es fàcil calcular quants valors contenen.
Permeten l’accés relatiu.
Permeten sobreescriptures parcials de les dades.
El codi és més complicat.
No és fàcil veure què contenen.
No permeten comprovar els tipus dels valors abans de llegir-los. En cas d’error, és difícil de detectar.

La biblioteca "joc.arena.fitxers"

Normalment, el tractament de dades dins de fitxers se sol fer en un package diferenciat. Aquesta decisió és coherent amb la de disposar, per exemple, d’un altre vinculat a l’entrada / sortida de dades amb el teclat i la pantalla. A l’hora de triar el nom del package, és recomanable mantenir també en el seu nom la relació jeràrquica entre mòduls dins l’aplicació. Per tant, aquest package es pot dir joc.arena.fitxers.

Una aproximació assenyada per enfocar l’estructura d’aquesta biblioteca és fer-ho de manera que cada fitxer sigui tractat per una classe diferent. En aquest cas, doncs, caldrà dues classes noves, una per tractar el fitxer amb la llista de puntuacions i un altre per al tractament dels adversaris.

La classe Ranquing

Aquesta classe serà l’encarregada de gestionar el fitxer amb les màximes puntuacions. Ha de poder tant llegir-les per mostrar-les per pantalla com modificar-ho per escriure’n de noves.

  • Una llista de màximes puntuacions./-8
  • Una llista de màximes puntuacions.

Amb vista a tenir una idea clara de com ha de funcionar, cal decidir quin serà el funcionament exacte de la llista de puntuacions màximes. Aquesta llista tindrà sempre 10 entrades, i inicialment, quan encara no s’ha jugat cap partida, hi haurà un seguit de puntuacions per defecte.

Elecció de tipus de fitxer i accés

Abans de començar cal escollir el nom del fitxer on mantenir les dades persistents, la seva ubicació al sistema de fitxers, i el seu tipus i com tractar-lo.

Pel que fa al nom, això ja és a gust del programador. En aquest cas, es pot anomenar “Ranquing”. Ara bé, per triar-ne la ubicació, el més assenyat normalment és que la ruta a un fitxer sigui relativa, per no imposar cap restricció sobre quines carpetes hi han d’haver en cada ordinador on es copiï el programa. En aquest cas es pot usar la pròpia carpeta de treball de l’aplicació.

Només queda decidir el tipus de fitxer. D’entrada, sempre val la pena preveure l’opció d’un fitxer de text seqüencial, ja que és el més senzill de processar i depurar. En aquest sentit, un llistat de 10 puntuacions és una informació que es pot tractar seqüencialment de manera simple, tant per mostrar-la per pantalla com per cercar on cal afegir nous elements. L’única part on no tot encaixa perfectament és per inserir puntuacions. Caldrà reescriure el fitxer des de zero. Però com és un fitxer molt petit, 10 línies, no és un gran problema i es considera assumible.

Propagació d'errors

Quan es realitzen operacions sobre fitxer, poden succeir un seguit de situacions errònies. Per exemple, intentar llegir dades d’un fitxer inexistent, llegir dades quan ja s’ha arribat al final del fitxer o d’un tipus inesperat, en tenir el fitxer un format incorrecte. En Java, aquests errors s’anomenen excepcions, i cal controlar-los usant una sentència try/catch.

En el cas d’un programa modular amb fitxers, és molt important que, cada cop que s’invoca un mètode on dins el seu codi es tracten fitxers, el codi que ha dut a terme la invocació pugui establir si tot ha anat bé o no. Per tant, els mètodes que tracten dades en fitxers han de poder avisar si han fet totes les tasques correctament o no.

La propagació d’errors és el mecanisme mitjançant el qual un mètode avisa al codi que l’ha invocat si ha pogut dur a terme la seva tasca correctament o no.

Hi ha diferents mecanismes per dur a terme la propagació d’errors. Per a aquesta classe s’ha usat el més simple, que és reservar un dels valors del paràmetre de sortida per indicar que ha succeït un error (sovint, s’usa el valor -1). Un cop s’ha avaluat el mètode invocat, cal comprovar si el resultat és el valor reservat o no. Si és així, és que hi ha hagut una excepció dins el mètode, i per tant aquest no ha pogut dur a terme la seva tasca correctament. Llavors, cal actuar en conseqüència. Per exemple, mostrant un missatge d’error per pantalla.

En el cas de mètodes que no disposen de cap paràmetre de sortida (tipus void), es força a què tinguin un paràmetre de sortida de tipus enter, que servirà exclusivament per establir si tot ha anat bé o no. Normalment, es fa que s’avaluïn a 0 si tot ha anat bé i -1 si hi ha hagut alguna excepció.

De moment, en el codi de la classe Ranquing només es pot veure la propagació d’errors pel que fa a la part de tractament d’excepcions i retorn del valor -1. Més endavant, quan es vegi com s’invoquen aquests mètodes des d’altres classes, us quedarà més clar com es controla la propagació per establir si el mètode ha funcionat correctament o no i actuar en conseqüència.

Codi font de la classe

Donades les necessitats exposades, el codi font de la classe pot ser el que es mostra tot seguit. Entre les seves particularitats, a part dels aspectes anteriors, hi ha les crides internes al mètode generarFitxerInicial, que comprova si el el fitxer de puntuacions existeix, i si no és el cas, el crea des de zero. Això és més eficient que no pas dir que hi ha un error i no poder fer-hi res. Sobretot en el cas que s’acaba de jugar una partida on s’ha obtingut una màxima puntuació, ja que aquesta es perdria! Sempre és millor corregir un error si és possible, en lloc de simplement anunciar-lo i no fer-hi res més.

També, donades les particularitats dels fitxers seqüencials, observeu en el mètode entrarPuntuacio com cal generar un fitxer temporal, ja que no es pot sobreescriure directament l’original.

  1. package joc.arena.fitxers;
  2. import java.io.File;
  3. import java.io.PrintStream;
  4. import java.util.Scanner;
  5. public class Ranquing {
  6. //Nom fitxer com a constant
  7. public static final String RANQUING = "Ranquing";
  8. /** Crea el fitxer de puntuacions inicial
  9.   * @return 0 si tot correcte, -1 si error
  10.   */
  11. public int generarFitxerInicial() {
  12. try {
  13. File ranquing = new File(RANQUING);
  14. if (ranquing.isFile() == false) {
  15. PrintStream ps = new PrintStream(ranquing);
  16. for (int i = 0; i < 10; i++) {
  17. ps.println("IOC " + (10 - i)*10);
  18. }
  19. ps.close();
  20. }
  21. return 0;
  22. } catch (Exception e) {
  23. //Propagació d'error
  24. return -1;
  25. }
  26. }
  27. /** Donada una puntuació, estableix la seva posició al fitxer
  28.   * @param punts Punts que cal comprovar
  29.   * @return Posició per ala puntuació. -1 si error.
  30.   */
  31. public int cercarRanking(int punts) {
  32. try {
  33. int err = generarFitxerInicial();
  34. if (err == -1) {
  35. return -1;
  36. }
  37. File ranquing = new File(RANQUING);
  38. Scanner lector = new Scanner(ranquing);
  39. int pos = 0;
  40. while (pos < 10) {
  41. lector.next();
  42. if (lector.hasNextInt()) {
  43. int ranqPts = lector.nextInt();
  44. if (punts > ranqPts) {
  45. return pos;
  46. }
  47. } else {
  48. //Error en el format del fitxer
  49. return -1;
  50. }
  51. pos++;
  52. }
  53. lector.close();
  54. return pos;
  55. } catch (Exception e) {
  56. return -1;
  57. }
  58. }
  59. /** Insereix una puntuació al ranquing
  60.   * @param inicials Inicials del jugador
  61.   * @param punts Puntuació assolida
  62.   * @param pos Posició dins el ranquing
  63.   * @return 0 si tot correcte, -1 si error.
  64.   */
  65. public int entrarPuntuacio(String inicials, int punts, int pos) {
  66. try {
  67. int err = generarFitxerInicial();
  68. if (err == -1) {
  69. return -1;
  70. }
  71. File ranquing = new File(RANQUING);
  72. Scanner lector = new Scanner(ranquing);
  73. File tmp = new File (RANQUING + ".tmp");
  74. PrintStream ps = new PrintStream(tmp);
  75. //Reescriure anteriors a posicio
  76. for(int i = 0; i < pos; i++) {
  77. String txt = lector.nextLine();
  78. ps.println(txt);
  79. }
  80. //Escriure nova puntuació
  81. ps.println(inicials + " " + punts);
  82. //Reescriure posteriors a posició
  83. for(int i = pos + 1; i < 10; i++) {
  84. String txt = lector.nextLine();
  85. ps.println(txt);
  86. }
  87. ps.close();
  88. lector.close();
  89. //S'esborra fitxer antic i es posa el nou
  90. ranquing.delete();
  91. tmp.renameTo(ranquing);
  92. return 0;
  93. } catch (Exception e) {
  94. return -1;
  95. }
  96. }
  97. /** Llegeix les puntuacions i les formata com una cadena de text
  98.   * @return Cadena de text resultant. null si hi ha error
  99.   */
  100. public String llegirRanquing() {
  101. try {
  102. int err = generarFitxerInicial();
  103. if (err == -1) {
  104. return null;
  105. }
  106. String txtRanquing = "Ranquing de puntuacions\n----------------------\n";
  107. File ranquing = new File(RANQUING);
  108. Scanner lector = new Scanner(ranquing);
  109. for (int i = 0; i < 10; i++) {
  110. //Llegir inicials
  111. txtRanquing = txtRanquing + lector.next();
  112. //Llegir punts
  113. if (lector.hasNextInt()) {
  114. txtRanquing = txtRanquing + "\t" + lector.nextInt() + "\n";
  115. } else {
  116. //Error en el format del fitxer
  117. return null;
  118. }
  119. }
  120. lector.close();
  121. return txtRanquing;
  122. } catch (Exception e) {
  123. return null;
  124. }
  125. }
  126. }

La classe Bestiari

La classe Bestiari és l’encarregada de generar els lluitadors, tant el jugador a l’inici del joc com els successius adversaris. A la versió original tot està emmagatzemat en el codi font del programa. Ara es vol fer una nova versió, pertanyent a un package diferent, que obtingui tota la informació des d’un fitxer. D’aquesta manera, és possible usar diferents llistes d’adversaris, o afegir-ne de nous, sense haver de modificar el codi font del programa. Només cal canviar aquest fitxer.

Elecció de tipus de fitxer i accés

En aquest cas, el nom del fitxer on tenir desades les dades dels adversaris serà “Adversaris”, i també s’usarà una ruta relativa.

Abans de poder escriure el codi font cal tenir ben clar quin serà el format del fitxer i la seva estructura. En aquest cas, us trobeu que heu d’adaptar una classe en la qual tot s’estructura dins un array originalment, i el que es vol és treballar sobre un fitxer. El tipus de fitxer que s’assembla més a un array en la seva manera de treballar, per poder accedir a les seves posicions de manera directa, és un fitxer d’accés relatiu. Per tant, s’usarà aquest sistema de tractament de dades, cosa que implica que cal usar un fitxer orientat a byte.

Amb un fitxer relatiu orientat a byte, fins i tot és possible plantejar cerques dicotòmiques directament sobre aquest.

Cal definir detalladament l’estructura del fitxer i com s’agrupen els conjunts de valors emmagatzemats (els atributs dels adversaris). El més sensat és proposar una adaptació més o menys directa de l’array usat en la versió original. Ara bé, per poder treballar amb un fitxer orientat a byte de manera relativa, com si fos un array, és important que cada conjunt de dades tingui una mida fixa (en aquest cas, cada adversari). Això vol dir que cal fitar la mida del nom. No pot ser de qualsevol llargària. Per a aquest programa, la mida es fitarà a 15 caràcters. En el cas de noms amb menys de 15 caràcters, la resta de bytes s’omplen tots a 0 fins a arribar als 15 caràcters (30 bytes).

D’aquesta manera, cada adversari ocupa sempre 50 bytes: 15*2 (nom, compost de 15 caràcters de 2 bytes) + 5*4 (els 5 atributs, que són enters de 4 bytes). La taula mostra un resum de l’estructura.

Taula Estructura de les dades associades a un adversari
Bytes 0-29 Bytes 30-33 Bytes 34-37 Bytes 38-41 Bytes 42-45 Bytes 46-49
Nom (caràcters) Nivell (enter) Punts (enter) Vida Màx (enter) Atac (enter) Defensa (enter)

Donada aquesta estructura, la figura mostraria un exemple de visualització del fitxer mitjançant un editor hexadecimal. Les dades relatives al primer adversari (de nom “Nan”) estan ombrejades. Fixeu-vos com els primers 34 bytes corresponen al nom. Els 6 primers són els caràcters del nom pròpiament i la resta són a 0. A partir del byte número 30, hi ha cinc enters, cadascun de 4 bytes, associats als seus atributs, de manera que:

  • Nivell = 0x00000001
  • Punts = 0x00000019 (25 en decimal)
  • Vida = 0x00000008
  • Atac = 0x00000003
  • Defensa = 0x00000003

Figura Contingut del fitxer d’adversaris visualitzat amb un editor hexadecimal

Codi font de la classe

Aquest cas és una mica diferent del rànquing de puntuacions, ja que no es tracta d’afegir un mòdul nou. Es tracta de reemplaçar un mòdul amb unes funcions concretes (gestionar adversaris a partir d’unes variables globals) per un amb unes altres (que aquesta gestió es faci usant un fitxer). Això és una situació molt interessant, ja que si s’apliquen els principis de modularitat correctament, les modificacions a la resta del programa haurien de ser mínimes.

  • Canviar un mòdul d'un programa hauria de ser com canviar un moble modular. Font: Bercik/-29
  • Canviar un mòdul d'un programa hauria de ser com canviar un moble modular. Font: Bercik

Aquest és, precisament, el sentit de la modularitat. Poder canviar un mòdul (en aquest cas, una classe), per un altre sense haver de tocar res més. Tal com es faria quan es canvia una calaixera modular o un mòdul de memòria de l’ordinador. Treieu l’antic, poseu el nou, i tot continua funcionant sense haver de tocar res més. Si s’assoleix això, tot s’ha fet perfectament.

L’estratègia per assolir aquesta fita sempre és la mateixa. Intentar mantenir els mateixos mètodes, amb els mateixos paràmetres, que hi havia a la classe original, i només modificar el bloc de codi que contenen. D’aquesta manera, no cal esmenar el codi on hi ha invocacions en qualsevol dels mètodes originals, ja que la seva definició serà la mateixa a la classe nova que a l’antiga. Si és necessari, es poden afegir nous mètodes, però és imprescindible mantenir el format dels que ja hi havia abans del canvi.

Per tant, fixeu-vos com, a la nova classe, els mètodes que hi ha són exactament els mateixos que hi havia inicialment, mantenint el seu format (nom i paràmetres). Tot i així, s’han creat alguns mètodes nous per tal reutilitzar codi (per exemple, crearAdversari o llegirNom).

  1. package joc.arena.fitxers;
  2. import java.io.File;
  3. import java.io.RandomAccessFile;
  4. import java.util.Random;
  5. import joc.arena.regles.Lluitador;
  6. public class Bestiari {
  7. //Constant amb el nom del fitxer d'adversaris
  8. private static final File ADVERSARIS = new File("Adversaris");
  9. //Jugador: ID = 0
  10. private int[] jugador = {0, 1, 0, 10, 10, 3, 3, 3, 3};
  11. private Lluitador lluitador = new Lluitador();
  12. //Tots els mètodes mantenen la seva declaració (nom i paràmetres)
  13. /** Genera un nou jugador
  14.   *
  15.   * @return Un array amb les dades d'un jugador inicial
  16.   */
  17. public int[] generarJugador() {
  18. lluitador.renovar(jugador);
  19. return jugador;
  20. }
  21. /** Donat un nom, genera l'adversari corresponent. Si aquest nom no existeix,
  22.   * es genera a l'atzar.
  23.   *
  24.   * @param nomAdv Nom de l'adversari a obtenir
  25.   * @return El Lluitador amb aquest nom, o null si no existeix
  26.   */
  27. public int[] cercarAdversari(String nomAdv) {
  28. try {
  29. int[] adversari = null;
  30. RandomAccessFile raf = new RandomAccessFile(ADVERSARIS, "r");
  31. long numAdv = ADVERSARIS.length()/50;
  32. for (int i = 0; i < numAdv; i++) {
  33. raf.seek(50*i);
  34. String nom = llegirNom(raf);
  35. if (nom.equalsIgnoreCase(nomAdv)) {
  36. adversari = crearAdversari(raf,i);
  37. }
  38. }
  39. raf.close();
  40. return adversari;
  41. } catch (Exception e) {
  42. return null;
  43. }
  44. }
  45. /** Obté l'adversari que hi ha en un ordre concret del fitxer,
  46.   * en fomat array.
  47.   *
  48.   * @param raf Fitxer relatiu d'on llegir-lo
  49.   * @param pos Posició de l'adversari dins del fitxer
  50.   * @return LLuitador llegit
  51.   */
  52. public int[] crearAdversari(RandomAccessFile raf, int pos) {
  53. try {
  54. int[] adversari = new int[9];
  55. raf.seek(pos*50 + 30);
  56. adversari[0] = pos + 1;
  57. adversari[1] = raf.readInt();
  58. adversari[2] = raf.readInt();
  59. adversari[3] = raf.readInt();
  60. adversari[4] = adversari[3];
  61. adversari[5] = raf.readInt();
  62. adversari[6] = adversari[5];
  63. adversari[7] = raf.readInt();
  64. adversari[8] = adversari[7];
  65. return adversari;
  66. } catch (Exception e) {
  67. return null;
  68. }
  69. }
  70. /** Donat un fitxer relatiu orientat a byte, correctament posicionat,
  71.   * llegeix el nom de l'adversari.
  72.   *
  73.   * @param raf Fitxer a tractar
  74.   * @return Nom llegit, o null si hi ha error
  75.   */
  76. public String llegirNom(RandomAccessFile raf) {
  77. try {
  78. String nom = "";
  79. char c = raf.readChar();
  80. while (c != 0x0000) {
  81. nom = nom + c;
  82. c = raf.readChar();
  83. }
  84. return nom;
  85. } catch (Exception e) {
  86. return null;
  87. }
  88. }
  89. /**Donat un nivell, genera l'adversari corresponent a l'atzar. Es tracta
  90.   * d'un adversari que sigui almenys d'aquest nivell
  91.   *
  92.   * @param nivell Nivell proper al de l'adversari a obtenir
  93.   * @return Un adversari
  94.   */
  95. public int[] triarAdversariAtzar(int nivell) {
  96. try {
  97. RandomAccessFile raf = new RandomAccessFile(ADVERSARIS,"r");
  98. Random rnd = new Random();
  99. int numAdv = (int)raf.length()/50;
  100. int[] adversari = null;
  101. boolean cercar = true;
  102. while (cercar) {
  103. int i = rnd.nextInt(numAdv);
  104. adversari = crearAdversari(raf, i);;
  105. int nivellAdv = lluitador.llegirNivell(adversari);
  106. int dif = nivell - nivellAdv;
  107. if ((dif >= -1)&&(dif <= 1)) {
  108. cercar = false;
  109. }
  110. }
  111. raf.close();
  112. return adversari;
  113. } catch (Exception e) {
  114. return null;
  115. }
  116. }
  117. /** Transforma un identificador de Lluitador al seu nom.
  118.   *
  119.   * @param id Identificador
  120.   * @return La cadena de text amb el nom.
  121.   */
  122. public String traduirIDANom(int id) {
  123. if (id == 0) {
  124. return "Aventurer";
  125. }
  126. id--;
  127. try {
  128. RandomAccessFile raf = new RandomAccessFile(ADVERSARIS, "r");
  129. int pos = 50*id;
  130. String nom = "DESCONEGUT";
  131. if (pos < raf.length()) {
  132. raf.seek(pos);
  133. nom = llegirNom(raf);
  134. }
  135. raf.close();
  136. return nom;
  137. } catch (Exception e) {
  138. return "DESCONEGUT";
  139. }
  140. }
  141. /** Diu si hi ha el fitxer d'adversaris
  142.   *
  143.   * @return Si existeix (true) o no (false)
  144.   */
  145. public boolean existeixFitxer() {
  146. return ADVERSARIS.isFile();
  147. }
  148. }

La classe auxiliar EditorBestiari

Eines de desenvolupament de jocs

És molt habitual que els programadors de videojocs tinguin eines auxiliars per generar els fitxers de dades dels jocs que estan creant. Aquestes s’anomenen eines de desenvolupament de jocs.

El problema dels fitxers orientats a byte és que no resulten fàcils d’editar. Per això, per a aquest cas, pot resultar útil disposar d’un programa auxiliar que serveixi d’editor de la llista d’adversaris, de manera que sigui fàcil manipular-la. Aquest pot visualitzar, afegir i esborrar entrades al fitxer binari.

Codi font de la classe

Tot seguit es mostra el codi font d’aquest programa auxiliar, creat monolíticament en una sola classe, però aplicant disseny descendent. El fitxer d’adversaris es pot editar partint des de zero, o bé des d’un ja existent. Per simplificar el seu codi, totes les operacions d’afegir o eliminar treballen sobre el final del fitxer del fitxer.

  1. package joc.arena.fitxers;
  2. import java.io.File;
  3. import java.io.RandomAccessFile;
  4. import java.util.Scanner;
  5. public class EditorBestiari {
  6. public static final int MAX_CHAR_NOM = 15;
  7. public static final int MIDA_BYTES_ADV = 50;
  8. public static void main (String[] args) {
  9. EditorBestiari programa = new EditorBestiari();
  10. programa.inici();
  11. }
  12. public void inici() {
  13. File fitxer = preguntarFitxer();
  14. boolean executar = true;
  15. while (executar) {
  16. executar = tractarMenu(fitxer);
  17. }
  18. }
  19. /** Pregunta a l'usuari el nom del fitxer a editar.
  20.   * @return Ruta al fitxer a editar.
  21.   */
  22. public File preguntarFitxer() {
  23. Scanner lector = new Scanner(System.in);
  24. System.out.print("Nom del fitxer a editar: ");
  25. String nom = lector.nextLine();
  26. File fitxer = new File(nom);
  27. return fitxer;
  28. }
  29. /** Fa el tractament del menú de l'usuari.
  30.   * @param fitxer Fitxer que cal tractar
  31.   * @return Si cal seguir executant del programa (true) o encara no (false)
  32.   */
  33. public boolean tractarMenu(File fitxer) {
  34. mostrarFitxer(fitxer);
  35. System.out.println("------------------------------");
  36. System.out.println("[A]fegir\t[E]liminar darrer\t[S]ortir");
  37. boolean preguntar = true;
  38. Scanner lector = new Scanner(System.in);
  39. while (preguntar) {
  40. System.out.print("Accio: ");
  41. String resposta = lector.nextLine();
  42. if ("A".equalsIgnoreCase(resposta)) {
  43. afegirAdversari(fitxer);
  44. preguntar = false;
  45. } else if ("E".equalsIgnoreCase(resposta)) {
  46. eliminarAdversari(fitxer);
  47. preguntar = false;
  48. } else if ("S".equalsIgnoreCase(resposta)) {
  49. return false;
  50. } else {
  51. System.out.print("Accio incorrecta...");
  52. }
  53. }
  54. return true;
  55. }
  56. /** Mostra el contingut del fitxer per pantalla
  57.   * @param fitxer Ruta al fitxer a mostrar
  58.   */
  59. public void mostrarFitxer(File fitxer) {
  60. try {
  61. if (fitxer.isFile() == false) {
  62. System.out.println("Encara no s'ha creat el fitxer.");
  63. } else {
  64. RandomAccessFile raf = new RandomAccessFile(fitxer, "r");
  65. long numAdversaris = fitxer.length()/MIDA_BYTES_ADV;
  66. if (numAdversaris == 0) {
  67. System.out.println("El fitxer és buit.");
  68. } else {
  69. for (int i = 0; i <numAdversaris; i++) {
  70. String nom = llegirNom(raf);
  71. System.out.print(nom);
  72. for (int z = 0; z < (MAX_CHAR_NOM - nom.length()); z++) {
  73. System.out.print(" ");
  74. }
  75. //Cada adversari coupa 50 bytes. El tros del nom n'ocupa 15*2
  76. raf.seek(i*MIDA_BYTES_ADV + MAX_CHAR_NOM*2);
  77. System.out.print(": \tNivell: " + raf.readInt());
  78. System.out.print(" (punts: " + raf.readInt() + ")");
  79. System.out.print("\tVIDA: " + raf.readInt());
  80. System.out.print("\tATAC: " + raf.readInt());
  81. System.out.println("\tDEFENSA: " + raf.readInt());
  82. }
  83. }
  84. raf.close();
  85. }
  86. } catch (Exception e) {
  87. System.out.println("Error accedint al fitxer!");
  88. }
  89. }
  90. /** Donat un fitxer relatiu orientat a byte, correctament posicionat,
  91.   * llegeix el nom de l'adversari.
  92.   * @param raf Fitxer a tractar
  93.   * @return Nom llegit, o null si hi ha error
  94.   */
  95. public String llegirNom(RandomAccessFile raf) {
  96. try {
  97. String nom = "";
  98. char c = raf.readChar();
  99. while (c != 0x0000) {
  100. nom = nom + c;
  101. c = raf.readChar();
  102. }
  103. return nom;
  104. } catch (Exception e) {
  105. return null;
  106. }
  107. }
  108. /** Escriu un nou adversari al final del fitxer, preguntant tot el que calgui
  109.   * a l'usuari.
  110.   * @param fitxer Ruta al fitxer on cal afegir l'adversari.
  111.   */
  112. public void afegirAdversari(File fitxer) {
  113. try {
  114. RandomAccessFile raf = new RandomAccessFile(fitxer, "rw");
  115. //Es posa al final de tot
  116. raf.seek(fitxer.length());
  117. System.out.println("Escriu el nom de l'adversari (màx. 12 lletres): ");
  118. Scanner lector = new Scanner(System.in);
  119. String nom = lector.nextLine();
  120. //Escriure el nom (màx. 15 caràcters)
  121. int err = escriureNom(nom, raf);
  122. if (err == -1) {
  123. System.out.println("Error escrivint dades al fitxer " + fitxer);
  124. } else {
  125. //Escriure valors - Nivell:XP:PV:Max PV:Atac:Max Atac:max Defensa
  126. System.out.print("Escriu els seus atributs, separats per espais. ");
  127. System.out.println ("(5 enters = Nivell Punts PV Atac Defensa):");
  128. int[] valors = llegirValors(lector);
  129. for (int i = 0; i < valors.length; i++) {
  130. raf.writeInt(valors[i]);
  131. }
  132. }
  133. raf.close();
  134. } catch (Exception e) {
  135. System.out.println("Error escrivint dades al fitxer " + fitxer);
  136. }
  137. }
  138. /** Llegeix cinc valors de tipus enter des del teclat.
  139.   * @param lector Scanner que llegeix del teclat
  140.   * @return Els cinc valors llegits, ordenadament
  141.   */
  142. public int[] llegirValors(Scanner lector) {
  143. int[] valors = new int[5];
  144. boolean preguntar = true;
  145. while(preguntar) {
  146. int numLlegits = 0;
  147. for (int i = 0; i < valors.length; i++) {
  148. if (lector.hasNextInt()) {
  149. valors[i] = lector.nextInt();
  150. numLlegits++;
  151. } else {
  152. lector.next();
  153. }
  154. }
  155. if (numLlegits == 5) {
  156. preguntar = false;
  157. } else {
  158. System.out.println("Els 5 valors no han estat correctes.");
  159. }
  160. }
  161. lector.nextLine();
  162. return valors;
  163. }
  164. /** Donat un nom en forma de cadena de text, l'escriu a un fitxer orientat a
  165.   * byte. Com a màxim el nom pot tenir 15 caràcters. La resta, s'omple a 0.
  166.   * @param nom Nom a escriure
  167.   * @param raf Fitxer relatiu correctament posicionat per a l'escriptura
  168.   * @return Si ha funcionat (0) o no (-1)
  169.   */
  170. public int escriureNom(String nom, RandomAccessFile raf) {
  171. try {
  172. int numChars = nom.length();
  173. if (numChars > MAX_CHAR_NOM) {
  174. numChars = MAX_CHAR_NOM;
  175. }
  176. for (int i = 0; i < numChars; i++) {
  177. raf.writeChar(nom.charAt(i));
  178. }
  179. char blank = 0x0000;
  180. for (int i = 0; i < (MAX_CHAR_NOM - numChars); i++) {
  181. raf.writeChar(blank);
  182. }
  183. return 0;
  184. } catch (Exception e) {
  185. return -1;
  186. }
  187. }
  188. /** Elimina el darrer adversari en un fitxer.
  189.   * @param fitxer Ruta del fitxer a modificar
  190.   */
  191. public void eliminarAdversari(File fitxer) {
  192. try {
  193. RandomAccessFile raf = new RandomAccessFile(fitxer,"rw");
  194. long novaMida = fitxer.length() - MIDA_BYTES_ADV;
  195. //Eliminar el darrer adversari és esborrar els darrers 50 bytes...
  196. if (novaMida >= 0) {
  197. raf.setLength(fitxer.length() - MIDA_BYTES_ADV);
  198. }
  199. raf.close();
  200. } catch (Exception e) {
  201. System.out.println("Error esborrant dades al fitxer " + fitxer);
  202. }
  203. }
  204. }

Esmenes als mòduls originals

Un cop s’han generat els nous mòduls que amplien el programa original de manera que algunes de les seves tasques depenguin de la lectura o escriptura de fitxers, cal modificar el codi original per tal que s’usin els nous mòduls. Una correcta aplicació de la modularitat hauria de minimitzar el nombre de canvis al codi i fer que aquests estiguin molt ben localitzats. De fet, el cas ideal seria que només cal afegir nou codi, però no modificar, o fins i tot eliminar, codi ja existent.

A la classe EntradaTeclat

El mètode ''trim'' elimina els espais a dreta i esquerra d'un text.

La introducció d’una màxima puntuació al rànquing implica poder demanar a l’usuari, si es compleixen les condicions necessàries, que introdueixi les seves inicials. Si es vol seguir estrictament el plantejament modular de l’aplicació, això vol dir que cal un nou mètode a la classe EntradaTeclat. Aquest ha de comprovar que, efectivament, es tracta d’unes inicials (en aquest cas, es considera que tres lletres).

Noteu que el canvi de la classe Bestiari no afecta en absolut aquesta classe, tot i que la fa servir. Això vol dir que s’ha aplicat correctament la modularitat.

  1. //Ara cal importar la nova versió
  2. import joc.arena.fitxers.Bestiari;
  3. //Nou mètode
  4. /** Pregunta les inicials del jugador si assoleix una màxima puntuació.
  5.   * @return Inicials (cadena de text amb 3 caràcters)
  6.   */
  7. public String preguntarInicials() {
  8. Scanner lector = new Scanner(System.in);
  9. System.out.println("has assolit una màxima puntuació!!");
  10. String inicials = "";
  11. boolean llegir = true;
  12. while (llegir) {
  13. System.out.print("Escriu les teves inicials: ");
  14. inicials = lector.nextLine();
  15. //Aquest mètode elimina espais laterals
  16. inicials = inicials.trim();
  17. if (inicials.length() == 3) {
  18. llegir = false;
  19. } else {
  20. System.out.print("Entrada incorrecta. ");
  21. }
  22. }
  23. return inicials.toUpperCase(); //Sempre es posaran en majúscula
  24. }

A la classe SortidaPantalla

Un altre nou aspecte del programa és que cal mostrar el rànquing de puntuacions en acabar el joc. Donat el plantejament de la classe Ranquing, aquesta tasca és molt simple i es podria dur a terme directament a JocArena. Tot i així, per ser coherents amb el fet que els mètodes que mostren diverses línies de text per pantalla, com l’estat del lluitador, són a aquesta classe, se serà estricte en l’aplicació de la modularitat tal com s’ha plantejat a l’aplicació originalment.

Com abans, noteu que el canvi de la classe Bestiari no afecta en absolut a aquesta classe, tot i que la fa servir. També observeu ara com es comprova la propagació d’errors des del mètode llegirRanquing, mirant si s’ha avaluat null o no.

  1. //Ara cal importar la nova versió
  2. import joc.arena.fitxers.Bestiari;
  3. //Nou mètode
  4. /** Mostra per pantalla la llista de puntuacions
  5.   */
  6. public void mostrarRanking() {
  7. Ranquing rnk = new Ranquing();
  8. String s = rnk.llegirRanquing();
  9. if (s == null) {
  10. System.out.println("Hi ha un error en els fitxers de puntuacions!");
  11. } else {
  12. System.out.println(s);
  13. }
  14. }

A la classe JocArena

A l’apartat d’annexos dels materials disposeu del codi font complet amb la solució final d’aquest exemple.

En aquesta classe, només cal modificar el mètode inici per tal que, el programa prevegi les noves funcionalitats. D’una banda, ara cal garantir que s’inicialitzen els valors dels adversaris a partir d’un fitxer. D’altra banda, en acabar la partida, cal que es comprovi si pertoca afegir una nova màxima puntuació al fitxer i realitzar les operacions corresponents al teclat i la pantalla (entrar inicials i mostrar rànquing actual).

Tot i que s’ha intentat minimitzar el nombre de canvis necessaris a causa de la nova classe Bestiari, cal alguna modificació, ja que almenys cal controlar si el fitxer d’adversaris existeix abans de fer res. A part d’això, el canvi de classe no afecta en res més.

  1. //Ara cal importar la nova versió
  2. import joc.arena.fitxers.Bestiari;
  3. public void inici() {
  4. //Mirar si hi ha fitxer d'adversaris
  5. if (bestiari.existeixFitxer() == false) {
  6. System.out.println("No hi ha el fitxer d'adversaris!");
  7. } else {
  8. //Codi original
  9. sortida.mostarBenvinguda();
  10. int[] jugador = bestiari.generarJugador();
  11. int numCombat = 0;
  12. boolean jugar = true;
  13. while (jugar) {
  14. numCombat++;
  15. //Abans de cada combat es restaura el jugador
  16. lluitador.restaurar(jugador);
  17. //Inici d'un combat
  18. System.out.println("*** COMBAT " + numCombat);
  19. System.out.print("Estat actual del jugador: ");
  20. sortida.mostrarLluitador(jugador);
  21. System.out.println("**************************");
  22. //S'obté l'adversari
  23. int[] adversari = entrada.triarAdversari(lluitador.llegirNivell(jugador));
  24. //Combat
  25. combatre(jugador, adversari);
  26. //Fi
  27. jugar = fiCombat(jugador, adversari);
  28. if (numCombat == MAX_COMBAT) {
  29. System.out.println("Has sobreviscut a tots els combats. Enhorabona!!");
  30. }
  31. }
  32. System.out.print("Estat final del jugador: ");
  33. sortida.mostrarLluitador(jugador);
  34. //Nou: comprovar si entra al rànquing (10 posicions)
  35. Ranquing rnk = new Ranquing();
  36. int punts = lluitador.llegirPunts(jugador);
  37. int pos = rnk.cercarRanking(punts);
  38. if (pos != -1) {
  39. if (pos < 10) {
  40. String inicials = entrada.preguntarInicials();
  41. err = rnk.entrarPuntuacio(inicials, punts, pos);
  42. if (err == -1) {
  43. System.out.println("Error accedint al fitxer de puntuacions.");
  44. }
  45. }
  46. sortida.mostrarRanking();
  47. } else {
  48. System.out.println("Error accedint al fitxer de puntuacions.");
  49. }
  50. }
  51. }
Anar a la pàgina anterior:
Exercicis d'autoavaluació
Anar a la pàgina següent:
Activitats