Epic Cosmos 2: recursos, edificios, naves
En resumen: Un juego por turnos: los recursos se extraen en cada turno, los edificios cuestan recursos, las minas duplican la extracción y la falta de recursos es una excepción propia.
Problema
El mundo está listo, pero no pasa nada
De la lección anterior tienes estrellas, planetas y satélites. Pero el juego todavía no funciona: no hay con qué construir ni nada que esperar.
La economía de Epic Cosmos:
- Cada planeta tiene cuatro recursos. En cada turno se extraen, cada uno a su propio ritmo: metal 1,0, gas 0,5, cristal 0,2, uranio 0,1 por turno.
- Los edificios cuestan recursos y tiempo. Las minas y la refinería duplican la tasa de su recurso en cada nivel. El laboratorio y el astillero desbloquean otras cosas.
- Las naves vuelan. El explorador reconoce la zona e informa qué hay en cada planeta.
- El juego es por turnos. Los puntos vienen de edificios, naves y descubrimientos. El juego termina al llegar a cierto número de puntos o de turnos.
Si los recursos no alcanzan, construir tiene que rechazar y decir qué falta. Eso es trabajo para una excepción propia.
La idea
El recurso
Un recurso tiene una cantidad (amount) y una tasa de extracción (harvestRate). En cada turno, harvest() suma la tasa a la cantidad. Una mina llama a boost() y la tasa se duplica.
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} debe escribir 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; }}Los cuatro tipos se distinguen solo por el nombre y la tasa:
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 tiene 0,5, Crystal 0,2 y Uranium 0,1.
Más precisamente, para curiosos
double no puede guardar 0,1 con exactitud. Si sumas 0,1 diez veces, obtienes 0,9999999999999999 y no 1,0. Entonces la comprobación «¿tienes 1 de uranio?» diría «no». Por eso round redondea cada cantidad nueva a décimas. En los juegos reales las cantidades suelen ser números enteros, por ejemplo en décimas de unidad, justo para evitar sorpresas así.
Míralo
Los recursos del planeta
Planet de la lección anterior recibe cuatro recursos y un método que los extrae todos a la vez:
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, cristal %.1f, uranio %.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}, cristal {Crystal.Amount:F1}, uranio {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 = [] # los edificios vienen más adelante en la lección
@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}, cristal {self._crystal.amount:.1f}, uranio {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 = []; // los edificios vienen más adelante en la lección
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)}, cristal ${this.#crystal.amount.toFixed(1)}, uranio ${this.#uranium.amount.toFixed(1)}`;}
printResources() { console.log(`${this.name}: ${this.resourcesText()}`);}Cinco turnos de extracción, luego se duplica el metal y pasa un turno más:
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, cristal 1.0, uranio 0.5Nova Terra: metal 7.0, gas 3.0, cristal 1.2, uranio 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, cristal 1.0, uranio 0.5Nova Terra: metal 7.0, gas 3.0, cristal 1.2, uranio 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, cristal 1.0, uranio 0.5Nova Terra: metal 7.0, gas 3.0, cristal 1.2, uranio 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, cristal 1.0, uranio 0.5Nova Terra: metal 7.0, gas 3.0, cristal 1.2, uranio 0.6Pruébalo
¿Cuánto gas?
¿Qué imprimirá el programa?
Resource gas = new Gas(0);gas.harvest();gas.boost();gas.harvest();gas.boost();gas.harvest();System.out.println(gas.getAmount() + " " + gas.getName() + ", tasa " + gas.getHarvestRate());La primera extracción es 0,5. Luego la tasa pasa a 1,0 y la segunda extracción es 1,0, en total 1,5. Luego la tasa pasa a 2,0 y la tercera extracción es 2,0, en total 3,5.
La idea
Construir cuesta
Cuando los recursos no alcanzan, construir tiene que rechazar. Quien llama quiere saber qué falta y cuánto, así que la excepción es propia y lleva el número:
class NotEnoughResourcesException extends Exception { private double missing;
public NotEnoughResourcesException(String purpose, String resource, double missing) { super(String.format("%s necesita %.1f más de %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} necesita {missing:F1} más de {resource}") { Missing = missing; }}class NotEnoughResourcesError(Exception): def __init__(self, purpose, resource, missing): super().__init__(f"{purpose} necesita {missing:.1f} más de {resource}") self.missing = missingclass NotEnoughResourcesError extends Error { constructor(purpose, resource, missing) { super(`${purpose} necesita ${missing.toFixed(1)} más de ${resource}`); this.name = "NotEnoughResourcesError"; this.missing = missing; }}El pago está en Planet. Primero comprueba los cuatro recursos y después gasta:
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); }}Como NotValidSpaceObjectException, esta excepción también extiende Exception. Por eso cada método del camino la declara con throws.
Pruébalo
Encuentra el error
La colonia tiene 20 de metal y nada de cristal. El Laboratorio cuesta 12 de metal y 5 de cristal. Construir se rechaza, pero el metal bajó a 8. ¿En qué línea está el problema?
Elige la línea con el error.
El metal se gasta justo después de su propia comprobación. Luego la comprobación del cristal lanza la excepción, pero el metal ya está gastado y el Laboratorio no se construyó. La excepción detiene el método, pero no deshace lo que se hizo antes. Primero todas las comprobaciones, después el gasto.
La idea
El edificio
Cada edificio tiene un nombre, un nivel, un tiempo de construcción y un precio en los cuatro recursos. Construir y mejorar son iguales para todos los edificios, por eso están en la clase abstracta BaseBuilding:
public String getRequiredBuilding() { return null;}
public void construct(Planet target) throws NotEnoughResourcesException { if (target.findBuilding(name) != null) { throw new IllegalStateException(name + " ya existe"); } String required = getRequiredBuilding(); if (required != null && target.findBuilding(required) == null) { throw new IllegalStateException(name + " requiere primero " + required); } 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 + " aún no existe"); } double factor = Math.pow(2, level); planet.spend(name + " nivel " + (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 + " ya existe"); } string required = GetRequiredBuilding(); if (required != null && target.FindBuilding(required) == null) { throw new InvalidOperationException(Name + " requiere primero " + required); } target.Spend(Name, MetalRequired, CrystalRequired, GasRequired, UraniumRequired); Planet = target; Level = 1; target.AddBuilding(this);}
public void Upgrade(){ if (Planet == null) { throw new InvalidOperationException(Name + " aún no existe"); } double factor = Math.Pow(2, Level); Planet.Spend(Name + " nivel " + (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} ya existe") required = self.get_required_building() if required is not None and target.find_building(required) is None: raise RuntimeError(f"{self._name} requiere primero {required}") 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} aún no existe") factor = 2 ** self._level self._planet.spend(f"{self._name} nivel {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} ya existe`); } const required = this.getRequiredBuilding(); if (required !== null && target.findBuilding(required) === null) { throw new Error(`${this.#name} requiere primero ${required}`); } 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} aún no existe`); } const factor = 2 ** this.#level; this.#planet.spend(`${this.#name} nivel ${this.#level + 1}`, this.#metalRequired * factor, this.#crystalRequired * factor, this.#gasRequired * factor, this.#uraniumRequired * factor); this.#level++;}
applyEffect() { throw new Error(`${this.constructor.name} debe escribir applyEffect()`);}constructrechaza si el edificio ya existe o si falta el edificio del que depende. Estos rechazos usan la excepción incorporada para el momento inadecuado. Después paga y registra el edificio en el planeta.upgradecuesta el doble en cada nivel: el nivel 2 cuesta 2 veces el precio, el nivel 3 cuesta 4 veces.applyEffectes abstracto: cada edificio sabe por sí mismo qué hace cuando está listo.
Más precisamente, para curiosos
getRequiredBuilding() devuelve null: por defecto un edificio no depende de nada. La subclase que depende de algo lo sobrescribe.
La idea
Minas y dependencias
Las minas se distinguen solo por el recurso que aceleran. Lo que comparten está en 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} debe escribir _resourceOf()`); }
applyEffect() { this._resourceOf(this.planet).boost(); }}class MetalMine extends Mine { public MetalMine() { super("Mina de metal", 1, 6, 2); }
@Override protected Resource resourceOf(Planet planet) { return planet.getMetal(); }}class MetalMine : Mine{ public MetalMine() : base("Mina de metal", 1, 6, 2) { }
protected override Resource ResourceOf(Planet planet) { return planet.Metal; }}class MetalMine(Mine): def __init__(self): super().__init__("Mina de metal", 1, 6, 2)
def _resource_of(self, planet): return planet.metalclass MetalMine extends Mine { constructor() { super("Mina de metal", 1, 6, 2); }
_resourceOf(planet) { return planet.metal; }}La Mina de metal y la Mina de cristal se pueden construir de inmediato. El Laboratorio necesita una Mina de cristal, y la Refinería de gas y el Astillero necesitan un Laboratorio:
class ResearchLab extends BaseBuilding { public ResearchLab() { super("Laboratorio", 0, 2, 12, 4, 0, 0); }
@Override public String getRequiredBuilding() { return "Mina de cristal"; }
@Override public void applyEffect() { // Desbloquea otros edificios. Los recursos no cambian. }}class ResearchLab : BaseBuilding{ public ResearchLab() : base("Laboratorio", 0, 2, 12, 4, 0, 0) { }
public override string GetRequiredBuilding() { return "Mina de cristal"; }
public override void ApplyEffect() { // Desbloquea otros edificios. Los recursos no cambian. }}class ResearchLab(BaseBuilding): def __init__(self): super().__init__("Laboratorio", 0, 2, 12, 4, 0, 0)
def get_required_building(self): return "Mina de cristal"
def apply_effect(self): # It unlocks other buildings. The resources do not change. passclass ResearchLab extends BaseBuilding { constructor() { super("Laboratorio", 0, 2, 12, 4, 0, 0); }
getRequiredBuilding() { return "Mina de cristal"; }
applyEffect() { // Desbloquea otros edificios. Los recursos no cambian. }}Un plan de tres edificios con 10 de metal y 3 de cristal. El Laboratorio se rechaza por la dependencia, y la Mina de cristal por el 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("Construido: " + building.getName() + "."); } catch (NotEnoughResourcesException e) { System.out.println("Rechazado: " + e.getMessage()); } catch (IllegalStateException e) { System.out.println("Rechazado: " + e.getMessage()); }}home.printResources();System.out.println("Tasa de extracción de metal: " + home.getMetal().getHarvestRate());Rechazado: Laboratorio requiere primero Mina de cristalConstruido: Mina de metal.Rechazado: Mina de cristal necesita 4.0 más de metalNova Terra: metal 4.0, gas 0.0, cristal 1.0, uranio 0.0Tasa de extracción de metal: 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("Construido: " + building.Name + "."); } catch (NotEnoughResourcesException e) { Console.WriteLine("Rechazado: " + e.Message); } catch (InvalidOperationException e) { Console.WriteLine("Rechazado: " + e.Message); }}home.PrintResources();Console.WriteLine("Tasa de extracción de metal: " + home.Metal.HarvestRate);Rechazado: Laboratorio requiere primero Mina de cristalConstruido: Mina de metal.Rechazado: Mina de cristal necesita 4.0 más de metalNova Terra: metal 4.0, gas 0.0, cristal 1.0, uranio 0.0Tasa de extracción de metal: 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"Construido: {building.name}.") except NotEnoughResourcesError as e: print(f"Rechazado: {e}") except RuntimeError as e: print(f"Rechazado: {e}")home.print_resources()print(f"Tasa de extracción de metal: {home.metal.harvest_rate}")Rechazado: Laboratorio requiere primero Mina de cristalConstruido: Mina de metal.Rechazado: Mina de cristal necesita 4.0 más de metalNova Terra: metal 4.0, gas 0.0, cristal 1.0, uranio 0.0Tasa de extracción de metal: 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(`Construido: ${building.name}.`); } catch (e) { console.log(`Rechazado: ${e.message}`); }}home.printResources();console.log(`Tasa de extracción de metal: ${home.metal.harvestRate}`);Rechazado: Laboratorio requiere primero Mina de cristalConstruido: Mina de metal.Rechazado: Mina de cristal necesita 4.0 más de metalNova Terra: metal 4.0, gas 0.0, cristal 1.0, uranio 0.0Tasa de extracción de metal: 2Planet 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("Construido: " + building.getName() + "."); } catch (NotEnoughResourcesException e) { System.out.println("Rechazado: " + e.getMessage()); } catch (IllegalStateException e) { System.out.println("Rechazado: " + e.getMessage()); }}home.printResources();System.out.println("Tasa de extracción de metal: " + home.getMetal().getHarvestRate());Paso a paso, en palabras
- Nova Terra tiene 10 de metal y 3 de cristal. El metal se extrae a 1,0 por turno.
- El plan es un array de 3 edificios. Cada uno tiene un precio en metal y cristal.
- Laboratorio requiere primero Mina de cristal, y todavía no existe. construct() lanza IllegalStateException antes de gastar nada.
- Mina de metal cuesta 6 de metal y 2 de cristal. Alcanza: quedan 4 de metal y 1 de cristal. applyEffect() duplica la tasa de metal.
- Mina de cristal necesita 8 de metal y hay 4. La comprobación lanza NotEnoughResourcesException y no se gasta nada.
- Los rechazos no gastaron nada. Solo se descontó el precio del edificio construido, y la tasa de metal es 2,0.
La idea
Naves y el explorador
SpaceShip de la lección anterior recibe un tipo, una velocidad, una capacidad de carga y una potencia de armas. Parte del planeta de origen y vuela con flyTo, como antes. El explorador es una nave con sensores: explore comprueba qué planetas están a su alcance e informa de sus recursos.
class Scout extends SpaceShip { private double sensorRange;
public Scout(String name, Planet home) { super(name, "Explorador", 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() + " encontró " + planet.getName() + ": " + planet.resourcesText()); }}class Scout : SpaceShip{ private double sensorRange;
public Scout(string name, Planet home) : base(name, "Explorador", 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 + " encontró " + planet.Name + ": " + planet.ResourcesText()); }}class Scout(SpaceShip): def __init__(self, name, home): super().__init__(name, "Explorador", 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} encontró {planet.name}: {planet.resources_text()}")class Scout extends SpaceShip { #sensorRange;
constructor(name, home) { super(name, "Explorador", 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} encontró ${planet.name}: ${planet.resourcesText()}`); }}El alcance se mide con la distancia del ejercicio 2 de la lección anterior. El método devuelve cuántos planetas encontró, para que el juego pueda dar puntos por ellos.
Míralo
El juego completo
Game guarda el planeta de origen, los otros planetas, el turno y los puntos. Los comandos vienen del jugador, uno por línea. Los edificios toman turnos, y durante cada turno el planeta extrae:
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("construir")) { build(createBuilding(rest)); } else if (verb.equals("mejorar")) { upgrade(rest); } else if (verb.equals("explorador")) { buildScout(); } else if (verb.equals("explorar")) { explore(); } else if (verb.equals("esperar")) { waitTurns(1); } else { System.out.println("Comando desconocido: " + command); } } catch (NotEnoughResourcesException e) { System.out.println("Rechazado: " + e.getMessage()); } catch (IllegalStateException e) { System.out.println("Rechazado: " + e.getMessage()); } catch (IllegalArgumentException e) { System.out.println("Rechazado: " + 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 == "construir") { Build(CreateBuilding(rest)); } else if (verb == "mejorar") { Upgrade(rest); } else if (verb == "explorador") { BuildScout(); } else if (verb == "explorar") { Explore(); } else if (verb == "esperar") { WaitTurns(1); } else { Console.WriteLine("Comando desconocido: " + command); } } catch (NotEnoughResourcesException e) { Console.WriteLine("Rechazado: " + e.Message); } catch (InvalidOperationException e) { Console.WriteLine("Rechazado: " + e.Message); } catch (ArgumentException e) { Console.WriteLine("Rechazado: " + e.Message); }}def handle(self, command): verb, _, rest = command.partition(" ") try: if verb == "construir": self._build(self._create_building(rest)) elif verb == "mejorar": self._upgrade(rest) elif verb == "explorador": self._build_scout() elif verb == "explorar": self._explore() elif verb == "esperar": self._wait_turns(1) else: print(f"Comando desconocido: {command}") except (NotEnoughResourcesError, RuntimeError, ValueError) as e: print(f"Rechazado: {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 === "construir") { this.#build(this.#createBuilding(rest)); } else if (verb === "mejorar") { this.#upgrade(rest); } else if (verb === "explorador") { this.#buildScout(); } else if (verb === "explorar") { this.#explore(); } else if (verb === "esperar") { this.#waitTurns(1); } else { console.log(`Comando desconocido: ${command}`); } } catch (e) { console.log(`Rechazado: ${e.message}`); }}Cada edificio aporta 10 puntos, una mejora 10 por el nuevo nivel, el explorador 15 y cada planeta encontrado 5. El juego termina a los 80 puntos o a los 40 turnos. El bucle en 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("Fin: " + game.getPoints() + " puntos en " + game.getTurn() + " turnos.");> construir laboratorioRechazado: Laboratorio requiere primero Mina de cristal turno 0, puntos 0 | metal 40.0, gas 0.0, cristal 15.0, uranio 0.0> construir mina de metalConstruido: Mina de metal. turno 1, puntos 10 | metal 35.0, gas 0.5, cristal 13.2, uranio 0.1> construir mina de cristalConstruido: Mina de cristal. turno 2, puntos 20 | metal 29.0, gas 1.0, cristal 12.4, uranio 0.2> construir laboratorioConstruido: Laboratorio. turno 4, puntos 30 | metal 21.0, gas 2.0, cristal 9.2, uranio 0.4> mejorar mina de metalMina de metal sube al nivel 2. turno 5, puntos 50 | metal 11.0, gas 2.5, cristal 5.6, uranio 0.5> construir astilleroRechazado: Astillero necesita 4.0 más de metal turno 5, puntos 50 | metal 11.0, gas 2.5, cristal 5.6, uranio 0.5> esperar turno 6, puntos 50 | metal 15.0, gas 3.0, cristal 6.0, uranio 0.6> construir astilleroConstruido: Astillero. turno 9, puntos 60 | metal 12.0, gas 2.5, cristal 3.2, uranio 0.9> exploradorRechazado: Explorador necesita 0.1 más de uranio turno 9, puntos 60 | metal 12.0, gas 2.5, cristal 3.2, uranio 0.9> esperar turno 10, puntos 60 | metal 16.0, gas 3.0, cristal 3.6, uranio 1.0> exploradorExplorador-1 está listo. turno 12, puntos 75 | metal 14.0, gas 2.0, cristal 1.4, uranio 0.2> explorarExplorador-1 encontró Cobalto: metal 500.0, gas 0.0, cristal 40.0, uranio 0.0Explorador-1 encontró Helia: metal 0.0, gas 300.0, cristal 0.0, uranio 20.0Planetas encontrados: 2 turno 13, puntos 85 | metal 18.0, gas 2.5, cristal 1.8, uranio 0.3Fin: 85 puntos en 13 turnos.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("Fin: " + game.Points + " puntos en " + game.Turn + " turnos.");> construir laboratorioRechazado: Laboratorio requiere primero Mina de cristal turno 0, puntos 0 | metal 40.0, gas 0.0, cristal 15.0, uranio 0.0> construir mina de metalConstruido: Mina de metal. turno 1, puntos 10 | metal 35.0, gas 0.5, cristal 13.2, uranio 0.1> construir mina de cristalConstruido: Mina de cristal. turno 2, puntos 20 | metal 29.0, gas 1.0, cristal 12.4, uranio 0.2> construir laboratorioConstruido: Laboratorio. turno 4, puntos 30 | metal 21.0, gas 2.0, cristal 9.2, uranio 0.4> mejorar mina de metalMina de metal sube al nivel 2. turno 5, puntos 50 | metal 11.0, gas 2.5, cristal 5.6, uranio 0.5> construir astilleroRechazado: Astillero necesita 4.0 más de metal turno 5, puntos 50 | metal 11.0, gas 2.5, cristal 5.6, uranio 0.5> esperar turno 6, puntos 50 | metal 15.0, gas 3.0, cristal 6.0, uranio 0.6> construir astilleroConstruido: Astillero. turno 9, puntos 60 | metal 12.0, gas 2.5, cristal 3.2, uranio 0.9> exploradorRechazado: Explorador necesita 0.1 más de uranio turno 9, puntos 60 | metal 12.0, gas 2.5, cristal 3.2, uranio 0.9> esperar turno 10, puntos 60 | metal 16.0, gas 3.0, cristal 3.6, uranio 1.0> exploradorExplorador-1 está listo. turno 12, puntos 75 | metal 14.0, gas 2.0, cristal 1.4, uranio 0.2> explorarExplorador-1 encontró Cobalto: metal 500.0, gas 0.0, cristal 40.0, uranio 0.0Explorador-1 encontró Helia: metal 0.0, gas 300.0, cristal 0.0, uranio 20.0Planetas encontrados: 2 turno 13, puntos 85 | metal 18.0, gas 2.5, cristal 1.8, uranio 0.3Fin: 85 puntos en 13 turnos.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"Fin: {game.points} puntos en {game.turn} turnos.")> construir laboratorioRechazado: Laboratorio requiere primero Mina de cristal turno 0, puntos 0 | metal 40.0, gas 0.0, cristal 15.0, uranio 0.0> construir mina de metalConstruido: Mina de metal. turno 1, puntos 10 | metal 35.0, gas 0.5, cristal 13.2, uranio 0.1> construir mina de cristalConstruido: Mina de cristal. turno 2, puntos 20 | metal 29.0, gas 1.0, cristal 12.4, uranio 0.2> construir laboratorioConstruido: Laboratorio. turno 4, puntos 30 | metal 21.0, gas 2.0, cristal 9.2, uranio 0.4> mejorar mina de metalMina de metal sube al nivel 2. turno 5, puntos 50 | metal 11.0, gas 2.5, cristal 5.6, uranio 0.5> construir astilleroRechazado: Astillero necesita 4.0 más de metal turno 5, puntos 50 | metal 11.0, gas 2.5, cristal 5.6, uranio 0.5> esperar turno 6, puntos 50 | metal 15.0, gas 3.0, cristal 6.0, uranio 0.6> construir astilleroConstruido: Astillero. turno 9, puntos 60 | metal 12.0, gas 2.5, cristal 3.2, uranio 0.9> exploradorRechazado: Explorador necesita 0.1 más de uranio turno 9, puntos 60 | metal 12.0, gas 2.5, cristal 3.2, uranio 0.9> esperar turno 10, puntos 60 | metal 16.0, gas 3.0, cristal 3.6, uranio 1.0> exploradorExplorador-1 está listo. turno 12, puntos 75 | metal 14.0, gas 2.0, cristal 1.4, uranio 0.2> explorarExplorador-1 encontró Cobalto: metal 500.0, gas 0.0, cristal 40.0, uranio 0.0Explorador-1 encontró Helia: metal 0.0, gas 300.0, cristal 0.0, uranio 20.0Planetas encontrados: 2 turno 13, puntos 85 | metal 18.0, gas 2.5, cristal 1.8, uranio 0.3Fin: 85 puntos en 13 turnos.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(`Fin: ${game.points} puntos en ${game.turn} turnos.`);rl.close();> construir laboratorioRechazado: Laboratorio requiere primero Mina de cristal turno 0, puntos 0 | metal 40.0, gas 0.0, cristal 15.0, uranio 0.0> construir mina de metalConstruido: Mina de metal. turno 1, puntos 10 | metal 35.0, gas 0.5, cristal 13.2, uranio 0.1> construir mina de cristalConstruido: Mina de cristal. turno 2, puntos 20 | metal 29.0, gas 1.0, cristal 12.4, uranio 0.2> construir laboratorioConstruido: Laboratorio. turno 4, puntos 30 | metal 21.0, gas 2.0, cristal 9.2, uranio 0.4> mejorar mina de metalMina de metal sube al nivel 2. turno 5, puntos 50 | metal 11.0, gas 2.5, cristal 5.6, uranio 0.5> construir astilleroRechazado: Astillero necesita 4.0 más de metal turno 5, puntos 50 | metal 11.0, gas 2.5, cristal 5.6, uranio 0.5> esperar turno 6, puntos 50 | metal 15.0, gas 3.0, cristal 6.0, uranio 0.6> construir astilleroConstruido: Astillero. turno 9, puntos 60 | metal 12.0, gas 2.5, cristal 3.2, uranio 0.9> exploradorRechazado: Explorador necesita 0.1 más de uranio turno 9, puntos 60 | metal 12.0, gas 2.5, cristal 3.2, uranio 0.9> esperar turno 10, puntos 60 | metal 16.0, gas 3.0, cristal 3.6, uranio 1.0> exploradorExplorador-1 está listo. turno 12, puntos 75 | metal 14.0, gas 2.0, cristal 1.4, uranio 0.2> explorarExplorador-1 encontró Cobalto: metal 500.0, gas 0.0, cristal 40.0, uranio 0.0Explorador-1 encontró Helia: metal 0.0, gas 300.0, cristal 0.0, uranio 20.0Planetas encontrados: 2 turno 13, puntos 85 | metal 18.0, gas 2.5, cristal 1.8, uranio 0.3Fin: 85 puntos en 13 turnos.Con esta entrada:
construir laboratorioconstruir mina de metalconstruir mina de cristalconstruir laboratoriomejorar mina de metalconstruir astilleroesperarconstruir astilleroexploradoresperarexploradorexplorarMás precisamente, para curiosos
- Los rechazos no avanzan el turno: el edificio no se empezó, así que el jugador puede probar otra cosa de inmediato.
- La investigación (las tecnologías de las reglas) está en el enunciado original solo como plan: una clase
Researchcon un nombre, un nivel y un precio, con métodos para investigar y subir de nivel. La investigación y los conflictos (piratas, ataques conweaponPower) son el siguiente paso si quieres seguir por tu cuenta.
Ejercicio
La colonia crece
Resuelve las tareas en tu editor, con las clases de la lección.
1. 100 de metal lo más rápido posible. Nova Terra empieza con 40 de metal y 15 de cristal. Lee el plan de la entrada, un comando por línea, ejecútalo y después espera hasta que el metal llegue a 100. Imprime en qué turno ocurrió. Con este plan es en el turno 21. Encuentra uno más rápido.
construir mina de metalmejorar mina de metalConstruido: Mina de metal.Mina de metal sube al nivel 2.100 de metal en el turno 21: turno 21, puntos 30 | metal 101.0, gas 10.5, cristal 13.2, uranio 2.12. Una nave de carga. Escribe CargoShip, una subclase de SpaceShip, con una capacidad de carga de 50. harvestResource(Planet planet) carga del metal del planeta todo lo que quepa, pero no más de lo que hay. unload(Planet planet) lo descarga. Cobalto tiene 120 de metal. La nave Mula hace tres viajes de Nova Terra a Cobalto y de vuelta.
Mula llegó a CobaltoMula cargó 50.0 de metal en CobaltoMula llegó a Nova TerraMula descargó 50.0 de metal en Nova TerraMula llegó a CobaltoMula cargó 50.0 de metal en CobaltoMula llegó a Nova TerraMula descargó 50.0 de metal en Nova TerraMula llegó a CobaltoMula cargó 20.0 de metal en CobaltoMula llegó a Nova TerraMula descargó 20.0 de metal en Nova TerraNova Terra: metal 120.0, gas 0.0, cristal 0.0, uranio 0.0Cobalto: metal 0.0, gas 0.0, cristal 0.0, uranio 0.03. Una simulación de 10 turnos. Sin jugador: el juego tiene una lista de edificios (Mina de metal, Mina de cristal, Laboratorio, Refinería de gas). En cada paso intenta construir el siguiente. Si no puede, espera un turno. Después de cada paso imprime el estado, hasta que los turnos lleguen a 10.
Construido: Mina de metal.turno 1, puntos 10 | metal 35.0, gas 0.5, cristal 13.2, uranio 0.1Construido: Mina de cristal.turno 2, puntos 20 | metal 29.0, gas 1.0, cristal 12.4, uranio 0.2Construido: Laboratorio.turno 4, puntos 30 | metal 21.0, gas 2.0, cristal 9.2, uranio 0.4Construido: Refinería de gas.turno 6, puntos 40 | metal 15.0, gas 3.0, cristal 7.0, uranio 0.6turno 7, puntos 40 | metal 17.0, gas 4.0, cristal 7.4, uranio 0.7turno 8, puntos 40 | metal 19.0, gas 5.0, cristal 7.8, uranio 0.8turno 9, puntos 40 | metal 21.0, gas 6.0, cristal 8.2, uranio 0.9turno 10, puntos 40 | metal 23.0, gas 7.0, cristal 8.6, uranio 1.0Más tareas
- Una mina de uranio. Añade una
UraniumMineque dependa de la Refinería de gas. - Puntos por recursos. Da 1 punto por cada 50 de metal extraído.
- Piratas. Cada 10 turnos, los piratas se llevan el 20 % del metal si el planeta no tiene una nave con una potencia de armas de al menos 10.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Comprobaciones de tipo · Excepciones · try, catch y finally · Lanzar excepciones y crear las tuyas
- Ejercicios sobre el tema: Hagamos un juego