Epic Cosmos 1: the objects in space
Problem
The space race
People can now fly to distant planets. Companies and countries race for their resources. You run one of the missions, and you want to become the strongest power in space.
The rules of Epic Cosmos:
- You earn points by discovering new planets and objects, mining resources, building buildings and ships, and making scientific discoveries.
- The objects are stars, planets and satellites. A star gives energy. Some planets have rare resources, and their atmosphere decides what you can build there. A satellite is for communication and scouting.
- You need resources to build, to do research and to keep your bases and ships running.
- Buildings mine resources, research technologies, defend the base and make ships.
- Technologies unlock new buildings, ships and skills.
- Conflicts: you are not alone in space. There are pirates and other players.
- The end: the game is over when someone reaches a set number of points or after a set number of turns. The player with the most points wins.
Before buildings and ships, you need the world itself: what objects there are, where they are and what can orbit what. That is this lesson. Resources, buildings and ships are in the next one.
The idea
Everything in space is a SpaceObject
A star, a planet and a satellite have something in common: a name, coordinates x, y, z and a mass. What they share goes into an abstract class:
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), mass %.2f%n", getKind(), name, coordinateX, coordinateY, coordinateZ, mass); }
protected void moveTo(double x, double y, double z) { coordinateX = x; coordinateY = y; coordinateZ = z; }}abstract class SpaceObject{ public string Name { get; } public double CoordinateX { get; private set; } public double CoordinateY { get; private set; } public double CoordinateZ { get; private set; } public double Mass { get; }
public SpaceObject(string name, double coordinateX, double coordinateY, double coordinateZ, double mass) { Name = name; CoordinateX = coordinateX; CoordinateY = coordinateY; CoordinateZ = coordinateZ; Mass = mass; }
public abstract string GetKind();
public void DisplayInfo() { Console.WriteLine($"{GetKind()} {Name}: ({CoordinateX:F1}, {CoordinateY:F1}, {CoordinateZ:F1}), mass {Mass:F2}"); }
protected void MoveTo(double x, double y, double z) { CoordinateX = x; CoordinateY = y; CoordinateZ = z; }}class SpaceObject(ABC): def __init__(self, name, coordinate_x, coordinate_y, coordinate_z, mass): self._name = name self._coordinate_x = coordinate_x self._coordinate_y = coordinate_y self._coordinate_z = coordinate_z self._mass = mass
@property def name(self): return self._name
@property def coordinate_x(self): return self._coordinate_x
@property def coordinate_y(self): return self._coordinate_y
@property def coordinate_z(self): return self._coordinate_z
@property def mass(self): return self._mass
@abstractmethod def get_kind(self): pass
def display_info(self): print(f"{self.get_kind()} {self._name}: ({self._coordinate_x:.1f}, {self._coordinate_y:.1f}, {self._coordinate_z:.1f}), mass {self._mass:.2f}")
def _move_to(self, x, y, z): self._coordinate_x = x self._coordinate_y = y self._coordinate_z = zclass SpaceObject { #name; #coordinateX; #coordinateY; #coordinateZ; #mass;
constructor(name, coordinateX, coordinateY, coordinateZ, mass) { this.#name = name; this.#coordinateX = coordinateX; this.#coordinateY = coordinateY; this.#coordinateZ = coordinateZ; this.#mass = mass; }
get name() { return this.#name; }
get coordinateX() { return this.#coordinateX; }
get coordinateY() { return this.#coordinateY; }
get coordinateZ() { return this.#coordinateZ; }
get mass() { return this.#mass; }
getKind() { throw new Error(`${this.constructor.name} must write getKind()`); }
displayInfo() { console.log(`${this.getKind()} ${this.#name}: (${this.#coordinateX.toFixed(1)}, ${this.#coordinateY.toFixed(1)}, ${this.#coordinateZ.toFixed(1)}), mass ${this.#mass.toFixed(2)}`); }
_moveTo(x, y, z) { this.#coordinateX = x; this.#coordinateY = y; this.#coordinateZ = z; }}- The coordinates are in millions of kilometers from the Sun, and the mass is in Earth masses: Earth is 1, the Sun is 333,000.
display_info()is shared by all of them. The kind (get_kind()) is abstract: each subclass says for itself what it is.-
_move_tostarts with an underscore: that is the convention for "only for the class and its subclasses". Python does not stop calls from outside, but the underscore says "do not touch". The same goes for the fieldsself._nameand the rest, which you read from outside through@property.
The abstract class extends ABC, and the abstract method has @abstractmethod above it and pass in its body. Python does not let you create an object from a class that still has an abstract method with no body:
class Comet(SpaceObject): pass
try: comet = Comet("Halley", 0, 0, 0, 0)except TypeError: print("TypeError: Comet has no get_kind and cannot become an object")TypeError: Comet has no get_kind and cannot become an objectThe star is the simplest subclass:
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 "Star"; }}class Star : OrbitableSpaceObject{ public Star(string name, double coordinateX, double coordinateY, double coordinateZ, double mass) : base(name, coordinateX, coordinateY, coordinateZ, mass) { }
public override string GetKind() { return "Star"; }}class Star(OrbitableSpaceObject): def get_kind(self): return "Star"class Star extends OrbitableSpaceObject { getKind() { return "Star"; }}The constructor is inherited: Star(...) calls the parent's constructor with the same arguments. That is why Star only writes its kind.
The three kinds of objects go in one list of SpaceObject. The loop does not know which is which, and each one says its own kind:
SpaceObject[] objects = { new Star("Sun", 0, 0, 0, 333000), new Planet("Earth", 149.6, 0, 0, 1), new Satellite("Moon", 150, 0, 0, 0.012),};for (SpaceObject object : objects) { object.displayInfo();}Star Sun: (0.0, 0.0, 0.0), mass 333000.00Planet Earth: (149.6, 0.0, 0.0), mass 1.00Satellite Moon: (150.0, 0.0, 0.0), mass 0.01SpaceObject[] objects ={ new Star("Sun", 0, 0, 0, 333000), new Planet("Earth", 149.6, 0, 0, 1), new Satellite("Moon", 150, 0, 0, 0.012),};foreach (SpaceObject spaceObject in objects){ spaceObject.DisplayInfo();}Star Sun: (0.0, 0.0, 0.0), mass 333000.00Planet Earth: (149.6, 0.0, 0.0), mass 1.00Satellite Moon: (150.0, 0.0, 0.0), mass 0.01objects = [ Star("Sun", 0, 0, 0, 333000), Planet("Earth", 149.6, 0, 0, 1), Satellite("Moon", 150, 0, 0, 0.012),]for space_object in objects: space_object.display_info()Star Sun: (0.0, 0.0, 0.0), mass 333000.00Planet Earth: (149.6, 0.0, 0.0), mass 1.00Satellite Moon: (150.0, 0.0, 0.0), mass 0.01const objects = [ new Star("Sun", 0, 0, 0, 333000), new Planet("Earth", 149.6, 0, 0, 1), new Satellite("Moon", 150, 0, 0, 0.012),];for (const spaceObject of objects) { spaceObject.displayInfo();}Star Sun: (0.0, 0.0, 0.0), mass 333000.00Planet Earth: (149.6, 0.0, 0.0), mass 1.00Satellite Moon: (150.0, 0.0, 0.0), mass 0.01The idea
Objects that get orbited
Planets orbit a star, and satellites orbit a planet. Both the star and the planet have to remember what orbits them. That is OrbitableSpaceObject: an abstract class between SpaceObject and them.
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(orbiters.length + " objects already orbit " + getName()); } 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; }}abstract class OrbitableSpaceObject : SpaceObject{ private SpaceObject[] orbiters = new SpaceObject[10]; private int orbiterCount = 0;
public OrbitableSpaceObject(string name, double coordinateX, double coordinateY, double coordinateZ, double mass) : base(name, coordinateX, coordinateY, coordinateZ, mass) { }
public void AddOrbiter(SpaceObject orbiter) { if (orbiterCount == orbiters.Length) { throw new InvalidOperationException(orbiters.Length + " objects already orbit " + Name); } 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; }}class OrbitableSpaceObject(SpaceObject): CAPACITY = 10
def __init__(self, name, coordinate_x, coordinate_y, coordinate_z, mass): super().__init__(name, coordinate_x, coordinate_y, coordinate_z, mass) self._orbiters = []
def add_orbiter(self, orbiter): if len(self._orbiters) == self.CAPACITY: raise RuntimeError(f"{self.CAPACITY} objects already orbit {self.name}") self._orbiters.append(orbiter)
def get_orbiters(self): return list(self._orbiters)class OrbitableSpaceObject extends SpaceObject { static CAPACITY = 10; #orbiters = [];
addOrbiter(orbiter) { if (this.#orbiters.length === OrbitableSpaceObject.CAPACITY) { throw new Error(`${OrbitableSpaceObject.CAPACITY} objects already orbit ${this.name}`); } this.#orbiters.push(orbiter); }
getOrbiters() { return [...this.#orbiters]; }}- The orbiting objects are in a list that grows on its own. You do not need a counter:
len(self._orbiters)says how many there are. - A star has room for 10 objects:
CAPACITYis on the class, shared by all objects of it. If the room fills up,add_orbiterthrowsRuntimeError: the built-in exception for when an action cannot happen at this moment. get_orbiters()returns a copy of the list.
More precisely, if you are curious
get_orbiters()returns a copy, not the list itself. Otherwise code outside could add withappendand get around the check for room.StarextendsOrbitableSpaceObject, notSpaceObjectdirectly. That way the star gets both the orbiting objects and everything fromSpaceObject.
You can see the copy in the output: what you add to it does not reach the star.
sun = Star("Sun", 0, 0, 0, 333000)Planet("Earth", 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()))Earth orbits Sun2 1The idea
The rule for orbits
A planet orbits a star. A satellite orbits a planet. Everything else is invalid. The objects that can orbit share a contract:
interface Orbiter { void orbit(SpaceObject target) throws NotValidSpaceObjectException;}interface Orbiter{ void Orbit(SpaceObject target);}class Orbiter(ABC): @abstractmethod def orbit(self, target): pass// The Orbiter contract: the object has a method orbit(target) that makes it orbit// target or throws NotValidSpaceObjectError.// JavaScript has no interface: Planet and Satellite simply have such a method.Python has no separate keyword for an interface. The contract is an abstract class with only abstract methods, and the class that fulfills it extends it next to its parent: class Planet(OrbitableSpaceObject, Orbiter). Python lets you extend several classes at once.
If the rule is broken, orbit throws its own exception. The name says exactly what happened:
class NotValidSpaceObjectException extends Exception { public NotValidSpaceObjectException(String message) { super(message); }}class NotValidSpaceObjectException : Exception{ public NotValidSpaceObjectException(string message) : base(message) { }}class NotValidSpaceObjectError(Exception): passclass NotValidSpaceObjectError extends Error { constructor(message) { super(message); this.name = "NotValidSpaceObjectError"; }}Here is how the planet enforces it:
@Overridepublic void orbit(SpaceObject target) throws NotValidSpaceObjectException { if (target instanceof Star star) { star.addOrbiter(this); System.out.println(getName() + " orbits " + star.getName()); } else { throw new NotValidSpaceObjectException(getName() + " cannot orbit " + target.getName() + ": a planet orbits only a star"); }}public void Orbit(SpaceObject target){ if (target is Star star) { star.AddOrbiter(this); Console.WriteLine(Name + " orbits " + star.Name); } else { throw new NotValidSpaceObjectException(Name + " cannot orbit " + target.Name + ": a planet orbits only a star"); }}def orbit(self, target): if isinstance(target, Star): target.add_orbiter(self) print(f"{self.name} orbits {target.name}") else: raise NotValidSpaceObjectError(f"{self.name} cannot orbit {target.name}: a planet orbits only a star")orbit(target) { if (target instanceof Star) { target.addOrbiter(this); console.log(`${this.name} orbits ${target.name}`); } else { throw new NotValidSpaceObjectError(`${this.name} cannot orbit ${target.name}: a planet orbits only a star`); }}isinstance(target, Star)checks the type. After the check you usetargetas a star: you do not need a new variable.- If the check fails, the exception stops the method. The planet is not added anywhere.
Satellitehas the sameorbit, except that it checks forPlanet.
More precisely, if you are curious
In Python nobody tracks what throws what. Neither the contract nor orbit says that it can throw, and nothing makes the caller catch it. The program will stop if nobody catches it, so in the game we always catch it. The name of the exception ends in Error, like the built-in ones (ValueError).
See it
What orbits what
sun = Star("Sun", 0, 0, 0, 333000)earth = Planet("Earth", 149.6, 0, 0, 1)moon = Satellite("Moon", 150, 0, 0, 0.012)try_orbit(earth, sun)try_orbit(moon, earth)try_orbit(moon, sun)Step by step, in words
- Three objects: a star, a planet and a satellite, each with its own coordinates and mass.
- A planet around a star: the rule allows it, and Earth sets off on its orbit.
- A satellite around a planet: allowed. The Moon orbits Earth and moves with it.
- A satellite around a star: orbit throws NotValidSpaceObjectError, and the except in try_orbit prints why.
Earth passes the star's check and is added to the objects that orbit it. The Moon passes Earth's check. When the Moon tries to orbit the Sun, the check for Planet fails. orbit throws, and the Moon stays around Earth.
The exception is caught in a small helper function. It tries the orbit and, if it is refused, says why:
static void tryOrbit(Orbiter orbiter, SpaceObject target) { try { orbiter.orbit(target); } catch (NotValidSpaceObjectException e) { System.out.println("Refused: " + e.getMessage()); }}static void TryOrbit(Orbiter orbiter, SpaceObject target){ try { orbiter.Orbit(target); } catch (NotValidSpaceObjectException e) { Console.WriteLine("Refused: " + e.Message); }}def try_orbit(orbiter, target): try: orbiter.orbit(target) except NotValidSpaceObjectError as e: print(f"Refused: {e}")function tryOrbit(orbiter, target) { try { orbiter.orbit(target); } catch (e) { if (!(e instanceof NotValidSpaceObjectError)) { throw e; } console.log(`Refused: ${e.message}`); }}Try it
How many orbit?
Mars tries to orbit Earth.
What will the program print?
def try_orbit(orbiter, target): try: orbiter.orbit(target) except NotValidSpaceObjectError as e: print(f"Refused: {e}")
sun = Star("Sun", 0, 0, 0, 333000)earth = Planet("Earth", 149.6, 0, 0, 1)mars = Planet("Mars", 227.9, 0, 0, 0.11)moon = Satellite("Moon", 150, 0, 0, 0.012)try_orbit(earth, sun)try_orbit(mars, earth)try_orbit(moon, earth)print(f"Around the Sun: {len(sun.get_orbiters())}")print(f"Around Earth: {len(earth.get_orbiters())}")Mars fails the check for Star and is not added anywhere. The helper function catches the refusal, so the program goes on with the Moon. Only Earth orbits the Sun, and only the Moon orbits Earth.
The idea
A ship that flies
Ships do not orbit. They fly from object to object. That is a different contract:
interface Fly { void flyTo(SpaceObject destination) throws NotValidSpaceObjectException;}interface Fly{ void FlyTo(SpaceObject destination);}class Fly(ABC): @abstractmethod def fly_to(self, destination): pass// The Fly contract: the object has a method flyTo(destination) that moves it to destination// or throws NotValidSpaceObjectError.SpaceShip extends SpaceObject and fulfills Fly. It cannot fly to a star. It flies to anything else and takes the coordinates of the target:
@Overridepublic void flyTo(SpaceObject destination) throws NotValidSpaceObjectException { if (destination instanceof Star) { throw new NotValidSpaceObjectException(getName() + " cannot fly to " + destination.getName() + ": the star would burn it"); } moveTo(destination.getCoordinateX(), destination.getCoordinateY(), destination.getCoordinateZ()); System.out.println(getName() + " arrived at " + destination.getName());}public void FlyTo(SpaceObject destination){ if (destination is Star) { throw new NotValidSpaceObjectException(Name + " cannot fly to " + destination.Name + ": the star would burn it"); } MoveTo(destination.CoordinateX, destination.CoordinateY, destination.CoordinateZ); Console.WriteLine(Name + " arrived at " + destination.Name);}def fly_to(self, destination): if isinstance(destination, Star): raise NotValidSpaceObjectError(f"{self.name} cannot fly to {destination.name}: the star would burn it") self._move_to(destination.coordinate_x, destination.coordinate_y, destination.coordinate_z) print(f"{self.name} arrived at {destination.name}")flyTo(destination) { if (destination instanceof Star) { throw new NotValidSpaceObjectError(`${this.name} cannot fly to ${destination.name}: the star would burn it`); } this._moveTo(destination.coordinateX, destination.coordinateY, destination.coordinateZ); console.log(`${this.name} arrived at ${destination.name}`);}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("Sun", 0, 0, 0, 333000);try { ship.flyTo(mars); ship.displayInfo(); ship.flyTo(sun); ship.displayInfo();} catch (NotValidSpaceObjectException e) { System.out.println("Refused: " + e.getMessage());}Wanderer arrived at MarsShip Wanderer: (227.9, 0.0, 0.0), mass 0.00Refused: Wanderer cannot fly to Sun: the star would burn itSpaceShip ship = new SpaceShip("Wanderer", 149.6, 0, 0, 0);Planet mars = new Planet("Mars", 227.9, 0, 0, 0.11);Star sun = new Star("Sun", 0, 0, 0, 333000);try{ ship.FlyTo(mars); ship.DisplayInfo(); ship.FlyTo(sun); ship.DisplayInfo();}catch (NotValidSpaceObjectException e){ Console.WriteLine("Refused: " + e.Message);}Wanderer arrived at MarsShip Wanderer: (227.9, 0.0, 0.0), mass 0.00Refused: Wanderer cannot fly to Sun: the star would burn itship = SpaceShip("Wanderer", 149.6, 0, 0, 0)mars = Planet("Mars", 227.9, 0, 0, 0.11)sun = Star("Sun", 0, 0, 0, 333000)try: ship.fly_to(mars) ship.display_info() ship.fly_to(sun) ship.display_info()except NotValidSpaceObjectError as e: print(f"Refused: {e}")Wanderer arrived at MarsShip Wanderer: (227.9, 0.0, 0.0), mass 0.00Refused: Wanderer cannot fly to Sun: the star would burn itconst ship = new SpaceShip("Wanderer", 149.6, 0, 0, 0);const mars = new Planet("Mars", 227.9, 0, 0, 0.11);const sun = new Star("Sun", 0, 0, 0, 333000);try { ship.flyTo(mars); ship.displayInfo(); ship.flyTo(sun); ship.displayInfo();} catch (e) { if (!(e instanceof NotValidSpaceObjectError)) { throw e; } console.log(`Refused: ${e.message}`);}Wanderer arrived at MarsShip Wanderer: (227.9, 0.0, 0.0), mass 0.00Refused: Wanderer cannot fly to Sun: the star would burn itThe second display_info() does not run: the exception jumps to except.
More precisely, if you are curious
class Probe(SpaceObject, Orbiter, Fly).Try it
Find the bug
The Moon orbits the Sun, and it should not. On which line in the satellite's orbit is the bug?
Pick the line with the bug.
OrbitableSpaceObject is the parent of both Planet and Star. The check lets both through. It should be isinstance(target, Planet): a satellite orbits only a planet.
Exercise
Space in code
Solve the tasks in your editor, with the classes from the lesson.
1. The solar system. Describe the eight planets with three lists: names, distances from the Sun (millions of km) and masses (Earth masses). In a loop, create each planet at x = its distance and make it orbit the Sun. Then show all the objects that orbit it.
| Planet | Distance | Mass |
|---|---|---|
| Mercury | 57.9 | 0.06 |
| Venus | 108.2 | 0.82 |
| Earth | 149.6 | 1 |
| Mars | 227.9 | 0.11 |
| Jupiter | 778.5 | 317.8 |
| Saturn | 1432 | 95.2 |
| Uranus | 2867 | 14.5 |
| Neptune | 4515 | 17.1 |
Mercury orbits SunVenus orbits SunEarth orbits SunMars orbits SunJupiter orbits SunSaturn orbits SunUranus orbits SunNeptune orbits Sun8 planets orbit Sun:Planet Mercury: (57.9, 0.0, 0.0), mass 0.06Planet Venus: (108.2, 0.0, 0.0), mass 0.82Planet Earth: (149.6, 0.0, 0.0), mass 1.00Planet Mars: (227.9, 0.0, 0.0), mass 0.11Planet Jupiter: (778.5, 0.0, 0.0), mass 317.80Planet Saturn: (1432.0, 0.0, 0.0), mass 95.20Planet Uranus: (2867.0, 0.0, 0.0), mass 14.50Planet Neptune: (4515.0, 0.0, 0.0), mass 17.102. Distance. Write def distance(a, b) with the Pythagorean theorem in three dimensions: the square root of the sum of the squares of the differences in x, y and z. Earth is at (149.6, 0, 0), Mars is at (0, 227.9, 0), the Moon is at (149.6, 0.4, 0), and the Sun is at the origin.
Earth to Mars: 272.6 million kmEarth to Moon: 0.4 million kmSun to Mars: 227.9 million km3. The whole tree. Change display_info so that the star also shows its planets and each planet shows its satellites, indented by two spaces per level. Hint: a parameter for the indent with a default value, indent="", so the call also works with no argument. OrbitableSpaceObject overrides the method and calls the parent. Earth and Mars orbit the Sun, the Moon orbits Earth, and Phobos orbits Mars.
Earth orbits SunMars orbits SunMoon orbits EarthPhobos orbits MarsStar Sun: (0.0, 0.0, 0.0), mass 333000.00 Planet Earth: (149.6, 0.0, 0.0), mass 1.00 Satellite Moon: (150.0, 0.0, 0.0), mass 0.01 Planet Mars: (227.9, 0.0, 0.0), mass 0.11 Satellite Phobos: (227.9, 0.0, 0.0), mass 0.00More tasks
- Added twice. Right now Earth can be added twice to the objects that orbit the Sun. Make a second try throw
NotValidSpaceObjectError. - A full star. Add 11 planets to one star and catch the exception at the eleventh.
- A probe. Make a
Probethat both orbits and flies (see "More precisely" under the ships).
Test