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.-
moveToisprotected: objects are not moved from outside, but a ship that is a subclass will be able to move.
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 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 with room for 10 and a counter of the places that are taken. A new one goes into the place whose number is the counter, then the counter grows.
- The array cannot grow. If it fills up,
addOrbiterthrows the built-in exception for "the wrong moment". getOrbiters()returns a new array with exactly the places that are taken, without the empty ones.
A list that grows on its own is handier. It comes in "Collections", and then the array and the counter become one line.
More precisely, if you are curious
getOrbiters()returns a copy, not the array itself. Otherwise code outside could write into the array, and the counter would lie.StarextendsOrbitableSpaceObject, notSpaceObjectdirectly. That way the star gets both the orbiting objects and everything fromSpaceObject.
The 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.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 Star starchecks the type and right away gives you a variable of typeStar.- 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
NotValidSpaceObjectException extends Exception, so it is checked. That is why throws is already in the interface: an implementation cannot throw a checked exception that the contract does not allow. Anyone who calls orbit has to catch it or declare it.
See it
What orbits what
Star sun = new Star("Sun", 0, 0, 0, 333000);Planet earth = new Planet("Earth", 149.6, 0, 0, 1);Satellite 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 NotValidSpaceObjectException, 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 method. 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?
static void tryOrbit(Orbiter orbiter, SpaceObject target) { try { orbiter.orbit(target); } catch (NotValidSpaceObjectException e) { System.out.println("Refused: " + e.getMessage()); }}
Star sun = new Star("Sun", 0, 0, 0, 333000);Planet earth = new Planet("Earth", 149.6, 0, 0, 1);Planet mars = new Planet("Mars", 227.9, 0, 0, 0.11);Satellite moon = new Satellite("Moon", 150, 0, 0, 0.012);tryOrbit(earth, sun);tryOrbit(mars, earth);tryOrbit(moon, earth);System.out.println("Around the Sun: " + sun.getOrbiters().length);System.out.println("Around Earth: " + earth.getOrbiters().length);Mars fails the check for Star and is not added anywhere. The helper method 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 implements 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 displayInfo() does not run: the exception jumps to catch.
More precisely, if you are curious
class Probe extends SpaceObject implements 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 target instanceof Planet 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 static double distance(SpaceObject a, SpaceObject 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 method with a parameter for the indent, which OrbitableSpaceObject overrides. 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
NotValidSpaceObjectException. - 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