Ir al contenido
wannacode

Epic Cosmos 1: los objetos en el espacio

En resumen: Un juego con objetos: estrellas, planetas y satélites son clases, la regla de «qué orbita alrededor de qué» es un contrato de clase abstracta, y una órbita no válida lanza tu propia excepción.

Problema

La carrera espacial

La humanidad ya puede volar a planetas lejanos. Empresas y países compiten por sus recursos. Tú diriges una de las misiones y quieres convertirte en la mayor potencia del espacio.

Las reglas de Epic Cosmos:

  • Ganas puntos al descubrir nuevos planetas y objetos, extraer recursos, construir edificios y naves, y hacer descubrimientos científicos.
  • Los objetos son estrellas, planetas y satélites. Una estrella da energía. Algunos planetas tienen recursos raros, y su atmósfera decide qué se puede construir allí. Un satélite sirve para las comunicaciones y la exploración.
  • Necesitas recursos para construir, investigar y mantener en marcha tus bases y naves.
  • Los edificios extraen recursos, investigan tecnologías, defienden la base y fabrican naves.
  • Las tecnologías desbloquean nuevos edificios, naves y habilidades.
  • Conflictos: en el espacio no estás solo. Hay piratas y otros jugadores.
  • El final: el juego termina cuando alguien alcanza un número fijo de puntos o después de un número fijo de turnos. Gana el jugador con más puntos.

Antes de los edificios y las naves necesitas el mundo mismo: qué objetos hay, dónde están y qué puede orbitar alrededor de qué. De eso trata esta lección. Los recursos, los edificios y las naves están en la siguiente.

La idea

Todo en el espacio es un SpaceObject

Una estrella, un planeta y un satélite tienen algo en común: un nombre, las coordenadas x, y, z y una masa. Lo común va a una clase abstracta:

abstract class SpaceObject {
private String name;
private double coordinateX;
private double coordinateY;
private double coordinateZ;
private double mass;
public SpaceObject(String name, double coordinateX, double coordinateY, double coordinateZ, double mass) {
this.name = name;
this.coordinateX = coordinateX;
this.coordinateY = coordinateY;
this.coordinateZ = coordinateZ;
this.mass = mass;
}
public String getName() {
return name;
}
public double getCoordinateX() {
return coordinateX;
}
public double getCoordinateY() {
return coordinateY;
}
public double getCoordinateZ() {
return coordinateZ;
}
public double getMass() {
return mass;
}
public abstract String getKind();
public void displayInfo() {
System.out.printf("%s %s: (%.1f, %.1f, %.1f), masa %.2f%n", getKind(), name, coordinateX, coordinateY, coordinateZ, mass);
}
protected void moveTo(double x, double y, double z) {
coordinateX = x;
coordinateY = y;
coordinateZ = z;
}
}
  • Las coordenadas están en millones de kilómetros desde el Sol, y la masa está en masas terrestres: la Tierra es 1, el Sol es 333 000.
  • display_info() es común a todos. El tipo (get_kind()) es abstracto: cada subclase dice por sí misma qué es.
  • _move_to empieza con un guion bajo: es la convención para «solo para la clase y sus subclases». Python no impide las llamadas desde fuera, pero el guion bajo dice «no lo toques». Lo mismo vale para los campos self._name y los demás, que lees desde fuera mediante @property.

La clase abstracta extiende ABC, y el método abstracto lleva @abstractmethod encima y pass en el cuerpo. Python no te deja crear un objeto de una clase que todavía tiene un método abstracto sin cuerpo:

class Comet(SpaceObject):
pass
try:
comet = Comet("Halley", 0, 0, 0, 0)
except TypeError:
print("TypeError: Comet no tiene get_kind y no puede convertirse en objeto")
Salida
TypeError: Comet no tiene get_kind y no puede convertirse en objeto

La estrella es la subclase más simple:

class Star extends OrbitableSpaceObject {
public Star(String name, double coordinateX, double coordinateY, double coordinateZ, double mass) {
super(name, coordinateX, coordinateY, coordinateZ, mass);
}
@Override
public String getKind() {
return "Estrella";
}
}

El constructor se hereda: Star(...) llama al constructor del padre con los mismos argumentos. Por eso Star solo escribe su tipo.

Los tres tipos de objetos van en una lista de SpaceObject. El bucle no sabe cuál es cuál, y cada uno dice su propio tipo:

SpaceObject[] objects = {
new Star("Sol", 0, 0, 0, 333000),
new Planet("Tierra", 149.6, 0, 0, 1),
new Satellite("Luna", 150, 0, 0, 0.012),
};
for (SpaceObject object : objects) {
object.displayInfo();
}
Salida
Estrella Sol: (0.0, 0.0, 0.0), masa 333000.00
Planeta Tierra: (149.6, 0.0, 0.0), masa 1.00
Satélite Luna: (150.0, 0.0, 0.0), masa 0.01

La idea

Objetos alrededor de los que se orbita

Los planetas orbitan alrededor de una estrella, y los satélites orbitan alrededor de un planeta. Tanto la estrella como el planeta tienen que recordar qué orbita a su alrededor. Eso es OrbitableSpaceObject: una clase abstracta entre SpaceObject y ellos.

abstract class OrbitableSpaceObject extends SpaceObject {
private SpaceObject[] orbiters = new SpaceObject[10];
private int orbiterCount = 0;
public OrbitableSpaceObject(String name, double coordinateX, double coordinateY, double coordinateZ, double mass) {
super(name, coordinateX, coordinateY, coordinateZ, mass);
}
public void addOrbiter(SpaceObject orbiter) {
if (orbiterCount == orbiters.length) {
throw new IllegalStateException("Alrededor de " + getName() + " ya orbitan " + orbiters.length + " objetos");
}
orbiters[orbiterCount] = orbiter;
orbiterCount++;
}
public SpaceObject[] getOrbiters() {
SpaceObject[] result = new SpaceObject[orbiterCount];
for (int i = 0; i < orbiterCount; i++) {
result[i] = orbiters[i];
}
return result;
}
}
  • Los objetos que orbitan están en una lista que crece sola. No necesitas un contador: len(self._orbiters) dice cuántos hay.
  • Una estrella tiene lugar para 10 objetos: CAPACITY está en la clase, compartida por todos sus objetos. Si el lugar se llena, add_orbiter lanza RuntimeError: la excepción integrada para cuando una acción no puede ocurrir en este momento.
  • get_orbiters() devuelve una copia de la lista.
Más precisamente, para curiosos
  • get_orbiters() devuelve una copia, no la lista misma. Si no, el código de fuera podría añadir con append y saltarse la comprobación de lugar.
  • Star extiende OrbitableSpaceObject, no SpaceObject directamente. Así la estrella recibe tanto los objetos que orbitan como todo lo de SpaceObject.

La copia se ve en la salida: lo que añades a ella no llega a la estrella.

sun = Star("Sol", 0, 0, 0, 333000)
Planet("Tierra", 149.6, 0, 0, 1).orbit(sun)
copy = sun.get_orbiters()
copy.append(Planet("Marte", 227.9, 0, 0, 0.11))
print(len(copy), len(sun.get_orbiters()))
Salida
Tierra orbita alrededor de Sol
2 1

La idea

La regla de las órbitas

Un planeta orbita alrededor de una estrella. Un satélite orbita alrededor de un planeta. Todo lo demás no es válido. Los objetos que pueden orbitar comparten un contrato:

interface Orbiter {
void orbit(SpaceObject target) throws NotValidSpaceObjectException;
}

Python no tiene una palabra clave aparte para interface. El contrato es una clase abstracta con solo métodos abstractos, y la clase que lo cumple la extiende junto a su padre: class Planet(OrbitableSpaceObject, Orbiter). Python te deja extender varias clases a la vez.

Si se rompe la regla, orbit lanza una excepción propia. El nombre dice exactamente qué pasó:

class NotValidSpaceObjectException extends Exception {
public NotValidSpaceObjectException(String message) {
super(message);
}
}

Así hace cumplir la regla el planeta:

@Override
public void orbit(SpaceObject target) throws NotValidSpaceObjectException {
if (target instanceof Star star) {
star.addOrbiter(this);
System.out.println(getName() + " orbita alrededor de " + star.getName());
} else {
throw new NotValidSpaceObjectException(getName() + " no puede orbitar alrededor de " + target.getName() + ": un planeta orbita solo una estrella");
}
}
  • isinstance(target, Star) comprueba el tipo. Después de la comprobación usas target como estrella: no necesitas una variable nueva.
  • Si la comprobación falla, la excepción detiene el método. El planeta no se añade a ningún lado.
  • Satellite tiene el mismo orbit, solo que comprueba Planet.
