Zum Inhalt
wannacode

Epic Cosmos 2: Ressourcen, Gebäude, Schiffe

Kurz gesagt: Ein rundenbasiertes Spiel: Ressourcen werden in jedem Zug gefördert, Gebäude kosten Ressourcen, Minen verdoppeln die Förderung, und fehlende Ressourcen sind eine eigene Exception.

Problem

Die Welt ist bereit, aber nichts passiert

Aus der letzten Lektion hast du Sterne, Planeten und Satelliten. Aber das Spiel läuft noch nicht: Es gibt nichts, womit du bauen kannst, und nichts, worauf du warten kannst.

Die Wirtschaft von Epic Cosmos:

  • Jeder Planet hat vier Ressourcen. In jedem Zug werden sie gefördert, jede mit ihrer eigenen Rate: Metall 1,0, Gas 0,5, Kristall 0,2, Uran 0,1 pro Zug.
  • Gebäude kosten Ressourcen und Zeit. Die Minen und die Raffinerie verdoppeln die Rate ihrer Ressource mit jeder Stufe. Das Forschungslabor und die Werft schalten andere Dinge frei.
  • Schiffe fliegen. Der Späher erkundet die Umgebung und meldet, was jeder Planet hat.
  • Das Spiel läuft in Zügen. Punkte kommen von Gebäuden, Schiffen und Entdeckungen. Das Ende kommt bei einer bestimmten Zahl von Punkten oder Zügen.

Wenn die Ressourcen nicht reichen, muss das Bauen ablehnen und sagen, was fehlt. Das ist eine Aufgabe für eine eigene Exception.

Die Idee

Die Ressource

Eine Ressource hat eine Menge (amount) und eine Förderrate (harvest_rate). In jedem Zug addiert harvest() die Rate zur Menge. Eine Mine ruft boost() auf, und die Rate verdoppelt sich.

abstract class Resource {
private double amount;
private double harvestRate;
public Resource(double amount, double harvestRate) {
this.amount = amount;
this.harvestRate = harvestRate;
}
public abstract String getName();
public double getAmount() {
return amount;
}
public double getHarvestRate() {
return harvestRate;
}
public void harvest() {
amount = round(amount + harvestRate);
}
public void boost() {
harvestRate = harvestRate * 2;
}
public void add(double quantity) {
amount = round(amount + quantity);
}
public void spend(double quantity) {
amount = round(amount - quantity);
}
private static double round(double value) {
return Math.round(value * 10) / 10.0;
}
}

Die vier Arten unterscheiden sich nur in Name und Rate:

class Metal extends Resource {
public Metal(double amount) {
super(amount, 1.0);
}
@Override
public String getName() {
return "Metall";
}
}

Gas hat 0,5, Crystal 0,2, Uranium 0,1.

Genauer, für Neugierige

float kann 0,1 nicht genau speichern. Addierst du 0,1 zehnmal, bekommst du 0,9999999999999999 statt 1,0. Dann würde die Prüfung „hast du 1 Uran“ „nein“ sagen. Deshalb rundet round(…, 1) jede neue Menge auf Zehntel. In echten Spielen sind die Mengen oft ganze Zahlen, zum Beispiel in Zehnteln einer Einheit, genau um solche Überraschungen zu vermeiden.

total = 0
for i in range(10):
total += 0.1
print(total)
Ausgabe
0.9999999999999999
round(x, 1) ist die eingebaute Funktion von Python zum Runden auf eine Stelle nach dem Komma, deshalb brauchst du keine eigene Methode.

Sieh es dir an

Die Ressourcen des Planeten

Planet aus der letzten Lektion bekommt vier Ressourcen und eine Methode, die sie alle auf einmal fördert:

private Resource metal = new Metal(0);
private Resource gas = new Gas(0);
private Resource crystal = new Crystal(0);
private Resource uranium = new Uranium(0);
public Resource getMetal() {
return metal;
}
public Resource getGas() {
return gas;
}
public Resource getCrystal() {
return crystal;
}
public Resource getUranium() {
return uranium;
}
public void harvestAll() {
metal.harvest();
gas.harvest();
crystal.harvest();
uranium.harvest();
}
public String resourcesText() {
return String.format("Metall %.1f, Gas %.1f, Kristall %.1f, Uran %.1f", metal.getAmount(), gas.getAmount(), crystal.getAmount(), uranium.getAmount());
}
public void printResources() {
System.out.println(getName() + ": " + resourcesText());
}

Die Ressourcen werden in __init__ des Planeten angelegt. Hier wird __init__ gebraucht, deshalb ruft es die Basisklasse mit super().__init__(...) auf und fügt dann das Eigene hinzu.

Fünf Züge Förderung, dann Metall verdoppeln und noch ein Zug:

Planet home = new Planet("Nova Terra", 100, 0, 0, 1);
for (int turn = 1; turn <= 5; turn++) {
home.harvestAll();
}
home.printResources();
home.getMetal().boost();
home.harvestAll();
home.printResources();
Ausgabe
Nova Terra: Metall 5.0, Gas 2.5, Kristall 1.0, Uran 0.5
Nova Terra: Metall 7.0, Gas 3.0, Kristall 1.2, Uran 0.6

Probier es aus

Wie viel Gas?

Was gibt das Programm aus?

gas = Gas(0)
gas.harvest()
gas.boost()
gas.harvest()
gas.boost()
gas.harvest()
print(f"{gas.amount} {gas.get_name()}, Rate {gas.harvest_rate}")
Antwort

Die Idee

Bauen kostet etwas

Wenn die Ressourcen nicht reichen, muss das Bauen ablehnen. Der Aufrufer will wissen, was und wie viel fehlt, deshalb ist die Exception eine eigene und trägt die Zahl:

class NotEnoughResourcesException extends Exception {
private double missing;
public NotEnoughResourcesException(String purpose, String resource, double missing) {
super(String.format("Für %s fehlen noch %.1f %s", purpose, missing, resource));
this.missing = missing;
}
public double getMissing() {
return missing;
}
}

Das Bezahlen steht in Planet. Zuerst prüft es die vier Ressourcen, dann gibt es aus:

public void spend(String purpose, double metalNeeded, double crystalNeeded, double gasNeeded, double uraniumNeeded) throws NotEnoughResourcesException {
check(purpose, metal, metalNeeded);
check(purpose, crystal, crystalNeeded);
check(purpose, gas, gasNeeded);
check(purpose, uranium, uraniumNeeded);
metal.spend(metalNeeded);
crystal.spend(crystalNeeded);
gas.spend(gasNeeded);
uranium.spend(uraniumNeeded);
}
private void check(String purpose, Resource resource, double needed) throws NotEnoughResourcesException {
if (resource.getAmount() < needed) {
throw new NotEnoughResourcesException(purpose, resource.getName(), needed - resource.getAmount());
}
}

Probier es aus

Finde den Fehler

Die Kolonie hat 20 Metall und keinen Kristall. Das Forschungslabor kostet 12 Metall und 5 Kristall. Das Bauen wird abgelehnt, aber das Metall ist auf 8 gesunken. In welcher Zeile liegt das Problem?

Wähle die Zeile mit dem Fehler.

Die Idee

Das Gebäude

Jedes Gebäude hat einen Namen, eine Stufe, eine Bauzeit und einen Preis in den vier Ressourcen. Bauen und Ausbauen sind für alle Gebäude gleich, deshalb stehen sie in der abstrakten Klasse BaseBuilding:

public String getRequiredBuilding() {
return null;
}
public void construct(Planet target) throws NotEnoughResourcesException {
if (target.findBuilding(name) != null) {
throw new IllegalStateException(name + " ist schon gebaut");
}
String required = getRequiredBuilding();
if (required != null && target.findBuilding(required) == null) {
throw new IllegalStateException(name + " setzt " + required + " voraus");
}
target.spend(name, metalRequired, crystalRequired, gasRequired, uraniumRequired);
planet = target;
level = 1;
target.addBuilding(this);
}
public void upgrade() throws NotEnoughResourcesException {
if (planet == null) {
throw new IllegalStateException(name + " ist noch nicht gebaut");
}
double factor = Math.pow(2, level);
planet.spend(name + " Stufe " + (level + 1), metalRequired * factor, crystalRequired * factor, gasRequired * factor, uraniumRequired * factor);
level++;
}
public abstract void applyEffect();
  • construct lehnt ab, wenn das Gebäude schon da ist oder wenn das Gebäude fehlt, von dem es abhängt. Diese Ablehnungen nutzen die vorhandene Exception für den falschen Moment (RuntimeError). Danach bezahlt es und trägt das Gebäude im Planeten ein.
  • upgrade kostet mit jeder Stufe doppelt so viel: Stufe 2 kostet das Zweifache des Preises, Stufe 3 das Vierfache.
  • apply_effect ist abstrakt: Jedes Gebäude weiß selbst, was es tut, wenn es fertig ist.
Genauer, für Neugierige

get_required_building() gibt None zurück: Standardmäßig hängt ein Gebäude von nichts ab. Eine Unterklasse, die von etwas abhängt, überschreibt es.

Die Idee

Minen und Abhängigkeiten

Die Minen unterscheiden sich nur darin, welche Ressource sie beschleunigen. Das Gemeinsame steht in Mine:

abstract class Mine extends BaseBuilding {
public Mine(String name, int constructionTime, double metalRequired, double crystalRequired) {
super(name, 0, constructionTime, metalRequired, crystalRequired, 0, 0);
}
protected abstract Resource resourceOf(Planet planet);
@Override
public void applyEffect() {
resourceOf(getPlanet()).boost();
}
}
class MetalMine extends Mine {
public MetalMine() {
super("Metallmine", 1, 6, 2);
}
@Override
protected Resource resourceOf(Planet planet) {
return planet.getMetal();
}
}

Metallmine und Kristallmine kannst du sofort bauen. Das Forschungslabor setzt eine Kristallmine voraus, und die Gasraffinerie und die Werft setzen ein Forschungslabor voraus:

class ResearchLab extends BaseBuilding {
public ResearchLab() {
super("Forschungslabor", 0, 2, 12, 4, 0, 0);
}
@Override
public String getRequiredBuilding() {
return "Kristallmine";
}
@Override
public void applyEffect() {
// Es schaltet andere Gebäude frei. Die Ressourcen ändern sich nicht.
}
}

Ein Plan aus drei Gebäuden mit 10 Metall und 3 Kristall. Das Forschungslabor wird wegen der Abhängigkeit abgelehnt, die Kristallmine wegen des Metalls:

Planet home = new Planet("Nova Terra", 100, 0, 0, 1);
home.getMetal().add(10);
home.getCrystal().add(3);
BaseBuilding[] plan = { new ResearchLab(), new MetalMine(), new CrystalMine() };
for (BaseBuilding building : plan) {
try {
building.construct(home);
building.applyEffect();
System.out.println(building.getName() + " ist fertig.");
} catch (NotEnoughResourcesException e) {
System.out.println("Abgelehnt: " + e.getMessage());
} catch (IllegalStateException e) {
System.out.println("Abgelehnt: " + e.getMessage());
}
}
home.printResources();
System.out.println("Förderrate Metall: " + home.getMetal().getHarvestRate());
Ausgabe
Abgelehnt: Forschungslabor setzt Kristallmine voraus
Metallmine ist fertig.
Abgelehnt: Für Kristallmine fehlen noch 4.0 Metall
Nova Terra: Metall 4.0, Gas 0.0, Kristall 1.0, Uran 0.0
Förderrate Metall: 2.0

