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.
displayInfo()is shared by all of them. The kind (getKind()) is abstract: each subclass says for itself what it is.-
#nameand the other fields with#are truly hidden: you cannot reach them from outside, so you read them throughget name(). But#is not visible in subclasses either, and JavaScript has noprotected. That is why_moveTohas an underscore: a convention for "only for subclasses" that JavaScript does not enforce.
JavaScript has no abstract classes or methods. The closest thing is for getKind() in the parent to throw an error: a subclass that does not write it finds out at the first call. An object of such a class is created without any trouble:
class Comet extends SpaceObject {}
const comet = new Comet("Halley", 0, 0, 0, 0);try { comet.displayInfo();} catch (e) { console.log(e.message);}Comet must write getKind()The 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: new 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 array 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 an array that grows on its own. You do not need a counter:
this.#orbiters.lengthsays 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,addOrbiterthrows a plainError: JavaScript has no separate exception for the wrong moment. getOrbiters()returns a copy of the array.
More precisely, if you are curious
getOrbiters()returns a copy, not the array itself. Otherwise code outside could add withpushand 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.
const sun = new Star("Sun", 0, 0, 0, 333000);new Planet("Earth", 149.6, 0, 0, 1).orbit(sun);const copy = sun.getOrbiters();copy.push(new Planet("Mars", 227.9, 0, 0, 0.11));console.log(copy.length, sun.getOrbiters().length);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.JavaScript has no interfaces. The contract is an agreement: every object that can orbit has a method orbit(target). The code that uses it just calls it, without asking what the object is. This is called duck typing: "if it walks like a duck and quacks like a duck, it is a duck".
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`); }}target instanceof Starchecks 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 JavaScript 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 (TypeError).
See it
What orbits what
const sun = new Star("Sun", 0, 0, 0, 333000);const earth = new Planet("Earth", 149.6, 0, 0, 1);const moon = new Satellite("Moon", 150, 0, 0, 0.012);tryOrbit(earth, sun);tryOrbit(moon, earth);tryOrbit(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 catch in tryOrbit 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}`); }}catch in JavaScript has no type in the parentheses: it catches every error, even one from a typo in the code. That is why tryOrbit checks with instanceof. It prints a refused orbit and throws any other error on with throw e.
Try it
How many orbit?
Mars tries to orbit Earth.
What will the program print?
function tryOrbit(orbiter, target) { try { orbiter.orbit(target); } catch (e) { if (!(e instanceof NotValidSpaceObjectError)) { throw e; } console.log(`Refused: ${e.message}`); }}
const sun = new Star("Sun", 0, 0, 0, 333000);const earth = new Planet("Earth", 149.6, 0, 0, 1);const mars = new Planet("Mars", 227.9, 0, 0, 0.11);const moon = new Satellite("Moon", 150, 0, 0, 0.012);tryOrbit(earth, sun);tryOrbit(mars, earth);tryOrbit(moon, earth);console.log(`Around the Sun: ${sun.getOrbiters().length}`);console.log(`Around Earth: ${earth.getOrbiters().length}`);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 has a method flyTo. 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 displayInfo() does not run: the exception jumps to catch.
More precisely, if you are curious
extends takes only one parent. A scouting satellite that can also fly is class Probe extends SpaceObject with both methods, orbit and flyTo: for duck typing that is enough.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 target instanceof 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 arrays: 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 function 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 displayInfo 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