Más precisamente, para curiosos

En Python nadie sigue qué lanza qué. Ni el contrato ni orbit dicen que puede lanzar una excepción, y nada obliga a quien llama a capturarla. El programa se detendrá si nadie la captura, así que en el juego siempre la capturamos. El nombre de la excepción termina en Error, como las integradas (ValueError).

Míralo

Qué orbita alrededor de qué

sun = Star("Sol", 0, 0, 0, 333000)
earth = Planet("Tierra", 149.6, 0, 0, 1)
moon = Satellite("Luna", 150, 0, 0, 0.012)
try_orbit(earth, sun)
try_orbit(moon, earth)
try_orbit(moon, sun)
Paso a paso, en palabras
  1. Tres objetos: una estrella, un planeta y un satélite, cada uno con sus coordenadas y su masa.
  2. Un planeta alrededor de una estrella: la regla lo permite, y la Tierra se pone en órbita.
  3. Un satélite alrededor de un planeta: permitido. La Luna orbita alrededor de la Tierra y se mueve con ella.
  4. Un satélite alrededor de una estrella: orbit lanza NotValidSpaceObjectError, y el except de try_orbit imprime el motivo.

La Tierra pasa la comprobación de la estrella y se añade a los objetos que orbitan a su alrededor. La Luna pasa la comprobación de la Tierra. Cuando la Luna intenta orbitar alrededor del Sol, la comprobación de Planet falla. orbit lanza la excepción, y la Luna sigue alrededor de la Tierra.

La excepción se captura en una pequeña función auxiliar. Esta prueba la órbita y, si la rechazan, dice por qué:

static void tryOrbit(Orbiter orbiter, SpaceObject target) {
try {
orbiter.orbit(target);
} catch (NotValidSpaceObjectException e) {
System.out.println("Rechazado: " + e.getMessage());
}
}

Pruébalo

¿Cuántos orbitan?

Marte intenta orbitar alrededor de la Tierra.

¿Qué imprimirá el programa?

def try_orbit(orbiter, target):
try:
orbiter.orbit(target)
except NotValidSpaceObjectError as e:
print(f"Rechazado: {e}")
sun = Star("Sol", 0, 0, 0, 333000)
earth = Planet("Tierra", 149.6, 0, 0, 1)
mars = Planet("Marte", 227.9, 0, 0, 0.11)
moon = Satellite("Luna", 150, 0, 0, 0.012)
try_orbit(earth, sun)
try_orbit(mars, earth)
try_orbit(moon, earth)
print(f"Alrededor de Sol: {len(sun.get_orbiters())}")
print(f"Alrededor de Tierra: {len(earth.get_orbiters())}")
Respuesta

La idea

Una nave que vuela

Las naves no orbitan. Vuelan de un objeto a otro. Eso es un contrato distinto:

interface Fly {
void flyTo(SpaceObject destination) throws NotValidSpaceObjectException;
}

SpaceShip extiende SpaceObject y cumple Fly. No puede volar a una estrella. Vuela a cualquier otra cosa y toma las coordenadas del destino:

@Override
public void flyTo(SpaceObject destination) throws NotValidSpaceObjectException {
if (destination instanceof Star) {
throw new NotValidSpaceObjectException(getName() + " no puede volar a " + destination.getName() + ": la estrella la quemaría");
}
moveTo(destination.getCoordinateX(), destination.getCoordinateY(), destination.getCoordinateZ());
System.out.println(getName() + " llegó a " + destination.getName());
}
SpaceShip ship = new SpaceShip("Errante", 149.6, 0, 0, 0);
Planet mars = new Planet("Marte", 227.9, 0, 0, 0.11);
Star sun = new Star("Sol", 0, 0, 0, 333000);
try {
ship.flyTo(mars);
ship.displayInfo();
ship.flyTo(sun);
ship.displayInfo();
} catch (NotValidSpaceObjectException e) {
System.out.println("Rechazado: " + e.getMessage());
}
Salida
Errante llegó a Marte
Nave Errante: (227.9, 0.0, 0.0), masa 0.00
Rechazado: Errante no puede volar a Sol: la estrella la quemaría

El segundo display_info() no se ejecuta: la excepción salta al except.

