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; }}abstract class Resource{ public double Amount { get; private set; } public double HarvestRate { get; private set; }
public Resource(double amount, double harvestRate) { Amount = amount; HarvestRate = harvestRate; }
public abstract string GetName();
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; }}class Resource(ABC): def __init__(self, amount, harvest_rate): self._amount = amount self._harvest_rate = harvest_rate
@abstractmethod def get_name(self): pass
@property def amount(self): return self._amount
@property def harvest_rate(self): return self._harvest_rate
def harvest(self): self._amount = round(self._amount + self._harvest_rate, 1)
def boost(self): self._harvest_rate = self._harvest_rate * 2
def add(self, quantity): self._amount = round(self._amount + quantity, 1)
def spend(self, quantity): self._amount = round(self._amount - quantity, 1)class Resource { #amount; #harvestRate;
constructor(amount, harvestRate) { this.#amount = amount; this.#harvestRate = harvestRate; }
getName() { throw new Error(`${this.constructor.name} must write getName()`); }
get amount() { return this.#amount; }
get harvestRate() { return this.#harvestRate; }
harvest() { this.#amount = Resource.#round(this.#amount + this.#harvestRate); }
boost() { this.#harvestRate = this.#harvestRate * 2; }
add(quantity) { this.#amount = Resource.#round(this.#amount + quantity); }
spend(quantity) { this.#amount = Resource.#round(this.#amount - quantity); }
static #round(value) { return Math.round(value * 10) / 10; }}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"; }}class Metal : Resource{ public Metal(double amount) : base(amount, 1.0) { }
public override string GetName() { return "metal"; }}class Metal(Resource): def __init__(self, amount): super().__init__(amount, 1.0)
def get_name(self): return "metal"class Metal extends Resource { constructor(amount) { super(amount, 1.0); }
getName() { return "metal"; }}Gas has 0.5, Crystal 0.2, Uranium 0.1.
More precisely, if you are curious
number 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 Resource.#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.
let total = 0;for (let i = 0; i < 10; i++) { total += 0.1;}console.log(total);0.9999999999999999Math.round rounds only to a whole number, so #round multiplies by 10, rounds and divides. static #round is a hidden method of the class itself: you call it as Resource.#round(...), and it is not visible from outside.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());}public Resource Metal { get; } = new Metal(0);public Resource Gas { get; } = new Gas(0);public Resource Crystal { get; } = new Crystal(0);public Resource Uranium { get; } = new Uranium(0);
public void HarvestAll(){ Metal.Harvest(); Gas.Harvest(); Crystal.Harvest(); Uranium.Harvest();}
public string ResourcesText(){ return $"metal {Metal.Amount:F1}, gas {Gas.Amount:F1}, crystal {Crystal.Amount:F1}, uranium {Uranium.Amount:F1}";}
public void PrintResources(){ Console.WriteLine(Name + ": " + ResourcesText());}def __init__(self, name, coordinate_x, coordinate_y, coordinate_z, mass): super().__init__(name, coordinate_x, coordinate_y, coordinate_z, mass) self._metal = Metal(0) self._gas = Gas(0) self._crystal = Crystal(0) self._uranium = Uranium(0) self._buildings = [] # the buildings come later in the lesson
@propertydef metal(self): return self._metal
@propertydef gas(self): return self._gas
@propertydef crystal(self): return self._crystal
@propertydef uranium(self): return self._uranium
def harvest_all(self): self._metal.harvest() self._gas.harvest() self._crystal.harvest() self._uranium.harvest()
def resources_text(self): return f"metal {self._metal.amount:.1f}, gas {self._gas.amount:.1f}, crystal {self._crystal.amount:.1f}, uranium {self._uranium.amount:.1f}"
def print_resources(self): print(f"{self.name}: {self.resources_text()}")#metal = new Metal(0);#gas = new Gas(0);#crystal = new Crystal(0);#uranium = new Uranium(0);#buildings = []; // the buildings come later in the lesson
get metal() { return this.#metal;}
get gas() { return this.#gas;}
get crystal() { return this.#crystal;}
get uranium() { return this.#uranium;}
harvestAll() { this.#metal.harvest(); this.#gas.harvest(); this.#crystal.harvest(); this.#uranium.harvest();}
resourcesText() { return `metal ${this.#metal.amount.toFixed(1)}, gas ${this.#gas.amount.toFixed(1)}, crystal ${this.#crystal.amount.toFixed(1)}, uranium ${this.#uranium.amount.toFixed(1)}`;}
printResources() { console.log(`${this.name}: ${this.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();Nova Terra: metal 5.0, gas 2.5, crystal 1.0, uranium 0.5Nova Terra: metal 7.0, gas 3.0, crystal 1.2, uranium 0.6Planet home = new Planet("Nova Terra", 100, 0, 0, 1);for (int turn = 1; turn <= 5; turn++){ home.HarvestAll();}home.PrintResources();home.Metal.Boost();home.HarvestAll();home.PrintResources();Nova Terra: metal 5.0, gas 2.5, crystal 1.0, uranium 0.5Nova Terra: metal 7.0, gas 3.0, crystal 1.2, uranium 0.6home = Planet("Nova Terra", 100, 0, 0, 1)for turn in range(1, 6): home.harvest_all()home.print_resources()home.metal.boost()home.harvest_all()home.print_resources()Nova Terra: metal 5.0, gas 2.5, crystal 1.0, uranium 0.5Nova Terra: metal 7.0, gas 3.0, crystal 1.2, uranium 0.6const home = new Planet("Nova Terra", 100, 0, 0, 1);for (let turn = 1; turn <= 5; turn++) { home.harvestAll();}home.printResources();home.metal.boost();home.harvestAll();home.printResources();Nova Terra: metal 5.0, gas 2.5, crystal 1.0, uranium 0.5Nova Terra: metal 7.0, gas 3.0, crystal 1.2, uranium 0.6Try it
How much gas?
What will the program print?
const gas = new Gas(0);gas.harvest();gas.boost();gas.harvest();gas.boost();gas.harvest();console.log(`${gas.amount} ${gas.getName()}, rate ${gas.harvestRate}`);The first harvest is 0.5. Then the rate becomes 1.0, and the second harvest is 1.0, for 1.5 in total. Then the rate becomes 2.0, and the third harvest is 2.0, for 3.5 in total. JavaScript prints 2, not 2.0: in a template string, a whole number is shown without a decimal part.
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; }}class NotEnoughResourcesException : Exception{ public double Missing { get; }
public NotEnoughResourcesException(string purpose, string resource, double missing) : base($"{purpose} needs {missing:F1} more {resource}") { Missing = missing; }}class NotEnoughResourcesError(Exception): def __init__(self, purpose, resource, missing): super().__init__(f"{purpose} needs {missing:.1f} more {resource}") self.missing = missingclass NotEnoughResourcesError extends Error { constructor(purpose, resource, missing) { super(`${purpose} needs ${missing.toFixed(1)} more ${resource}`); this.name = "NotEnoughResourcesError"; this.missing = 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()); }}public void Spend(string purpose, double metalNeeded, double crystalNeeded, double gasNeeded, double uraniumNeeded){ 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){ if (resource.Amount < needed) { throw new NotEnoughResourcesException(purpose, resource.GetName(), needed - resource.Amount); }}def spend(self, purpose, metal_needed, crystal_needed, gas_needed, uranium_needed): self._check(purpose, self._metal, metal_needed) self._check(purpose, self._crystal, crystal_needed) self._check(purpose, self._gas, gas_needed) self._check(purpose, self._uranium, uranium_needed) self._metal.spend(metal_needed) self._crystal.spend(crystal_needed) self._gas.spend(gas_needed) self._uranium.spend(uranium_needed)
def _check(self, purpose, resource, needed): if resource.amount < needed: raise NotEnoughResourcesError(purpose, resource.get_name(), needed - resource.amount)spend(purpose, metalNeeded, crystalNeeded, gasNeeded, uraniumNeeded) { this.#check(purpose, this.#metal, metalNeeded); this.#check(purpose, this.#crystal, crystalNeeded); this.#check(purpose, this.#gas, gasNeeded); this.#check(purpose, this.#uranium, uraniumNeeded); this.#metal.spend(metalNeeded); this.#crystal.spend(crystalNeeded); this.#gas.spend(gasNeeded); this.#uranium.spend(uraniumNeeded);}
#check(purpose, resource, needed) { if (resource.amount < needed) { throw new NotEnoughResourcesError(purpose, resource.getName(), needed - resource.amount); }}#check is a hidden method: # in front of a method name means the same as in front of a field. You call it only from inside, as this.#check(...).
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 metal is spent right after its own check. Then the crystal check throws, but the metal is already spent, and the lab is not built. The exception stops the method, but it does not undo what was done before it. First all the checks, then the spending.
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();public virtual string GetRequiredBuilding(){ return null;}
public void Construct(Planet target){ if (target.FindBuilding(Name) != null) { throw new InvalidOperationException(Name + " is already built"); } string required = GetRequiredBuilding(); if (required != null && target.FindBuilding(required) == null) { throw new InvalidOperationException(Name + " needs " + required + " first"); } target.Spend(Name, MetalRequired, CrystalRequired, GasRequired, UraniumRequired); Planet = target; Level = 1; target.AddBuilding(this);}
public void Upgrade(){ if (Planet == null) { throw new InvalidOperationException(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();def get_required_building(self): return None
def construct(self, target): if target.find_building(self._name) is not None: raise RuntimeError(f"{self._name} is already built") required = self.get_required_building() if required is not None and target.find_building(required) is None: raise RuntimeError(f"{self._name} needs {required} first") target.spend(self._name, self._metal_required, self._crystal_required, self._gas_required, self._uranium_required) self._planet = target self._level = 1 target.add_building(self)
def upgrade(self): if self._planet is None: raise RuntimeError(f"{self._name} is not built yet") factor = 2 ** self._level self._planet.spend(f"{self._name} level {self._level + 1}", self._metal_required * factor, self._crystal_required * factor, self._gas_required * factor, self._uranium_required * factor) self._level += 1
@abstractmethoddef apply_effect(self): passgetRequiredBuilding() { return null;}
construct(target) { if (target.findBuilding(this.#name) !== null) { throw new Error(`${this.#name} is already built`); } const required = this.getRequiredBuilding(); if (required !== null && target.findBuilding(required) === null) { throw new Error(`${this.#name} needs ${required} first`); } target.spend(this.#name, this.#metalRequired, this.#crystalRequired, this.#gasRequired, this.#uraniumRequired); this.#planet = target; this.#level = 1; target.addBuilding(this);}
upgrade() { if (this.#planet === null) { throw new Error(`${this.#name} is not built yet`); } const factor = 2 ** this.#level; this.#planet.spend(`${this.#name} level ${this.#level + 1}`, this.#metalRequired * factor, this.#crystalRequired * factor, this.#gasRequired * factor, this.#uraniumRequired * factor); this.#level++;}
applyEffect() { throw new Error(`${this.constructor.name} must write applyEffect()`);}constructrefuses 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: in JavaScript that is a plainError. Then it pays and records the building on the planet.upgradecosts twice as much at every level: level 2 is 2 times the price, level 3 is 4 times.applyEffectis abstract: each building knows for itself what it does when it is ready. As withgetKind, in the parent it only throws an error.
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(); }}abstract class Mine : BaseBuilding{ public Mine(string name, int constructionTime, double metalRequired, double crystalRequired) : base(name, 0, constructionTime, metalRequired, crystalRequired, 0, 0) { }
protected abstract Resource ResourceOf(Planet planet);
public override void ApplyEffect() { ResourceOf(Planet).Boost(); }}class Mine(BaseBuilding): def __init__(self, name, construction_time, metal_required, crystal_required): super().__init__(name, 0, construction_time, metal_required, crystal_required, 0, 0)
@abstractmethod def _resource_of(self, planet): pass
def apply_effect(self): self._resource_of(self.planet).boost()class Mine extends BaseBuilding { constructor(name, constructionTime, metalRequired, crystalRequired) { super(name, 0, constructionTime, metalRequired, crystalRequired, 0, 0); }
_resourceOf(planet) { throw new Error(`${this.constructor.name} must write _resourceOf()`); }
applyEffect() { this._resourceOf(this.planet).boost(); }}class MetalMine extends Mine { public MetalMine() { super("Metal mine", 1, 6, 2); }
@Override protected Resource resourceOf(Planet planet) { return planet.getMetal(); }}class MetalMine : Mine{ public MetalMine() : base("Metal mine", 1, 6, 2) { }
protected override Resource ResourceOf(Planet planet) { return planet.Metal; }}class MetalMine(Mine): def __init__(self): super().__init__("Metal mine", 1, 6, 2)
def _resource_of(self, planet): return planet.metalclass MetalMine extends Mine { constructor() { super("Metal mine", 1, 6, 2); }
_resourceOf(planet) { return planet.metal; }}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. }}class ResearchLab : BaseBuilding{ public ResearchLab() : base("Research lab", 0, 2, 12, 4, 0, 0) { }
public override string GetRequiredBuilding() { return "Crystal mine"; }
public override void ApplyEffect() { // It unlocks other buildings. The resources do not change. }}class ResearchLab(BaseBuilding): def __init__(self): super().__init__("Research lab", 0, 2, 12, 4, 0, 0)
def get_required_building(self): return "Crystal mine"
def apply_effect(self): # It unlocks other buildings. The resources do not change. passclass ResearchLab extends BaseBuilding { constructor() { super("Research lab", 0, 2, 12, 4, 0, 0); }
getRequiredBuilding() { return "Crystal mine"; }
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());Refused: Research lab needs Crystal mine firstMetal mine is ready.Refused: Crystal mine needs 4.0 more metalNova Terra: metal 4.0, gas 0.0, crystal 1.0, uranium 0.0Metal harvest rate: 2.0Planet 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);Refused: Research lab needs Crystal mine firstMetal mine is ready.Refused: Crystal mine needs 4.0 more metalNova Terra: metal 4.0, gas 0.0, crystal 1.0, uranium 0.0Metal harvest rate: 2home = Planet("Nova Terra", 100, 0, 0, 1)home.metal.add(10)home.crystal.add(3)plan = [ResearchLab(), MetalMine(), CrystalMine()]for building in plan: try: building.construct(home) building.apply_effect() print(f"{building.name} is ready.") except NotEnoughResourcesError as e: print(f"Refused: {e}") except RuntimeError as e: print(f"Refused: {e}")home.print_resources()print(f"Metal harvest rate: {home.metal.harvest_rate}")Refused: Research lab needs Crystal mine firstMetal mine is ready.Refused: Crystal mine needs 4.0 more metalNova Terra: metal 4.0, gas 0.0, crystal 1.0, uranium 0.0Metal harvest rate: 2.0const home = new Planet("Nova Terra", 100, 0, 0, 1);home.metal.add(10);home.crystal.add(3);const plan = [new ResearchLab(), new MetalMine(), new CrystalMine()];for (const building of plan) { try { building.construct(home); building.applyEffect(); console.log(`${building.name} is ready.`); } catch (e) { console.log(`Refused: ${e.message}`); }}home.printResources();console.log(`Metal harvest rate: ${home.metal.harvestRate}`);Refused: Research lab needs Crystal mine firstMetal mine is ready.Refused: Crystal mine needs 4.0 more metalNova Terra: metal 4.0, gas 0.0, crystal 1.0, uranium 0.0Metal harvest rate: 2In JavaScript there is one catch for all errors. Here every error that reaches it is a refused build, so it prints the message, whatever it is. If other errors can happen along the way, check the type with instanceof and throw the rest on, like tryOrbit in the last lesson.
const home = new Planet("Nova Terra", 100, 0, 0, 1);home.metal.add(10);home.crystal.add(3);const plan = [new ResearchLab(), new MetalMine(), new CrystalMine()];for (const building of plan) { try { building.construct(home); building.applyEffect(); console.log(`${building.name} is ready.`); } catch (e) { console.log(`Refused: ${e.message}`); }}home.printResources();console.log(`Metal harvest rate: ${home.metal.harvestRate}`);Step by step, in words
- Nova Terra has 10 metal and 3 crystal. Metal is mined at 1.0 per turn.
- The plan is an array of 3 buildings. Each one has a price in metal and crystal.
- Research lab needs a Crystal mine, and there is none yet. construct() throws Error before it spends anything.
- Metal mine costs 6 metal and 2 crystal. That is enough: 4 metal and 1 crystal are left. applyEffect() doubles the metal rate.
- Crystal mine needs 8 metal, and there is 4. The check throws NotEnoughResourcesError, and nothing is spent.
- 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()); }}class Scout : SpaceShip{ private double sensorRange;
public Scout(string name, Planet home) : base(name, "Scout", 40, 0, 0, home) { sensorRange = 100; }
public int Explore(Planet[] planets) { int found = 0; foreach (Planet planet in planets) { double dx = planet.CoordinateX - CoordinateX; double dy = planet.CoordinateY - CoordinateY; double dz = planet.CoordinateZ - CoordinateZ; if (Math.Sqrt(dx * dx + dy * dy + dz * dz) <= sensorRange) { ReportResources(planet); found++; } } return found; }
public void ReportResources(Planet planet) { Console.WriteLine(Name + " found " + planet.Name + ": " + planet.ResourcesText()); }}class Scout(SpaceShip): def __init__(self, name, home): super().__init__(name, "Scout", 40, 0, 0, home) self._sensor_range = 100
def explore(self, planets): found = 0 for planet in planets: dx = planet.coordinate_x - self.coordinate_x dy = planet.coordinate_y - self.coordinate_y dz = planet.coordinate_z - self.coordinate_z if math.sqrt(dx * dx + dy * dy + dz * dz) <= self._sensor_range: self.report_resources(planet) found += 1 return found
def report_resources(self, planet): print(f"{self.name} found {planet.name}: {planet.resources_text()}")class Scout extends SpaceShip { #sensorRange;
constructor(name, home) { super(name, "Scout", 40, 0, 0, home); this.#sensorRange = 100; }
explore(planets) { let found = 0; for (const planet of planets) { const dx = planet.coordinateX - this.coordinateX; const dy = planet.coordinateY - this.coordinateY; const dz = planet.coordinateZ - this.coordinateZ; if (Math.sqrt(dx * dx + dy * dy + dz * dz) <= this.#sensorRange) { this.reportResources(planet); found++; } } return found; }
reportResources(planet) { console.log(`${this.name} found ${planet.name}: ${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()); }}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 == "build") { Build(CreateBuilding(rest)); } else if (verb == "upgrade") { Upgrade(rest); } else if (verb == "scout") { BuildScout(); } else if (verb == "explore") { Explore(); } else if (verb == "wait") { WaitTurns(1); } else { Console.WriteLine("Unknown command: " + command); } } catch (NotEnoughResourcesException e) { Console.WriteLine("Refused: " + e.Message); } catch (InvalidOperationException e) { Console.WriteLine("Refused: " + e.Message); } catch (ArgumentException e) { Console.WriteLine("Refused: " + e.Message); }}def handle(self, command): verb, _, rest = command.partition(" ") try: if verb == "build": self._build(self._create_building(rest)) elif verb == "upgrade": self._upgrade(rest) elif verb == "scout": self._build_scout() elif verb == "explore": self._explore() elif verb == "wait": self._wait_turns(1) else: print(f"Unknown command: {command}") except (NotEnoughResourcesError, RuntimeError, ValueError) as e: print(f"Refused: {e}")handle(command) { const space = command.indexOf(" "); const verb = space === -1 ? command : command.slice(0, space); const rest = space === -1 ? "" : command.slice(space + 1); try { if (verb === "build") { this.#build(this.#createBuilding(rest)); } else if (verb === "upgrade") { this.#upgrade(rest); } else if (verb === "scout") { this.#buildScout(); } else if (verb === "explore") { this.#explore(); } else if (verb === "wait") { this.#waitTurns(1); } else { console.log(`Unknown command: ${command}`); } } catch (e) { console.log(`Refused: ${e.message}`); }}There are three kinds of refusals here: NotEnoughResourcesError, Error for the wrong moment and RangeError for an unknown building. The single catch prints the message of each of them.
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 of the program:
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.");> build research labRefused: Research lab needs Crystal mine first turn 0, points 0 | metal 40.0, gas 0.0, crystal 15.0, uranium 0.0> build metal mineMetal mine is ready. turn 1, points 10 | metal 35.0, gas 0.5, crystal 13.2, uranium 0.1> build crystal mineCrystal mine is ready. turn 2, points 20 | metal 29.0, gas 1.0, crystal 12.4, uranium 0.2> build research labResearch lab is ready. turn 4, points 30 | metal 21.0, gas 2.0, crystal 9.2, uranium 0.4> upgrade metal mineMetal mine is at level 2. turn 5, points 50 | metal 11.0, gas 2.5, crystal 5.6, uranium 0.5> build shipyardRefused: 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 shipyardShipyard is ready. turn 9, points 60 | metal 12.0, gas 2.5, crystal 3.2, uranium 0.9> scoutRefused: 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> scoutScout-1 is ready. turn 12, points 75 | metal 14.0, gas 2.0, crystal 1.4, uranium 0.2> exploreScout-1 found Cobalt: metal 500.0, gas 0.0, crystal 40.0, uranium 0.0Scout-1 found Helia: metal 0.0, gas 300.0, crystal 0.0, uranium 20.0Planets found: 2 turn 13, points 85 | metal 18.0, gas 2.5, crystal 1.8, uranium 0.3End: 85 points in 13 turns.Game game = new Game(home, others, 80, 40);while (!game.IsOver()){ string command = Console.ReadLine(); if (command == null) { break; } Console.WriteLine("> " + command); game.Handle(command); Console.WriteLine(" " + game.Status());}Console.WriteLine("End: " + game.Points + " points in " + game.Turn + " turns.");> build research labRefused: Research lab needs Crystal mine first turn 0, points 0 | metal 40.0, gas 0.0, crystal 15.0, uranium 0.0> build metal mineMetal mine is ready. turn 1, points 10 | metal 35.0, gas 0.5, crystal 13.2, uranium 0.1> build crystal mineCrystal mine is ready. turn 2, points 20 | metal 29.0, gas 1.0, crystal 12.4, uranium 0.2> build research labResearch lab is ready. turn 4, points 30 | metal 21.0, gas 2.0, crystal 9.2, uranium 0.4> upgrade metal mineMetal mine is at level 2. turn 5, points 50 | metal 11.0, gas 2.5, crystal 5.6, uranium 0.5> build shipyardRefused: 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 shipyardShipyard is ready. turn 9, points 60 | metal 12.0, gas 2.5, crystal 3.2, uranium 0.9> scoutRefused: 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> scoutScout-1 is ready. turn 12, points 75 | metal 14.0, gas 2.0, crystal 1.4, uranium 0.2> exploreScout-1 found Cobalt: metal 500.0, gas 0.0, crystal 40.0, uranium 0.0Scout-1 found Helia: metal 0.0, gas 300.0, crystal 0.0, uranium 20.0Planets found: 2 turn 13, points 85 | metal 18.0, gas 2.5, crystal 1.8, uranium 0.3End: 85 points in 13 turns.game = Game(home, others, 80, 40)while not game.is_over(): try: command = input() except EOFError: break print(f"> {command}") game.handle(command) print(f" {game.status()}")print(f"End: {game.points} points in {game.turn} turns.")> build research labRefused: Research lab needs Crystal mine first turn 0, points 0 | metal 40.0, gas 0.0, crystal 15.0, uranium 0.0> build metal mineMetal mine is ready. turn 1, points 10 | metal 35.0, gas 0.5, crystal 13.2, uranium 0.1> build crystal mineCrystal mine is ready. turn 2, points 20 | metal 29.0, gas 1.0, crystal 12.4, uranium 0.2> build research labResearch lab is ready. turn 4, points 30 | metal 21.0, gas 2.0, crystal 9.2, uranium 0.4> upgrade metal mineMetal mine is at level 2. turn 5, points 50 | metal 11.0, gas 2.5, crystal 5.6, uranium 0.5> build shipyardRefused: 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 shipyardShipyard is ready. turn 9, points 60 | metal 12.0, gas 2.5, crystal 3.2, uranium 0.9> scoutRefused: 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> scoutScout-1 is ready. turn 12, points 75 | metal 14.0, gas 2.0, crystal 1.4, uranium 0.2> exploreScout-1 found Cobalt: metal 500.0, gas 0.0, crystal 40.0, uranium 0.0Scout-1 found Helia: metal 0.0, gas 300.0, crystal 0.0, uranium 20.0Planets found: 2 turn 13, points 85 | metal 18.0, gas 2.5, crystal 1.8, uranium 0.3End: 85 points in 13 turns.const game = new Game(home, others, 80, 40);while (!game.isOver()) { const command = await ask(""); if (command === undefined) { break; } console.log(`> ${command}`); game.handle(command); console.log(` ${game.status()}`);}console.log(`End: ${game.points} points in ${game.turn} turns.`);rl.close();> build research labRefused: Research lab needs Crystal mine first turn 0, points 0 | metal 40.0, gas 0.0, crystal 15.0, uranium 0.0> build metal mineMetal mine is ready. turn 1, points 10 | metal 35.0, gas 0.5, crystal 13.2, uranium 0.1> build crystal mineCrystal mine is ready. turn 2, points 20 | metal 29.0, gas 1.0, crystal 12.4, uranium 0.2> build research labResearch lab is ready. turn 4, points 30 | metal 21.0, gas 2.0, crystal 9.2, uranium 0.4> upgrade metal mineMetal mine is at level 2. turn 5, points 50 | metal 11.0, gas 2.5, crystal 5.6, uranium 0.5> build shipyardRefused: 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 shipyardShipyard is ready. turn 9, points 60 | metal 12.0, gas 2.5, crystal 3.2, uranium 0.9> scoutRefused: 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> scoutScout-1 is ready. turn 12, points 75 | metal 14.0, gas 2.0, crystal 1.4, uranium 0.2> exploreScout-1 found Cobalt: metal 500.0, gas 0.0, crystal 40.0, uranium 0.0Scout-1 found Helia: metal 0.0, gas 300.0, crystal 0.0, uranium 20.0Planets found: 2 turn 13, points 85 | metal 18.0, gas 2.5, crystal 1.8, uranium 0.3End: 85 points in 13 turns.ask is the input function from the lesson on input. When the input runs out, it returns undefined, and break stops the loop.With this input:
build research labbuild metal minebuild crystal minebuild research labupgrade metal minebuild shipyardwaitbuild shipyardscoutwaitscoutexploreMore 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
Researchwith a name, a level and a price, with methods for researching and leveling up. Research and conflicts (pirates, attacks withweaponPower) 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.
build metal mineupgrade metal mineMetal 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.12. A cargo ship. Write CargoShip, a subclass of SpaceShip, with a cargo capacity of 50. harvestResource(planet) loads as much of the planet's metal as fits, but not more than there is. unload(planet) unloads it. Cobalt has 120 metal. The ship Mule makes three trips from Nova Terra to Cobalt and back.
Mule arrived at CobaltMule loaded 50.0 metal from CobaltMule arrived at Nova TerraMule unloaded 50.0 metal at Nova TerraMule arrived at CobaltMule loaded 50.0 metal from CobaltMule arrived at Nova TerraMule unloaded 50.0 metal at Nova TerraMule arrived at CobaltMule loaded 20.0 metal from CobaltMule arrived at Nova TerraMule unloaded 20.0 metal at Nova TerraNova Terra: metal 120.0, gas 0.0, crystal 0.0, uranium 0.0Cobalt: metal 0.0, gas 0.0, crystal 0.0, uranium 0.03. 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.
Metal mine is ready.turn 1, points 10 | metal 35.0, gas 0.5, crystal 13.2, uranium 0.1Crystal mine is ready.turn 2, points 20 | metal 29.0, gas 1.0, crystal 12.4, uranium 0.2Research lab is ready.turn 4, points 30 | metal 21.0, gas 2.0, crystal 9.2, uranium 0.4Gas refinery is ready.turn 6, points 40 | metal 15.0, gas 3.0, crystal 7.0, uranium 0.6turn 7, points 40 | metal 17.0, gas 4.0, crystal 7.4, uranium 0.7turn 8, points 40 | metal 19.0, gas 5.0, crystal 7.8, uranium 0.8turn 9, points 40 | metal 21.0, gas 6.0, crystal 8.2, uranium 0.9turn 10, points 40 | metal 23.0, gas 7.0, crystal 8.6, uranium 1.0More tasks
- A uranium mine. Add a
UraniumMinethat 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