Skip to content
wannacode

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;
}
}
  • 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_to starts 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 fields self._name and 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")
Output
TypeError: Comet has no get_kind and cannot become an object

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

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();
}
Output
Star 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

The 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;
}
}
  • 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: CAPACITY is on the class, shared by all objects of it. If the room fills up, add_orbiter throws RuntimeError: 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 with append and get around the check for room.
  • Star extends OrbitableSpaceObject, not SpaceObject directly. That way the star gets both the orbiting objects and everything from SpaceObject.

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()))
Output
Earth orbits Sun
2 1

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

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);
}
}

Here is how the planet enforces it:

@Override
public 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");
}
}
  • isinstance(target, Star) checks the type. After the check you use target as a star: you do not need a new variable.
  • If the check fails, the exception stops the method. The planet is not added anywhere.
  • Satellite has the same orbit, except that it checks for Planet.
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
  1. Three objects: a star, a planet and a satellite, each with its own coordinates and mass.
  2. A planet around a star: the rule allows it, and Earth sets off on its orbit.
  3. A satellite around a planet: allowed. The Moon orbits Earth and moves with it.
  4. 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());
}
}

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())}")
Answer

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

SpaceShip extends SpaceObject and fulfills Fly. It cannot fly to a star. It flies to anything else and takes the coordinates of the target:

@Override
public 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());
}
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());
}
Output
Wanderer arrived at Mars
Ship Wanderer: (227.9, 0.0, 0.0), mass 0.00
Refused: Wanderer cannot fly to Sun: the star would burn it

The second display_info() does not run: the exception jumps to except.

More precisely, if you are curious
One class can extend several contracts. A scouting satellite that can also fly would be 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.

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
Output
Mercury orbits Sun
Venus orbits Sun
Earth orbits Sun
Mars orbits Sun
Jupiter orbits Sun
Saturn orbits Sun
Uranus orbits Sun
Neptune orbits Sun
8 planets orbit Sun:
Planet Mercury: (57.9, 0.0, 0.0), mass 0.06
Planet Venus: (108.2, 0.0, 0.0), mass 0.82
Planet Earth: (149.6, 0.0, 0.0), mass 1.00
Planet Mars: (227.9, 0.0, 0.0), mass 0.11
Planet Jupiter: (778.5, 0.0, 0.0), mass 317.80
Planet Saturn: (1432.0, 0.0, 0.0), mass 95.20
Planet Uranus: (2867.0, 0.0, 0.0), mass 14.50
Planet Neptune: (4515.0, 0.0, 0.0), mass 17.10

2. 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.

Output
Earth to Mars: 272.6 million km
Earth to Moon: 0.4 million km
Sun to Mars: 227.9 million km

3. 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.

Output
Earth orbits Sun
Mars orbits Sun
Moon orbits Earth
Phobos orbits Mars
Star 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.00

More 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 Probe that both orbits and flies (see "More precisely" under the ships).

Test

Check yourself

1. Comet extends SpaceObject but does not write get_kind. What happens at Comet("Halley", 0, 0, 0, 0)?
2. Why does get_orbiters() return a copy of the list and not the list itself?
3. A satellite tries moon.orbit(sun). What happens?
4. Why is Orbiter in Python an abstract class with just one abstract method?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·