Más precisamente, para curiosos
Una clase puede extender varios contratos. Un satélite explorador que también pueda volar sería class Probe(SpaceObject, Orbiter, Fly).

Pruébalo

Encuentra el error

La Luna orbita alrededor del Sol, y no debería. ¿En qué línea del orbit del satélite está el error?

Elige la línea con el error.

Ejercicio

El espacio en código

Resuelve las tareas en tu editor, con las clases de la lección.

1. El sistema solar. Describe los ocho planetas con tres listas: nombres, distancias al Sol (millones de km) y masas (masas terrestres). En un bucle, crea cada planeta con x = su distancia y haz que orbite alrededor del Sol. Después muestra todos los objetos que orbitan a su alrededor.

Planeta Distancia Masa
Mercurio 57,9 0,06
Venus 108,2 0,82
Tierra 149,6 1
Marte 227,9 0,11
Júpiter 778,5 317,8
Saturno 1432 95,2
Urano 2867 14,5
Neptuno 4515 17,1
Salida
Mercurio orbita alrededor de Sol
Venus orbita alrededor de Sol
Tierra orbita alrededor de Sol
Marte orbita alrededor de Sol
Júpiter orbita alrededor de Sol
Saturno orbita alrededor de Sol
Urano orbita alrededor de Sol
Neptuno orbita alrededor de Sol
Alrededor de Sol orbitan 8 planetas:
Planeta Mercurio: (57.9, 0.0, 0.0), masa 0.06
Planeta Venus: (108.2, 0.0, 0.0), masa 0.82
Planeta Tierra: (149.6, 0.0, 0.0), masa 1.00
Planeta Marte: (227.9, 0.0, 0.0), masa 0.11
Planeta Júpiter: (778.5, 0.0, 0.0), masa 317.80
Planeta Saturno: (1432.0, 0.0, 0.0), masa 95.20
Planeta Urano: (2867.0, 0.0, 0.0), masa 14.50
Planeta Neptuno: (4515.0, 0.0, 0.0), masa 17.10

2. Distancia. Escribe def distance(a, b) con el teorema de Pitágoras en tres dimensiones: la raíz cuadrada de la suma de los cuadrados de las diferencias en x, y y z. La Tierra está en (149,6; 0; 0), Marte está en (0; 227,9; 0), la Luna está en (149,6; 0,4; 0) y el Sol está en el origen.

Salida
Tierra – Marte: 272.6 millones de km
Tierra – Luna: 0.4 millones de km
Sol – Marte: 227.9 millones de km

3. El árbol completo. Cambia display_info para que la estrella muestre también sus planetas y cada planeta muestre sus satélites, con una sangría de dos espacios por nivel. Pista: un parámetro para la sangría con un valor por defecto, indent="", para que la llamada también funcione sin argumento. OrbitableSpaceObject sobrescribe el método y llama al del padre. La Tierra y Marte orbitan alrededor del Sol, la Luna orbita alrededor de la Tierra y Fobos orbita alrededor de Marte.

Salida
Tierra orbita alrededor de Sol
Marte orbita alrededor de Sol
Luna orbita alrededor de Tierra
Fobos orbita alrededor de Marte
Estrella Sol: (0.0, 0.0, 0.0), masa 333000.00
Planeta Tierra: (149.6, 0.0, 0.0), masa 1.00
Satélite Luna: (150.0, 0.0, 0.0), masa 0.01
Planeta Marte: (227.9, 0.0, 0.0), masa 0.11
Satélite Fobos: (227.9, 0.0, 0.0), masa 0.00

Más tareas

  • Añadida dos veces. Ahora mismo la Tierra se puede añadir dos veces a los objetos que orbitan alrededor del Sol. Haz que un segundo intento lance NotValidSpaceObjectError.
  • Una estrella llena. Añade 11 planetas a una estrella y captura la excepción en el undécimo.
  • Una sonda. Haz una Probe que orbite y también vuele (mira «Más precisamente, para curiosos» en la parte de las naves).

Test

Ponte a prueba

1. Comet extiende SpaceObject pero no escribe get_kind. ¿Qué pasa con Comet("Halley", 0, 0, 0, 0)?
2. ¿Por qué get_orbiters() devuelve una copia de la lista y no la lista misma?
3. Un satélite intenta moon.orbit(sun). ¿Qué pasa?
4. ¿Por qué Orbiter en Python es una clase abstracta con un solo método abstracto?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·