Zum Inhalt
wannacode

Epic Cosmos 1: die Objekte im All

Kurz gesagt: Ein Spiel mit Objekten: Sterne, Planeten und Satelliten sind Klassen, die Regel „wer wen umkreist“ ist der Vertrag einer abstrakten Klasse, und eine ungültige Umlaufbahn löst eine eigene Exception aus.

Problem

Das Wettrennen im All

Die Menschheit kann jetzt zu fernen Planeten fliegen. Konzerne und Staaten wetteifern um ihre Ressourcen. Du leitest eine der Missionen und willst die stärkste Macht im All werden.

Die Regeln von Epic Cosmos:

  • Punkte bekommst du für das Entdecken neuer Planeten und Objekte, den Abbau von Ressourcen, den Bau von Gebäuden und Schiffen und wissenschaftliche Entdeckungen.
  • Die Objekte sind Sterne, Planeten und Satelliten. Ein Stern liefert Energie. Manche Planeten haben seltene Ressourcen, und ihre Atmosphäre entscheidet, was man dort bauen kann. Ein Satellit dient der Kommunikation und der Aufklärung.
  • Ressourcen brauchst du zum Bauen, für die Forschung und um deine Basen und Schiffe zu unterhalten.
  • Gebäude bauen Ressourcen ab, erforschen Technologien, schützen die Basis und bauen Schiffe.
  • Technologien schalten neue Gebäude, Schiffe und Fähigkeiten frei.
  • Konflikte: Im All bist du nicht allein. Es gibt Piraten und andere Spieler.
  • Das Ende: Das Spiel ist vorbei, wenn jemand eine bestimmte Punktzahl erreicht oder nach einer bestimmten Anzahl von Zügen. Es gewinnt, wer die meisten Punkte hat.

Vor den Gebäuden und Schiffen brauchst du die Welt selbst: welche Objekte es gibt, wo sie sind und was wen umkreisen kann. Das ist diese Lektion. Ressourcen, Gebäude und Schiffe kommen in der nächsten.

Die Idee

Alles im All ist ein SpaceObject

Ein Stern, ein Planet und ein Satellit haben etwas gemeinsam: einen Namen, Koordinaten x, y, z und eine Masse. Das Gemeinsame kommt in eine abstrakte Klasse:

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), Masse %.2f%n", getKind(), name, coordinateX, coordinateY, coordinateZ, mass);
}
protected void moveTo(double x, double y, double z) {
coordinateX = x;
coordinateY = y;
coordinateZ = z;
}
}
  • Die Koordinaten sind in Millionen Kilometern von der Sonne, und die Masse ist in Erdmassen: Die Erde ist 1, die Sonne 333.000.
  • display_info() ist allen gemeinsam. Die Art (get_kind()) ist abstrakt: Jede Unterklasse sagt selbst, was sie ist.
  • _move_to beginnt mit einem Unterstrich: Das ist die Absprache für „nur für die Klasse und ihre Unterklassen“. Python verbietet den Aufruf von außen nicht, aber der Unterstrich sagt „nicht anfassen“. Dasselbe gilt für die Felder self._name und die übrigen, die du von außen über @property liest.

Die abstrakte Klasse erbt von ABC, und die abstrakte Methode hat @abstractmethod darüber und pass im Rumpf. Python lässt nicht zu, dass du ein Objekt aus einer Klasse erzeugst, die noch eine abstrakte Methode ohne Rumpf hat:

class Comet(SpaceObject):
pass
try:
comet = Comet("Halley", 0, 0, 0, 0)
except TypeError:
print("TypeError: Comet hat kein get_kind und kann kein Objekt werden")
Ausgabe
TypeError: Comet hat kein get_kind und kann kein Objekt werden

Der Stern ist die einfachste Unterklasse:

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 "Stern";
}
}

Der Konstruktor wird geerbt: Star(...) ruft den der Basisklasse mit denselben Argumenten auf. Darum schreibt Star nur seine Art.

Die drei Arten von Objekten liegen in einer Liste von SpaceObject. Die Schleife weiß nicht, was was ist, und jedes Objekt sagt seine eigene Art:

SpaceObject[] objects = {
new Star("Sonne", 0, 0, 0, 333000),
new Planet("Erde", 149.6, 0, 0, 1),
new Satellite("Mond", 150, 0, 0, 0.012),
};
for (SpaceObject object : objects) {
object.displayInfo();
}
Ausgabe
Stern Sonne: (0.0, 0.0, 0.0), Masse 333000.00
Planet Erde: (149.6, 0.0, 0.0), Masse 1.00
Satellit Mond: (150.0, 0.0, 0.0), Masse 0.01

Die Idee

Objekte, die umkreist werden

Planeten umkreisen einen Stern, und Satelliten umkreisen einen Planeten. Sowohl der Stern als auch der Planet müssen sich merken, was sie umkreist. Das ist OrbitableSpaceObject: eine abstrakte Klasse zwischen SpaceObject und ihnen.

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("Um " + getName() + " kreisen schon " + orbiters.length + " Objekte");
}
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;
}
}
  • Die Umkreisenden liegen in einer Liste, die von allein wächst. Einen Zähler brauchst du nicht: len(self._orbiters) sagt, wie viele es sind.
  • Um einen Stern ist Platz für 10 Objekte: CAPACITY gehört zur Klasse und ist allen ihren Objekten gemeinsam. Wenn der Platz voll ist, wirft add_orbiter RuntimeError: die fertige Exception für den Fall, dass eine Aktion in diesem Moment nicht geht.
  • get_orbiters() gibt eine Kopie der Liste zurück.
Genauer, für Neugierige
  • get_orbiters() gibt eine Kopie zurück, nicht die Liste selbst. Sonst könnte Code von außen mit append hinzufügen und die Prüfung auf Platz umgehen.
  • Star erbt von OrbitableSpaceObject, nicht direkt von SpaceObject. So bekommt der Stern sowohl die Umkreisenden als auch alles von SpaceObject.

Die Kopie siehst du in der Ausgabe: Was du ihr hinzufügst, erreicht den Stern nicht.

sun = Star("Sonne", 0, 0, 0, 333000)
Planet("Erde", 149.6, 0, 0, 1).orbit(sun)
copy = sun.get_orbiters()
copy.append(Planet("Mars", 227.9, 0, 0, 0.11))
print(len(copy), len(sun.get_orbiters()))
Ausgabe
Erde umkreist Sonne
2 1

Die Idee

Die Regel für die Umlaufbahnen

Ein Planet umkreist einen Stern. Ein Satellit umkreist einen Planeten. Alles andere ist ungültig. Die Objekte, die umkreisen können, haben einen gemeinsamen Vertrag:

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

Python hat kein eigenes Schlüsselwort für ein Interface. Der Vertrag ist eine abstrakte Klasse nur mit abstrakten Methoden, und die Klasse, die ihn erfüllt, erbt ihn neben ihrer Basisklasse: class Planet(OrbitableSpaceObject, Orbiter). Python erlaubt es, von mehreren Klassen gleichzeitig zu erben.

Wenn die Regel verletzt wird, wirft orbit eine eigene Exception. Der Name sagt genau, was passiert ist:

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

So setzt der Planet sie durch:

@Override
public void orbit(SpaceObject target) throws NotValidSpaceObjectException {
if (target instanceof Star star) {
star.addOrbiter(this);
System.out.println(getName() + " umkreist " + star.getName());
} else {
throw new NotValidSpaceObjectException(getName() + " kann " + target.getName() + " nicht umkreisen: ein Planet umkreist nur einen Stern");
}
}
  • isinstance(target, Star) prüft den Typ. Nach der Prüfung benutzt du target als Stern: Eine neue Variable brauchst du nicht.
  • Wenn die Prüfung fehlschlägt, stoppt die Exception die Methode. Der Planet wird nirgends hinzugefügt.
  • Satellite hat dasselbe orbit, nur dass es auf Planet prüft.
Genauer, für Neugierige

In Python verfolgt niemand, wer was wirft. Weder der Vertrag noch orbit sagen, dass es werfen kann, und nichts zwingt den Aufrufer, es abzufangen. Das Programm bricht ab, wenn niemand es abfängt, deshalb fangen wir es im Spiel immer ab. Der Name der Exception endet auf Error, wie bei den eingebauten (ValueError).

Sieh es dir an

Wer wen umkreist

