Ir al contenido
wannacode

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

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

Gas tiene 0,5, Crystal 0,2 y Uranium 0,1.

Más precisamente, para curiosos

float 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(…, 1) 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í.

total = 0
for i in range(10):
total += 0.1
print(total)
Salida
0.9999999999999999
round(x, 1) es la función incorporada de Python para redondear a un decimal, así que no hace falta un método aparte.

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

Los recursos se crean en el __init__ del planeta. Aquí __init__ hace falta, así que llama al padre con super().__init__(...) y después añade lo suyo.

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();
Salida
Nova Terra: metal 5.0, gas 2.5, cristal 1.0, uranio 0.5
Nova Terra: metal 7.0, gas 3.0, cristal 1.2, uranio 0.6

Pruébalo

¿Cuánto gas?

¿Qué imprimirá el programa?

gas = Gas(0)
gas.harvest()
gas.boost()
gas.harvest()
gas.boost()
gas.harvest()
print(f"{gas.amount} {gas.get_name()}, tasa {gas.harvest_rate}")
Respuesta

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

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

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.

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();
  • construct rechaza si el edificio ya existe o si falta el edificio del que depende. Estos rechazos usan la excepción incorporada para el momento inadecuado (RuntimeError). Después paga y registra el edificio en el planeta.
  • upgrade cuesta el doble en cada nivel: el nivel 2 cuesta 2 veces el precio, el nivel 3 cuesta 4 veces.
  • apply_effect es abstracto: cada edificio sabe por sí mismo qué hace cuando está listo.
Más precisamente, para curiosos

get_required_building() devuelve None: 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();
}
}
class MetalMine extends Mine {
public MetalMine() {
super("Mina de metal", 1, 6, 2);
}
@Override
protected Resource resourceOf(Planet planet) {
return planet.getMetal();
}
}

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

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());
Salida
Rechazado: Laboratorio requiere primero Mina de cristal
Construido: Mina de metal.
Rechazado: Mina de cristal necesita 4.0 más de metal
Nova Terra: metal 4.0, gas 0.0, cristal 1.0, uranio 0.0
Tasa de extracción de metal: 2.0

home = 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}")
Paso a paso, en palabras
  1. Nova Terra tiene 10 de metal y 3 de cristal. El metal se extrae a 1,0 por turno.
  2. El plan es una lista de 3 edificios. Cada uno tiene un precio en metal y cristal.
  3. Laboratorio requiere primero Mina de cristal, y todavía no existe. construct() lanza RuntimeError antes de gastar nada.
  4. Mina de metal cuesta 6 de metal y 2 de cristal. Alcanza: quedan 4 de metal y 1 de cristal. apply_effect() duplica la tasa de metal.
  5. Mina de cristal necesita 8 de metal y hay 4. La comprobación lanza NotEnoughResourcesError y no se gasta nada.
  6. 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 fly_to, 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());
}
}

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

partition(" ") divide el texto en el primer espacio y devuelve tres partes: lo que va antes del espacio, el espacio mismo y lo que va después. Si no hay espacio, la tercera parte queda vacía. La del medio no la necesitamos, por eso es _. Todos los rechazos se capturan con un solo except y una tupla de tipos.

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 del programa:

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.");
Salida
> construir laboratorio
Rechazado: 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 metal
Construido: Mina de metal.
turno 1, puntos 10 | metal 35.0, gas 0.5, cristal 13.2, uranio 0.1
> construir mina de cristal
Construido: Mina de cristal.
turno 2, puntos 20 | metal 29.0, gas 1.0, cristal 12.4, uranio 0.2
> construir laboratorio
Construido: Laboratorio.
turno 4, puntos 30 | metal 21.0, gas 2.0, cristal 9.2, uranio 0.4
> mejorar mina de metal
Mina de metal sube al nivel 2.
turno 5, puntos 50 | metal 11.0, gas 2.5, cristal 5.6, uranio 0.5
> construir astillero
Rechazado: 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 astillero
Construido: Astillero.
turno 9, puntos 60 | metal 12.0, gas 2.5, cristal 3.2, uranio 0.9
> explorador
Rechazado: 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
> explorador
Explorador-1 está listo.
turno 12, puntos 75 | metal 14.0, gas 2.0, cristal 1.4, uranio 0.2
> explorar
Explorador-1 encontró Cobalto: metal 500.0, gas 0.0, cristal 40.0, uranio 0.0
Explorador-1 encontró Helia: metal 0.0, gas 300.0, cristal 0.0, uranio 20.0
Planetas encontrados: 2
turno 13, puntos 85 | metal 18.0, gas 2.5, cristal 1.8, uranio 0.3
Fin: 85 puntos en 13 turnos.
input() lanza EOFError cuando se acaba la entrada. Entonces break detiene el bucle.

