Videojuego por turnos (POO) y copia de ficheros binarios
Esta prΓ‘ctica implementa un mini videojuego por turnos en Java, con varios tipos de personajes que se atacan hasta que uno queda a 0 de vida. El objetivo real (mΓ‘s allΓ‘ de matar orcos) es practicar: clases abstractas, herencia, polimorfismo, encapsulaciΓ³n, composiciΓ³n y un bucle de juego sencillo.
Las clases importantes del paquete videojuego son:
La idea es que Personaje define lo comΓΊn de todos los personajes
(vida, ataque base, pociones, mΓ©todos como atacar, defender, etc.),
y las subclases (Guerrero, Mago, Arquero, Enemigo)
aΓ±aden comportamiento especΓfico.
Personaje
La clase Personaje es abstracta, lo que significa que:
new Personaje(...) estΓ‘ prohibido).getTipo(), habilidadEspecial(...), actuar(...)).JAVA// Clase base de todos los personajes public abstract class Personaje { // Atributos privados (encapsulaciΓ³n) private String nombre; private int vida; private int vidamax; private int ataquebase; private int pociones; private boolean defendiendo; private java.util.List<Pocion> inventarioPociones; // Constructor: se llama desde las clases hijas con super(...) public Personaje(String nombre, int vidamax, int ataquebase) { this.nombre = nombre; this.vida = vidamax; this.vidamax = vidamax; this.ataquebase = ataquebase; this.pociones = 3; // por ejemplo this.inventarioPociones = new java.util.ArrayList<>(); } // MΓ©todo que comprueba si el personaje sigue vivo public boolean estaVivo() { return this.vida > 0; } // MΓ©todo de ataque genΓ©rico con posible crΓtico public void atacar(Personaje objetivo) { java.util.Random azar = new java.util.Random(); int danioFinal = ataquebase; // 20% de probabilidad de crΓtico if (azar.nextInt(100) < 20) { danioFinal *= 2; System.out.println(nombre + " hace un GOLPE CRΓTICO de " + danioFinal); } // Si el objetivo se defiende, reduce el daΓ±o if (objetivo.getDefendiendo()) { danioFinal /= 2; } objetivo.setVida(Math.max(objetivo.getVida() - danioFinal, 0)); } // MΓ©todos abstractos que las hijas deben implementar public abstract String getTipo(); public abstract void habilidadEspecial(Personaje objetivo); public abstract void actuar(Personaje objetivo); }
private y se accede por getters/setters.
Esto protege el estado del objeto y evita que el resto del programa lo toque directamente a lo loco.
Personaje, pero en tiempo de
ejecuciΓ³n pueden ser Guerrero, Mago, Enemigo, etc. Cada uno sobrescribe sus mΓ©todos.
Cada clase hija extiende Personaje y aΓ±ade sus propios atributos y comportamiento.
Por ejemplo, Guerrero tiene estamina, Mago tiene mana,
Arquero tiene flechas, etc.
JAVA// Ejemplo simplificado de Guerrero public class Guerrero extends Personaje { private int estamina; public Guerrero(String nombre, int vidamax, int ataquebase, int estamina) { super(nombre, vidamax, ataquebase); this.estamina = estamina; } @Override public String getTipo() { return "Guerrero"; } @Override public void habilidadEspecial(Personaje objetivo) { // Ejemplo: gastar estamina para hacer mΓ‘s daΓ±o if (estamina >= 10) { estamina -= 10; System.out.println(getNombre() + " usa GOLPE BRUTAL sobre " + objetivo.getNombre()); objetivo.setVida(Math.max(objetivo.getVida() - 30, 0)); } else { System.out.println(getNombre() + " estΓ‘ cansado, usa ataque normal."); atacar(objetivo); } } @Override public void actuar(Personaje objetivo) { // Decide si usar habilidad especial o ataque normal if (estamina >= 10) habilidadEspecial(objetivo); else atacar(objetivo); } }
Mago
Tiene atributo mana y habilidades mΓ‘gicas. Suele gastar mana para lanzar
hechizos mΓ‘s potentes, por ejemplo curarse o hacer bastante daΓ±o.
Arquero
Tiene atributo flechas. Puede hacer ataques a distancia mΓ‘s fuertes mientras tenga flechas.
Cuando se queda sin flechas, se ve obligado a atacar de forma mΓ‘s bΓ‘sica.
personaje.habilidadEspecial(objetivo).
Eso es polimorfismo en acciΓ³n.
Enemigo: IA simple basada en la vida
La clase Enemigo tambiΓ©n extiende Personaje, pero su mΓ©todo actuar
no se decide por un jugador humano, sino por una lΓ³gica interna sencilla:
JAVA// Parte del mΓ©todo actuar en Enemigo public void actuar(Personaje objetivo) { double porcentajeVida = (double) this.getVida() / this.getVidamax(); // Si la vida baja del 30%, se defiende if (porcentajeVida < 0.3) { this.defender(); System.out.println(getNombre() + " estΓ‘ herido y se pone en modo defensivo."); } else { // Si no, ataca normalmente this.atacar(objetivo); } }
Con esto se consigue que el enemigo tenga un comportamiento mΓnimamente inteligente sin necesidad de un sistema de IA complejo.
Pocion y el inventario del personaje
La clase Pocion es muy sencilla, pero muestra un concepto clave: composiciΓ³n.
Un Personaje tiene una lista de Pocion, es decir, estΓ‘ formado tambiΓ©n por esos objetos.
JAVA// Clase Pocion public class Pocion { private int curacion = 20; public void usar(Personaje objetivo) { int vidaActual = objetivo.getVida(); int vidaMax = objetivo.getVidamax(); objetivo.setVida(Math.min(vidaActual + curacion, vidaMax)); } }
En Personaje hay algo como:
JAVA// Uso del inventario de pociones public void usarPocion() { if (!inventarioPociones.isEmpty()) { Pocion p = inventarioPociones.remove(0); // coge la primera pociΓ³n p.usar(this); // se aplica sobre este personaje } else { System.out.println(nombre + " no tiene pociones."); } }
usar de Pocion recibe un
Personaje como parΓ‘metro. Esto permite usar la misma clase Pocion para
cualquier tipo de personaje (Guerrero, Mago, Enemigo...) sin duplicar cΓ³digo.
Combate y el flujo de ejecuciΓ³n
La clase Combate orquesta el intercambio de golpes entre dos personajes:
controla los turnos hasta que uno muere.
JAVA// Bucle principal de combate public class Combate { private Personaje p1, p2; private int turnos = 1; public Combate(Personaje p1, Personaje p2) { this.p1 = p1; this.p2 = p2; } public void iniciar() { while (p1.estaVivo() && p2.estaVivo()) { System.out.println("\n--- TURNO " + turnos + " ---"); p1.mostrarEstado(); p2.mostrarEstado(); p1.actuar(p2); if (!p2.estaVivo()) break; p2.actuar(p1); p1.finturno(); p2.finturno(); turnos++; } System.out.println("\nGANADOR: " + (p1.estaVivo() ? p1.getNombre() : p2.getNombre())); } }
while se ve el polimorfismo claramente:
se llama a actuar sobre Personaje, pero el cΓ³digo ejecutado dependerΓ‘
de si el objeto real es Guerrero, Mago, Enemigo, etc.
Main: punto de entradaJAVA public class Main { public static void main(String[] args) { Personaje p1 = new Guerrero("MΓ‘ximo", 100, 15, 50); Personaje p2 = new Enemigo("Orco Carnicero", 120, 12); Combate duelo = new Combate(p1, p2); duelo.iniciar(); } }
AquΓ se ve la βfoto finalβ: se crean los objetos concretos, se construye el combate con ellos y
se llama a iniciar() para ejecutar el bucle.
Esta prΓ‘ctica trabaja con ficheros binarios usando streams de bytes. AdemΓ‘s de copiar un archivo origen a un destino, tambiΓ©n:
Al inicio, el programa pide al usuario las rutas de origen y destino:
JAVA public class CopiaFicherosBinarios { public static void main(String[] args) { java.util.Scanner sc = new java.util.Scanner(System.in); try { System.out.println("Indica la ruta del archivo ORIGEN: "); String rutaOrigen = sc.nextLine().trim(); System.out.println("Indica la ruta del archivo DESTINO: "); String rutaDestino = sc.nextLine().trim(); java.io.File origen = new java.io.File(rutaOrigen); java.io.File destino = new java.io.File(rutaDestino); // Comprobaciones bΓ‘sicas if (!origen.exists()) { System.out.println("El archivo origen no existe."); return; } if (!origen.isFile()) { System.out.println("El origen no es un fichero vΓ‘lido."); return; } // (Opcional) comprobar extensiΓ³n .jpg, .bin, etc. // AquΓ irΓa la llamada al mΓ©todo de copia... } catch (java.io.IOException e) { System.out.println("Error de E/S: " + e.getMessage()); } } }
isFile()) antes de intentar abrirlo con streams. Si no, revientas a excepciones.
FileInputStream y FileOutputStreamLa copia real se hace leyendo bloques de bytes de un stream de entrada y escribiΓ©ndolos en el de salida. Usar un buffer mejora el rendimiento frente a leer byte a byte.
JAVA private static void copiarFichero(java.io.File origen, java.io.File destino) throws java.io.IOException { try (java.io.FileInputStream fis = new java.io.FileInputStream(origen); java.io.FileOutputStream fos = new java.io.FileOutputStream(destino)) { byte[] buffer = new byte[4096]; // 4 KB por lectura int bytesLeidos; while ((bytesLeidos = fis.read(buffer)) != -1) { fos.write(buffer, 0, bytesLeidos); } } }
FileInputStream para el origen.FileOutputStream para el destino.byte[]) dentro de un bucle while.DespuΓ©s de copiar, se suele comprobar si ambos ficheros son iguales comparando byte a byte:
JAVA private static boolean sonFicherosIguales(java.io.File f1, java.io.File f2) throws java.io.IOException { if (f1.length() != f2.length()) { return false; // tamaΓ±os distintos β ya no son iguales } try (java.io.FileInputStream in1 = new java.io.FileInputStream(f1); java.io.FileInputStream in2 = new java.io.FileInputStream(f2)) { int b1, b2; do { b1 = in1.read(); b2 = in2.read(); if (b1 != b2) return false; } while (b1 != -1); } return true; }
De este modo se asegura que no solo se ha copiado βalgoβ, sino que el contenido es exactamente igual.
La prΓ‘ctica tambiΓ©n escribe un log en texto usando BufferedWriter y
FileWriter, aΓ±adiendo fecha y estadΓsticas de la copia.
JAVA private static void escribirInforme( java.io.File origen, java.io.File destino, boolean sonIguales, String textoAnalisis) throws java.io.IOException { String salto = System.lineSeparator(); java.time.LocalDateTime ahora = java.time.LocalDateTime.now(); java.time.format.DateTimeFormatter formato = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String marcaTiempo = ahora.format(formato); try (java.io.BufferedWriter bw = new java.io.BufferedWriter( new java.io.FileWriter("informe_copia.txt", true))) { bw.write("========== COPIA DE FICHEROS ==========" + salto); bw.write("Fecha y hora: " + marcaTiempo + salto); bw.write("Origen: " + origen.getAbsolutePath() + salto); bw.write("Destino: " + destino.getAbsolutePath() + salto); bw.write("ΒΏFicheros idΓ©nticos?: " + sonIguales + salto); bw.write("EstadΓsticas:" + salto); bw.write(textoAnalisis + salto + salto); } }
LocalDateTime, DateTimeFormatter,
BufferedWriter con modo append, y composiciΓ³n de cadenas con saltos de lΓnea.
Las dos prΓ‘cticas no son aleatorias: estΓ‘n elegidas para cubrir bloques tΓpicos de la asignatura de ProgramaciΓ³n (y de la vida real, para variar). AquΓ tienes una visiΓ³n comparativa.
| Concepto | PrΓ‘ctica videojuego | PrΓ‘ctica ficheros binarios |
|---|---|---|
Clases y objetos |
Personaje, Guerrero, Mago, Combate, etc. |
CopiaFicherosBinarios, uso de File, Scanner, streams. |
| Herencia | Guerrero, Mago, Arquero, Enemigo extienden Personaje. |
No hay herencia relevante, es mΓ‘s procedural. |
| Polimorfismo | Llamadas a actuar() y habilidadEspecial() sobre referencias Personaje. |
No se usa, pero sΓ se practica sobrecarga de responsabilidades en funciones. |
| ComposiciΓ³n | Personaje tiene un List<Pocion>. |
La clase usa varios objetos: File, streams, BufferedWriter. |
| Manejo de errores | Validar vida β₯ 0, controlar estados como defendiendo, etc. | try/catch de IOException, validaciΓ³n de rutas y existencia de ficheros. |
| E/S | Principalmente consola (mensajes de combate). | Lectura y escritura binaria + log de texto. |
toString).
Combate controla turnos, Personaje su estado, Pocion la curaciΓ³n, etc.
IOException,
controlar estados lΓmite (vida 0, sin pociones, sin flechas...).