sun = Star("Sonne", 0, 0, 0, 333000)
earth = Planet("Erde", 149.6, 0, 0, 1)
moon = Satellite("Mond", 150, 0, 0, 0.012)
try_orbit(earth, sun)
try_orbit(moon, earth)
try_orbit(moon, sun)
Schritt für Schritt, in Worten
  1. Drei Objekte: ein Stern, ein Planet und ein Satellit, jedes mit eigenen Koordinaten und eigener Masse.
  2. Ein Planet um einen Stern: Die Regel erlaubt es, und die Erde startet auf ihre Umlaufbahn.
  3. Ein Satellit um einen Planeten: erlaubt. Der Mond umkreist die Erde und bewegt sich mit ihr.
  4. Ein Satellit um einen Stern: orbit wirft NotValidSpaceObjectError, und das except in try_orbit gibt aus, warum.

Die Erde besteht die Prüfung des Sterns und wird zu den Objekten hinzugefügt, die ihn umkreisen. Der Mond besteht die Prüfung der Erde. Wenn der Mond versucht, die Sonne zu umkreisen, schlägt die Prüfung auf Planet fehl. orbit wirft, und der Mond bleibt bei der Erde.

Die Exception wird in einer kleinen Hilfsfunktion abgefangen. Sie probiert die Umlaufbahn aus und sagt bei einer Ablehnung, warum:

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

Probier es aus

Wie viele umkreisen?

Der Mars versucht, die Erde zu umkreisen.

Was gibt das Programm aus?

def try_orbit(orbiter, target):
try:
orbiter.orbit(target)
except NotValidSpaceObjectError as e:
print(f"Abgelehnt: {e}")
sun = Star("Sonne", 0, 0, 0, 333000)
earth = Planet("Erde", 149.6, 0, 0, 1)
mars = Planet("Mars", 227.9, 0, 0, 0.11)
moon = Satellite("Mond", 150, 0, 0, 0.012)
try_orbit(earth, sun)
try_orbit(mars, earth)
try_orbit(moon, earth)
print(f"Um die Sonne: {len(sun.get_orbiters())}")
print(f"Um die Erde: {len(earth.get_orbiters())}")
Antwort

Die Idee

Ein Schiff, das fliegt

Schiffe umkreisen nichts, sie fliegen von Objekt zu Objekt. Das ist ein anderer Vertrag:

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

SpaceShip erbt von SpaceObject und erfüllt Fly. Zu einem Stern kann es nicht fliegen. Zu allem anderen fliegt es und übernimmt die Koordinaten des Ziels:

@Override
public void flyTo(SpaceObject destination) throws NotValidSpaceObjectException {
if (destination instanceof Star) {
throw new NotValidSpaceObjectException(getName() + " kann nicht zu " + destination.getName() + " fliegen: Der Stern würde das Schiff verbrennen");
}
moveTo(destination.getCoordinateX(), destination.getCoordinateY(), destination.getCoordinateZ());
System.out.println(getName() + " ist bei " + destination.getName() + " angekommen");
}
SpaceShip ship = new SpaceShip("Wanderer", 149.6, 0, 0, 0);
Planet mars = new Planet("Mars", 227.9, 0, 0, 0.11);
Star sun = new Star("Sonne", 0, 0, 0, 333000);
try {
ship.flyTo(mars);
ship.displayInfo();
ship.flyTo(sun);
ship.displayInfo();
} catch (NotValidSpaceObjectException e) {
System.out.println("Abgelehnt: " + e.getMessage());
}
Ausgabe
Wanderer ist bei Mars angekommen
Schiff Wanderer: (227.9, 0.0, 0.0), Masse 0.00
Abgelehnt: Wanderer kann nicht zu Sonne fliegen: Der Stern würde das Schiff verbrennen

Das zweite display_info() läuft nicht: Die Exception springt zu except.

Genauer, für Neugierige
Eine Klasse kann von mehreren Verträgen erben. Ein Aufklärungssatellit, der auch fliegen kann, wäre class Probe(SpaceObject, Orbiter, Fly).

Probier es aus

Finde den Fehler

Der Mond umkreist die Sonne, und das soll er nicht. In welcher Zeile im orbit des Satelliten steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Übung