Con esta entrada:

Entrada
construir laboratorio
construir mina de metal
construir mina de cristal
construir laboratorio
mejorar mina de metal
construir astillero
esperar
construir astillero
explorador
esperar
explorador
explorar
Má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 Research con un nombre, un nivel y un precio, con métodos para investigar y subir de nivel. La investigación y los conflictos (piratas, ataques con weapon_power) 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.

Entrada
construir mina de metal
mejorar mina de metal
Salida
Construido: 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.1

2. Una nave de carga. Escribe CargoShip, una subclase de SpaceShip, con una capacidad de carga de 50. harvest_resource(planet) carga del metal del planeta todo lo que quepa, pero no más de lo que hay. unload(planet) lo descarga. Cobalto tiene 120 de metal. La nave Mula hace tres viajes de Nova Terra a Cobalto y de vuelta.

Salida
Mula llegó a Cobalto
Mula cargó 50.0 de metal en Cobalto
Mula llegó a Nova Terra
Mula descargó 50.0 de metal en Nova Terra
Mula llegó a Cobalto
Mula cargó 50.0 de metal en Cobalto
Mula llegó a Nova Terra
Mula descargó 50.0 de metal en Nova Terra
Mula llegó a Cobalto
Mula cargó 20.0 de metal en Cobalto
Mula llegó a Nova Terra
Mula descargó 20.0 de metal en Nova Terra
Nova Terra: metal 120.0, gas 0.0, cristal 0.0, uranio 0.0
Cobalto: metal 0.0, gas 0.0, cristal 0.0, uranio 0.0

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

Salida
Construido: Mina de metal.
turno 1, puntos 10 | metal 35.0, gas 0.5, cristal 13.2, uranio 0.1
Construido: Mina de cristal.
turno 2, puntos 20 | metal 29.0, gas 1.0, cristal 12.4, uranio 0.2
Construido: Laboratorio.
turno 4, puntos 30 | metal 21.0, gas 2.0, cristal 9.2, uranio 0.4
Construido: Refinería de gas.
turno 6, puntos 40 | metal 15.0, gas 3.0, cristal 7.0, uranio 0.6
turno 7, puntos 40 | metal 17.0, gas 4.0, cristal 7.4, uranio 0.7
turno 8, puntos 40 | metal 19.0, gas 5.0, cristal 7.8, uranio 0.8
turno 9, puntos 40 | metal 21.0, gas 6.0, cristal 8.2, uranio 0.9
turno 10, puntos 40 | metal 23.0, gas 7.0, cristal 8.6, uranio 1.0

Más tareas

  • Una mina de uranio. Añade una UraniumMine que 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

1. El metal se extrae a 1,0 por turno. Construyes una Mina de metal y la mejoras hasta el nivel 3. ¿Cuánto metal por turno da el planeta?
2. ¿Por qué spend primero comprueba los cuatro recursos y solo después gasta?
3. El Laboratorio necesita antes una Mina de cristal. ¿Por qué este error usa la excepción incorporada y no NotEnoughResourcesError?
4. ¿Cuándo termina el juego de la lección?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·