home = Planet("Nova Terra", 100, 0, 0, 1)
home.metal.add(10)
home.crystal.add(3)
plan = [ResearchLab(), MetalMine(), CrystalMine()]
for building in plan:
try:
building.construct(home)
building.apply_effect()
print(f"{building.name} ist fertig.")
except NotEnoughResourcesError as e:
print(f"Abgelehnt: {e}")
except RuntimeError as e:
print(f"Abgelehnt: {e}")
home.print_resources()
print(f"Förderrate Metall: {home.metal.harvest_rate}")
Schritt für Schritt, in Worten
  1. Nova Terra hat 10 Metall und 3 Kristall. Metall wird mit 1.0 pro Zug gefördert.
  2. Der Plan ist eine Liste aus 3 Gebäuden. Jedes hat einen Preis in Metall und Kristall.
  3. Forschungslabor setzt Kristallmine voraus, doch das fehlt noch. construct() wirft RuntimeError, bevor etwas ausgegeben wird.
  4. Metallmine kostet 6 Metall und 2 Kristall. Das reicht: 4 Metall und 1 Kristall bleiben übrig. apply_effect() verdoppelt die Förderrate von Metall.
  5. Kristallmine braucht 8 Metall, vorhanden sind 4. Die Prüfung wirft NotEnoughResourcesError, und nichts wird ausgegeben.
  6. Die Ablehnungen haben nichts ausgegeben. Nur der Preis des gebauten Gebäudes ist weg, und die Förderrate von Metall ist 2.0.

Die Idee

Schiffe und der Späher

SpaceShip aus der letzten Lektion bekommt einen Typ, eine Geschwindigkeit, eine Ladekapazität und eine Waffenstärke. Es startet auf dem Heimatplaneten und fliegt mit fly_to, wie zuvor. Der Späher ist ein Schiff mit Sensoren: explore prüft, welche Planeten in seiner Reichweite liegen, und meldet ihre Ressourcen.

class Scout extends SpaceShip {
private double sensorRange;
public Scout(String name, Planet home) {
super(name, "Späher", 40, 0, 0, home);
sensorRange = 100;
}
public int explore(Planet[] planets) {
int found = 0;
for (Planet planet : planets) {
double dx = planet.getCoordinateX() - getCoordinateX();
double dy = planet.getCoordinateY() - getCoordinateY();
double dz = planet.getCoordinateZ() - getCoordinateZ();
if (Math.sqrt(dx * dx + dy * dy + dz * dz) <= sensorRange) {
reportResources(planet);
found++;
}
}
return found;
}
public void reportResources(Planet planet) {
System.out.println(getName() + " fand " + planet.getName() + ": " + planet.resourcesText());
}
}

Die Reichweite wird mit dem Abstand aus Aufgabe 2 der letzten Lektion gemessen. Die Methode gibt zurück, wie viele Planeten sie gefunden hat, damit das Spiel Punkte dafür geben kann.

Sieh es dir an

Das ganze Spiel

Game hält den Heimatplaneten, die anderen Planeten, den Zug und die Punkte. Die Befehle kommen vom Spieler, einer pro Zeile. Gebäude brauchen Züge, und in jedem Zug fördert der Planet:

public void handle(String command) {
int space = command.indexOf(' ');
String verb = space == -1 ? command : command.substring(0, space);
String rest = space == -1 ? "" : command.substring(space + 1);
try {
if (verb.equals("bauen")) {
build(createBuilding(rest));
} else if (verb.equals("ausbauen")) {
upgrade(rest);
} else if (verb.equals("späher")) {
buildScout();
} else if (verb.equals("erkunden")) {
explore();
} else if (verb.equals("warten")) {
waitTurns(1);
} else {
System.out.println("Unbekannter Befehl: " + command);
}
} catch (NotEnoughResourcesException e) {
System.out.println("Abgelehnt: " + e.getMessage());
} catch (IllegalStateException e) {
System.out.println("Abgelehnt: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println("Abgelehnt: " + e.getMessage());
}
}

partition(" ") teilt den Text am ersten Leerzeichen und gibt drei Teile zurück: vor dem Leerzeichen, das Leerzeichen selbst und danach. Gibt es kein Leerzeichen, ist der dritte Teil leer. Den mittleren brauchen wir nicht, deshalb heißt er _. Alle Ablehnungen werden mit einem except und einem Tupel der Typen gefangen.

Jedes Gebäude bringt 10 Punkte, ein Ausbau das Zehnfache der neuen Stufe, der Späher 15 und jeder gefundene Planet 5. Das Spiel endet bei 80 Punkten oder 40 Zügen. Die Schleife des Programms:

Game game = new Game(home, others, 80, 40);
Scanner in = new Scanner(System.in);
while (!game.isOver() && in.hasNextLine()) {
String command = in.nextLine();
System.out.println("> " + command);
game.handle(command);
System.out.println(" " + game.status());
}
System.out.println("Ende: " + game.getPoints() + " Punkte in " + game.getTurn() + " Zügen.");
Ausgabe
> bauen forschungslabor
Abgelehnt: Forschungslabor setzt Kristallmine voraus
Zug 0, Punkte 0 | Metall 40.0, Gas 0.0, Kristall 15.0, Uran 0.0
> bauen metallmine
Metallmine ist fertig.
Zug 1, Punkte 10 | Metall 35.0, Gas 0.5, Kristall 13.2, Uran 0.1
> bauen kristallmine
Kristallmine ist fertig.
Zug 2, Punkte 20 | Metall 29.0, Gas 1.0, Kristall 12.4, Uran 0.2
> bauen forschungslabor
Forschungslabor ist fertig.
Zug 4, Punkte 30 | Metall 21.0, Gas 2.0, Kristall 9.2, Uran 0.4
> ausbauen metallmine
Metallmine ist auf Stufe 2.
Zug 5, Punkte 50 | Metall 11.0, Gas 2.5, Kristall 5.6, Uran 0.5
> bauen werft
Abgelehnt: Für Werft fehlen noch 4.0 Metall
Zug 5, Punkte 50 | Metall 11.0, Gas 2.5, Kristall 5.6, Uran 0.5
> warten
Zug 6, Punkte 50 | Metall 15.0, Gas 3.0, Kristall 6.0, Uran 0.6
> bauen werft
Werft ist fertig.
Zug 9, Punkte 60 | Metall 12.0, Gas 2.5, Kristall 3.2, Uran 0.9
> späher
Abgelehnt: Für Späher fehlen noch 0.1 Uran
Zug 9, Punkte 60 | Metall 12.0, Gas 2.5, Kristall 3.2, Uran 0.9
> warten
Zug 10, Punkte 60 | Metall 16.0, Gas 3.0, Kristall 3.6, Uran 1.0
> späher
Späher-1 ist fertig.
Zug 12, Punkte 75 | Metall 14.0, Gas 2.0, Kristall 1.4, Uran 0.2
> erkunden
Späher-1 fand Kobalt: Metall 500.0, Gas 0.0, Kristall 40.0, Uran 0.0
Späher-1 fand Helia: Metall 0.0, Gas 300.0, Kristall 0.0, Uran 20.0
Gefundene Planeten: 2
Zug 13, Punkte 85 | Metall 18.0, Gas 2.5, Kristall 1.8, Uran 0.3
Ende: 85 Punkte in 13 Zügen.
input() wirft EOFError, wenn die Eingabe zu Ende ist. Dann stoppt break die Schleife.

Mit dieser Eingabe:

Eingabe
bauen forschungslabor
bauen metallmine
bauen kristallmine
bauen forschungslabor
ausbauen metallmine
bauen werft
warten
bauen werft
späher
warten
späher
erkunden
Genauer, für Neugierige
  • Ablehnungen bewegen den Zug nicht: Das Gebäude wurde nicht begonnen, deshalb kann der Spieler sofort etwas anderes versuchen.
  • Forschung (die Technologien aus den Regeln) steht in der alten Aufgabe nur als Plan: eine Klasse Research mit Name, Stufe und Preis, mit Methoden zum Erforschen und Hochstufen. Sie und die Konflikte (Piraten, Angriff mit weapon_power) sind der nächste Schritt, wenn du allein weitermachen willst.

Übung

Die Kolonie wächst

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

1. 100 Metall so schnell wie möglich. Nova Terra startet mit 40 Metall und 15 Kristall. Lies den Plan aus der Eingabe, ein Befehl pro Zeile, führe ihn aus und warte dann, bis das Metall 100 erreicht. Gib aus, in welchem Zug das passiert ist. Mit diesem Plan ist es Zug 21. Finde einen schnelleren.

Eingabe
bauen metallmine
ausbauen metallmine
Ausgabe
Metallmine ist fertig.
Metallmine ist auf Stufe 2.
100 Metall im Zug 21: Zug 21, Punkte 30 | Metall 101.0, Gas 10.5, Kristall 13.2, Uran 2.1

2. Ein Frachter. Schreibe CargoShip, eine Unterklasse von SpaceShip, mit einer Ladekapazität von 50. harvest_resource(planet) lädt so viel vom Metall des Planeten, wie hineinpasst, aber nicht mehr, als da ist. unload(planet) lädt es wieder ab. Kobalt hat 120 Metall. Das Schiff Muli macht drei Fahrten von Nova Terra nach Kobalt und zurück.

Ausgabe
Muli ist bei Kobalt angekommen
Muli hat 50.0 Metall von Kobalt geladen
Muli ist bei Nova Terra angekommen
Muli hat 50.0 Metall bei Nova Terra entladen
Muli ist bei Kobalt angekommen
Muli hat 50.0 Metall von Kobalt geladen
Muli ist bei Nova Terra angekommen
Muli hat 50.0 Metall bei Nova Terra entladen
Muli ist bei Kobalt angekommen
Muli hat 20.0 Metall von Kobalt geladen
Muli ist bei Nova Terra angekommen
Muli hat 20.0 Metall bei Nova Terra entladen
Nova Terra: Metall 120.0, Gas 0.0, Kristall 0.0, Uran 0.0
Kobalt: Metall 0.0, Gas 0.0, Kristall 0.0, Uran 0.0

3. Eine Simulation über 10 Züge. Ohne Spieler: Das Spiel hat eine Liste von Gebäuden (Metallmine, Kristallmine, Forschungslabor, Gasraffinerie). In jedem Schritt versucht es, das nächste zu bauen. Geht das nicht, wartet es einen Zug. Nach jedem Schritt gibt es den Zustand aus, bis die Züge 10 erreichen.

Ausgabe
Metallmine ist fertig.
Zug 1, Punkte 10 | Metall 35.0, Gas 0.5, Kristall 13.2, Uran 0.1
Kristallmine ist fertig.
Zug 2, Punkte 20 | Metall 29.0, Gas 1.0, Kristall 12.4, Uran 0.2
Forschungslabor ist fertig.
Zug 4, Punkte 30 | Metall 21.0, Gas 2.0, Kristall 9.2, Uran 0.4
Gasraffinerie ist fertig.
Zug 6, Punkte 40 | Metall 15.0, Gas 3.0, Kristall 7.0, Uran 0.6
Zug 7, Punkte 40 | Metall 17.0, Gas 4.0, Kristall 7.4, Uran 0.7
Zug 8, Punkte 40 | Metall 19.0, Gas 5.0, Kristall 7.8, Uran 0.8
Zug 9, Punkte 40 | Metall 21.0, Gas 6.0, Kristall 8.2, Uran 0.9
Zug 10, Punkte 40 | Metall 23.0, Gas 7.0, Kristall 8.6, Uran 1.0

Weitere Aufgaben

  • Eine Uranmine. Füge eine UraniumMine hinzu, die von der Gasraffinerie abhängt.
  • Punkte für Ressourcen. Gib 1 Punkt für je 50 geförderte Metall.
  • Piraten. Alle 10 Züge nehmen Piraten 20 % des Metalls, wenn der Planet kein Schiff mit einer Waffenstärke von mindestens 10 hat.

Test

Teste dich

1. Metall wird mit 1,0 pro Zug gefördert. Du baust eine Metallmine und baust sie bis Stufe 3 aus. Wie viel Metall pro Zug liefert der Planet?
2. Warum prüft spend zuerst alle vier Ressourcen und gibt erst danach etwas aus?
3. Das Forschungslabor setzt zuerst eine Kristallmine voraus. Warum wirft dieser Fehler die vorhandene Exception und nicht NotEnoughResourcesError?
4. Wann endet das Spiel in der Lektion?

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