Das All im Code

Löse die Aufgaben in deinem Editor, mit den Klassen aus der Lektion.

1. Das Sonnensystem. Beschreibe die acht Planeten mit drei Listen: Namen, Abstände von der Sonne (Millionen km) und Massen (Erdmassen). Erzeuge in einer Schleife jeden Planeten bei x = Abstand und lass ihn die Sonne umkreisen. Zeige danach alle Umkreisenden.

Planet Abstand Masse
Merkur 57,9 0,06
Venus 108,2 0,82
Erde 149,6 1
Mars 227,9 0,11
Jupiter 778,5 317,8
Saturn 1.432 95,2
Uranus 2.867 14,5
Neptun 4.515 17,1
Ausgabe
Merkur umkreist Sonne
Venus umkreist Sonne
Erde umkreist Sonne
Mars umkreist Sonne
Jupiter umkreist Sonne
Saturn umkreist Sonne
Uranus umkreist Sonne
Neptun umkreist Sonne
Um Sonne kreisen 8 Planeten:
Planet Merkur: (57.9, 0.0, 0.0), Masse 0.06
Planet Venus: (108.2, 0.0, 0.0), Masse 0.82
Planet Erde: (149.6, 0.0, 0.0), Masse 1.00
Planet Mars: (227.9, 0.0, 0.0), Masse 0.11
Planet Jupiter: (778.5, 0.0, 0.0), Masse 317.80
Planet Saturn: (1432.0, 0.0, 0.0), Masse 95.20
Planet Uranus: (2867.0, 0.0, 0.0), Masse 14.50
Planet Neptun: (4515.0, 0.0, 0.0), Masse 17.10

2. Abstand. Schreibe def distance(a, b) mit dem Satz des Pythagoras in drei Dimensionen: die Wurzel aus der Summe der Quadrate der Unterschiede in x, y und z. Die Erde liegt bei (149,6; 0; 0), der Mars bei (0; 227,9; 0), der Mond bei (149,6; 0,4; 0), und die Sonne liegt im Ursprung.

Ausgabe
Erde bis Mars: 272.6 Mio. km
Erde bis Mond: 0.4 Mio. km
Sonne bis Mars: 227.9 Mio. km

3. Der ganze Baum. Ändere display_info so, dass der Stern auch seine Planeten zeigt und jeder Planet seine Satelliten, mit zwei Leerzeichen Einrückung pro Ebene. Tipp: ein Parameter für die Einrückung mit einem Standardwert, indent="", damit der Aufruf auch ohne Argument geht. OrbitableSpaceObject überschreibt die Methode und ruft die der Basisklasse auf. Erde und Mars umkreisen die Sonne, der Mond umkreist die Erde, und Phobos umkreist den Mars.

Ausgabe
Erde umkreist Sonne
Mars umkreist Sonne
Mond umkreist Erde
Phobos umkreist Mars
Stern Sonne: (0.0, 0.0, 0.0), Masse 333000.00
Planet Erde: (149.6, 0.0, 0.0), Masse 1.00
Satellit Mond: (150.0, 0.0, 0.0), Masse 0.01
Planet Mars: (227.9, 0.0, 0.0), Masse 0.11
Satellit Phobos: (227.9, 0.0, 0.0), Masse 0.00

Weitere Aufgaben

  • Doppelter Versuch. Im Moment kann die Erde zweimal zu den Objekten hinzugefügt werden, die die Sonne umkreisen. Sorge dafür, dass ein zweiter Versuch NotValidSpaceObjectError wirft.
  • Ein voller Stern. Füge einem Stern 11 Planeten hinzu und fange die Exception beim elften ab.
  • Eine Sonde. Baue eine Probe, die sowohl umkreist als auch fliegt (siehe „Genauer“ bei den Schiffen).

Test

Teste dich

1. Comet erbt von SpaceObject, schreibt aber get_kind nicht. Was passiert bei Comet("Halley", 0, 0, 0, 0)?
2. Warum gibt get_orbiters() eine Kopie der Liste zurück und nicht die Liste selbst?
3. Ein Satellit versucht moon.orbit(sun). Was passiert?
4. Warum ist Orbiter in Python eine abstrakte Klasse mit nur einer abstrakten Methode?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·