Skip to content
wannacode

Epic Cosmos 2: resources, buildings, ships

Problem

The world is ready, but nothing happens

From the last lesson you have stars, planets and satellites. But the game does not run yet: there is nothing to build with and nothing to wait for.

The economy of Epic Cosmos:

  • Every planet has four resources. Every turn they are mined, each at its own rate: metal 1.0, gas 0.5, crystal 0.2, uranium 0.1 per turn.
  • Buildings cost resources and time. Mines and the refinery double the rate of their resource at every level. The research lab and the shipyard unlock other things.
  • Ships fly. The scout explores the area and reports what each planet has.
  • The game is played in turns. Points come from buildings, ships and discoveries. The game ends at a set number of points or turns.

If there are not enough resources, building has to refuse and say what is missing. That is a job for your own exception.

The idea

The resource

A resource has an amount (amount) and a harvest rate (harvestRate). Every turn, Harvest() adds the rate to the amount. A mine calls Boost(), and the rate doubles.

abstract class Resource {
private double amount;
private double harvestRate;
public Resource(double amount, double harvestRate) {
this.amount = amount;
this.harvestRate = harvestRate;
}
public abstract String getName();
public double getAmount() {
return amount;
}
public double getHarvestRate() {
return harvestRate;
}
public void harvest() {
amount = round(amount + harvestRate);
}
public void boost() {
harvestRate = harvestRate * 2;
}
public void add(double quantity) {
amount = round(amount + quantity);
}
public void spend(double quantity) {
amount = round(amount - quantity);
}
private static double round(double value) {
return Math.round(value * 10) / 10.0;
}
}

The four kinds differ only in name and rate:

class Metal extends Resource {
public Metal(double amount) {
super(amount, 1.0);
}
@Override
public String getName() {
return "metal";
}
}

Gas has 0.5, Crystal 0.2, Uranium 0.1.

More precisely, if you are curious

double cannot store 0.1 exactly. If you add 0.1 ten times, you get 0.9999999999999999, not 1.0. Then the check "do you have 1 uranium" would say "no". That is why Round rounds every new amount to tenths. Real games often keep amounts as whole numbers, for example in tenths of a unit, exactly to avoid surprises like this.

See it

The planet's resources

Planet from the last lesson gets four resources and a method that mines them all at once:

private Resource metal = new Metal(0);
private Resource gas = new Gas(0);
private Resource crystal = new Crystal(0);
private Resource uranium = new Uranium(0);
public Resource getMetal() {
return metal;
}
public Resource getGas() {
return gas;
}
public Resource getCrystal() {
return crystal;
}
public Resource getUranium() {
return uranium;
}
public void harvestAll() {
metal.harvest();
gas.harvest();
crystal.harvest();
uranium.harvest();
}
public String resourcesText() {
return String.format("metal %.1f, gas %.1f, crystal %.1f, uranium %.1f", metal.getAmount(), gas.getAmount(), crystal.getAmount(), uranium.getAmount());
}
public void printResources() {
System.out.println(getName() + ": " + resourcesText());
}

Five turns of mining, then doubling the metal and one more turn:

Planet home = new Planet("Nova Terra", 100, 0, 0, 1);
for (int turn = 1; turn <= 5; turn++) {
home.harvestAll();
}
home.printResources();
home.getMetal().boost();
home.harvestAll();
home.printResources();
Output
Nova Terra: metal 5.0, gas 2.5, crystal 1.0, uranium 0.5
Nova Terra: metal 7.0, gas 3.0, crystal 1.2, uranium 0.6

Try it

How much gas?

What will the program print?

Resource gas = new Gas(0);
gas.Harvest();
gas.Boost();
gas.Harvest();
gas.Boost();
gas.Harvest();
Console.WriteLine(gas.Amount + " " + gas.GetName() + ", rate " + gas.HarvestRate);
Answer

The idea

Building costs something

When there are not enough resources, building has to refuse. The caller wants to know what is missing and how much, so the exception is your own and carries the number:

class NotEnoughResourcesException extends Exception {
private double missing;
public NotEnoughResourcesException(String purpose, String resource, double missing) {
super(String.format("%s needs %.1f more %s", purpose, missing, resource));
this.missing = missing;
}
public double getMissing() {
return missing;
}
}

Paying is in Planet. It first checks the four resources, then spends:

public void spend(String purpose, double metalNeeded, double crystalNeeded, double gasNeeded, double uraniumNeeded) throws NotEnoughResourcesException {
check(purpose, metal, metalNeeded);
check(purpose, crystal, crystalNeeded);
check(purpose, gas, gasNeeded);
check(purpose, uranium, uraniumNeeded);
metal.spend(metalNeeded);
crystal.spend(crystalNeeded);
gas.spend(gasNeeded);
uranium.spend(uraniumNeeded);
}
private void check(String purpose, Resource resource, double needed) throws NotEnoughResourcesException {
if (resource.getAmount() < needed) {
throw new NotEnoughResourcesException(purpose, resource.getName(), needed - resource.getAmount());
}
}

Try it

Find the bug

The colony has 20 metal and no crystal. The Research lab costs 12 metal and 5 crystal. Building is refused, but the metal has dropped to 8. On which line is the problem?

Pick the line with the bug.

The idea

The building

Every building has a name, a level, a construction time and a price in the four resources. Building and upgrading are the same for all buildings, so they are in the abstract BaseBuilding:

public String getRequiredBuilding() {
return null;
}
public void construct(Planet target) throws NotEnoughResourcesException {
if (target.findBuilding(name) != null) {
throw new IllegalStateException(name + " is already built");
}
String required = getRequiredBuilding();
if (required != null && target.findBuilding(required) == null) {
throw new IllegalStateException(name + " needs " + required + " first");
}
target.spend(name, metalRequired, crystalRequired, gasRequired, uraniumRequired);
planet = target;
level = 1;
target.addBuilding(this);
}
public void upgrade() throws NotEnoughResourcesException {
if (planet == null) {
throw new IllegalStateException(name + " is not built yet");
}
double factor = Math.pow(2, level);
planet.spend(name + " level " + (level + 1), metalRequired * factor, crystalRequired * factor, gasRequired * factor, uraniumRequired * factor);
level++;
}
public abstract void applyEffect();
  • Construct refuses if the building is already there or if the building it depends on is missing. These refusals use the built-in exception for the wrong moment. Then it pays and records the building on the planet.
  • Upgrade costs twice as much at every level: level 2 is 2 times the price, level 3 is 4 times.
  • ApplyEffect is abstract: each building knows for itself what it does when it is ready.
More precisely, if you are curious

GetRequiredBuilding() returns null: by default a building does not depend on anything. A subclass that depends on something overrides it.

The idea

Mines and dependencies

Mines differ only in which resource they speed up. What they share is in Mine:

abstract class Mine extends BaseBuilding {
public Mine(String name, int constructionTime, double metalRequired, double crystalRequired) {
super(name, 0, constructionTime, metalRequired, crystalRequired, 0, 0);
}
protected abstract Resource resourceOf(Planet planet);
@Override
public void applyEffect() {
resourceOf(getPlanet()).boost();
}
}
class MetalMine extends Mine {
public MetalMine() {
super("Metal mine", 1, 6, 2);
}
@Override
protected Resource resourceOf(Planet planet) {
return planet.getMetal();
}
}

The Metal mine and the Crystal mine can be built right away. The Research lab needs a Crystal mine, and the Gas refinery and the Shipyard need a Research lab:

class ResearchLab extends BaseBuilding {
public ResearchLab() {
super("Research lab", 0, 2, 12, 4, 0, 0);
}
@Override
public String getRequiredBuilding() {
return "Crystal mine";
}
@Override
public void applyEffect() {
// It unlocks other buildings. The resources do not change.
}
}

A plan of three buildings with 10 metal and 3 crystal. The Research lab is refused because of the dependency, and the Crystal mine because of the metal:

Planet home = new Planet("Nova Terra", 100, 0, 0, 1);
home.getMetal().add(10);
home.getCrystal().add(3);
BaseBuilding[] plan = { new ResearchLab(), new MetalMine(), new CrystalMine() };
for (BaseBuilding building : plan) {
try {
building.construct(home);
building.applyEffect();
System.out.println(building.getName() + " is ready.");
} catch (NotEnoughResourcesException e) {
System.out.println("Refused: " + e.getMessage());
} catch (IllegalStateException e) {
System.out.println("Refused: " + e.getMessage());
}
}
home.printResources();
System.out.println("Metal harvest rate: " + home.getMetal().getHarvestRate());
Output
Refused: Research lab needs Crystal mine first
Metal mine is ready.
Refused: Crystal mine needs 4.0 more metal
Nova Terra: metal 4.0, gas 0.0, crystal 1.0, uranium 0.0
Metal harvest rate: 2.0

