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.
  • displayInfo() is shared by all of them. The kind (getKind()) is abstract: each subclass says for itself what it is.
  • moveTo is protected: 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";
}
}

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();
}
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 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, addOrbiter throws 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.
  • Star extends OrbitableSpaceObject, not SpaceObject directly. That way the star gets both the orbiting objects and everything from SpaceObject.

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

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");
}
}
  • target instanceof Star star checks the type and right away gives you a variable of type Star.
  • 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

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

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);
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 implements 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 displayInfo() does not run: the exception jumps to catch.

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

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

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

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 NotValidSpaceObjectException.
  • 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. Why is getKind() an abstract method, while displayInfo() is not?
2. Why does OrbitableSpaceObject keep a counter as well as the array?
3. A satellite tries moon.orbit(sun). What happens?
4. NotValidSpaceObjectException extends Exception. Why does the Orbiter interface say throws NotValidSpaceObjectException?

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