Planet home = new Planet("Nova Terra", 100, 0, 0, 1);
home.Metal.Add(10);
home.Crystal.Add(3);
BaseBuilding[] plan = { new ResearchLab(), new MetalMine(), new CrystalMine() };
foreach (BaseBuilding building in plan)
{
try
{
building.Construct(home);
building.ApplyEffect();
Console.WriteLine(building.Name + " is ready.");
}
catch (NotEnoughResourcesException e)
{
Console.WriteLine("Refused: " + e.Message);
}
catch (InvalidOperationException e)
{
Console.WriteLine("Refused: " + e.Message);
}
}
home.PrintResources();
Console.WriteLine("Metal harvest rate: " + home.Metal.HarvestRate);
Step by step, in words
  1. Nova Terra has 10 metal and 3 crystal. Metal is mined at 1.0 per turn.
  2. The plan is an array of 3 buildings. Each one has a price in metal and crystal.
  3. Research lab needs a Crystal mine, and there is none yet. Construct() throws InvalidOperationException before it spends anything.
  4. Metal mine costs 6 metal and 2 crystal. That is enough: 4 metal and 1 crystal are left. ApplyEffect() doubles the metal rate.
  5. Crystal mine needs 8 metal, and there is 4. The check throws NotEnoughResourcesException, and nothing is spent.
  6. The refusals spent nothing. Only the price of the building that was built is gone, and the metal rate is 2.

The idea

Ships and the scout

SpaceShip from the last lesson gets a type, a speed, a cargo capacity and a weapon power. It starts at the home planet and flies with FlyTo, as before. The scout is a ship with sensors: Explore checks which planets are in its range and reports their resources.

class Scout extends SpaceShip {
private double sensorRange;
public Scout(String name, Planet home) {
super(name, "Scout", 40, 0, 0, home);
sensorRange = 100;
}
public int explore(Planet[] planets) {
int found = 0;
for (Planet planet : planets) {
double dx = planet.getCoordinateX() - getCoordinateX();
double dy = planet.getCoordinateY() - getCoordinateY();
double dz = planet.getCoordinateZ() - getCoordinateZ();
if (Math.sqrt(dx * dx + dy * dy + dz * dz) <= sensorRange) {
reportResources(planet);
found++;
}
}
return found;
}
public void reportResources(Planet planet) {
System.out.println(getName() + " found " + planet.getName() + ": " + planet.resourcesText());
}
}

The range is measured with the distance from exercise 2 in the last lesson. The method returns how many planets it found, so the game can give points for them.

See it

The whole game

Game keeps the home planet, the other planets, the turn and the points. The commands come from the player, one per line. Buildings take turns, and during every turn the planet mines:

public void handle(String command) {
int space = command.indexOf(' ');
String verb = space == -1 ? command : command.substring(0, space);
String rest = space == -1 ? "" : command.substring(space + 1);
try {
if (verb.equals("build")) {
build(createBuilding(rest));
} else if (verb.equals("upgrade")) {
upgrade(rest);
} else if (verb.equals("scout")) {
buildScout();
} else if (verb.equals("explore")) {
explore();
} else if (verb.equals("wait")) {
waitTurns(1);
} else {
System.out.println("Unknown command: " + command);
}
} catch (NotEnoughResourcesException e) {
System.out.println("Refused: " + e.getMessage());
} catch (IllegalStateException e) {
System.out.println("Refused: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println("Refused: " + e.getMessage());
}
}

Every building brings 10 points, an upgrade 10 times the new level, the scout 15, and every planet found 5. The game ends at 80 points or 40 turns. The loop in Main:

Game game = new Game(home, others, 80, 40);
Scanner in = new Scanner(System.in);
while (!game.isOver() && in.hasNextLine()) {
String command = in.nextLine();
System.out.println("> " + command);
game.handle(command);
System.out.println(" " + game.status());
}
System.out.println("End: " + game.getPoints() + " points in " + game.getTurn() + " turns.");
Output
> build research lab
Refused: Research lab needs Crystal mine first
turn 0, points 0 | metal 40.0, gas 0.0, crystal 15.0, uranium 0.0
> build metal mine
Metal mine is ready.
turn 1, points 10 | metal 35.0, gas 0.5, crystal 13.2, uranium 0.1
> build crystal mine
Crystal mine is ready.
turn 2, points 20 | metal 29.0, gas 1.0, crystal 12.4, uranium 0.2
> build research lab
Research lab is ready.
turn 4, points 30 | metal 21.0, gas 2.0, crystal 9.2, uranium 0.4
> upgrade metal mine
Metal mine is at level 2.
turn 5, points 50 | metal 11.0, gas 2.5, crystal 5.6, uranium 0.5
> build shipyard
Refused: Shipyard needs 4.0 more metal
turn 5, points 50 | metal 11.0, gas 2.5, crystal 5.6, uranium 0.5
> wait
turn 6, points 50 | metal 15.0, gas 3.0, crystal 6.0, uranium 0.6
> build shipyard
Shipyard is ready.
turn 9, points 60 | metal 12.0, gas 2.5, crystal 3.2, uranium 0.9
> scout
Refused: scout needs 0.1 more uranium
turn 9, points 60 | metal 12.0, gas 2.5, crystal 3.2, uranium 0.9
> wait
turn 10, points 60 | metal 16.0, gas 3.0, crystal 3.6, uranium 1.0
> scout
Scout-1 is ready.
turn 12, points 75 | metal 14.0, gas 2.0, crystal 1.4, uranium 0.2
> explore
Scout-1 found Cobalt: metal 500.0, gas 0.0, crystal 40.0, uranium 0.0
Scout-1 found Helia: metal 0.0, gas 300.0, crystal 0.0, uranium 20.0
Planets found: 2
turn 13, points 85 | metal 18.0, gas 2.5, crystal 1.8, uranium 0.3
End: 85 points in 13 turns.

With this input:

Input
build research lab
build metal mine
build crystal mine
build research lab
upgrade metal mine
build shipyard
wait
build shipyard
scout
wait
scout
explore
More precisely, if you are curious
  • Refusals do not move the turn: the building was not started, so the player can try something else right away.
  • Research (the technologies from the rules) is in the old assignment only as a plan: a class Research with a name, a level and a price, with methods for researching and leveling up. Research and conflicts (pirates, attacks with WeaponPower) are the next step if you want to go on by yourself.

Exercise

The colony grows

Solve the tasks in your editor, with the classes from the lesson.

1. 100 metal as fast as possible. Nova Terra starts with 40 metal and 15 crystal. Read the plan from the input, one command per line, run it, and then wait until the metal reaches 100. Print on which turn that happened. With this plan it is on turn 21. Find a faster one.

Input
build metal mine
upgrade metal mine
Output
Metal mine is ready.
Metal mine is at level 2.
100 metal on turn 21: turn 21, points 30 | metal 101.0, gas 10.5, crystal 13.2, uranium 2.1

2. A cargo ship. Write CargoShip, a subclass of SpaceShip, with a cargo capacity of 50. HarvestResource(Planet planet) loads as much of the planet's metal as fits, but not more than there is. Unload(Planet planet) unloads it. Cobalt has 120 metal. The ship Mule makes three trips from Nova Terra to Cobalt and back.

Output
Mule arrived at Cobalt
Mule loaded 50.0 metal from Cobalt
Mule arrived at Nova Terra
Mule unloaded 50.0 metal at Nova Terra
Mule arrived at Cobalt
Mule loaded 50.0 metal from Cobalt
Mule arrived at Nova Terra
Mule unloaded 50.0 metal at Nova Terra
Mule arrived at Cobalt
Mule loaded 20.0 metal from Cobalt
Mule arrived at Nova Terra
Mule unloaded 20.0 metal at Nova Terra
Nova Terra: metal 120.0, gas 0.0, crystal 0.0, uranium 0.0
Cobalt: metal 0.0, gas 0.0, crystal 0.0, uranium 0.0

3. A 10-turn simulation. No player: the game has a list of buildings (Metal mine, Crystal mine, Research lab, Gas refinery). At every step it tries to build the next one. If it cannot, it waits one turn. After every step it prints the state, until the turns reach 10.

Output
Metal mine is ready.
turn 1, points 10 | metal 35.0, gas 0.5, crystal 13.2, uranium 0.1
Crystal mine is ready.
turn 2, points 20 | metal 29.0, gas 1.0, crystal 12.4, uranium 0.2
Research lab is ready.
turn 4, points 30 | metal 21.0, gas 2.0, crystal 9.2, uranium 0.4
Gas refinery is ready.
turn 6, points 40 | metal 15.0, gas 3.0, crystal 7.0, uranium 0.6
turn 7, points 40 | metal 17.0, gas 4.0, crystal 7.4, uranium 0.7
turn 8, points 40 | metal 19.0, gas 5.0, crystal 7.8, uranium 0.8
turn 9, points 40 | metal 21.0, gas 6.0, crystal 8.2, uranium 0.9
turn 10, points 40 | metal 23.0, gas 7.0, crystal 8.6, uranium 1.0

More tasks

  • A uranium mine. Add a UraniumMine that depends on the Gas refinery.
  • Points for resources. Give 1 point for every 50 metal mined.
  • Pirates. Every 10 turns, pirates take 20% of the metal if the planet has no ship with a weapon power of at least 10.

Test

Check yourself

1. Metal is mined at 1.0 per turn. You build a Metal mine and upgrade it to level 3. How much metal per turn does the planet give?
2. Why does Spend first check all four resources and only then spend?
3. The Research lab needs a Crystal mine first. Why does this error use the built-in exception and not NotEnoughResourcesException?
4. When does the game in the lesson end?

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