Към съдържанието
wannacode

Epic Cosmos 2: ресурси, сгради, кораби

Проблем

Светът е готов, но нищо не става

От миналия урок имаш звезди, планети и спътници. Но играта още не върви: няма с какво да строиш и няма за какво да чакаш.

Икономиката на Epic Cosmos:

  • Всяка планета има четири ресурса. Всеки ход те се добиват със своя скорост: метал 1,0, газ 0,5, кристал 0,2, уран 0,1 на ход.
  • Сградите струват ресурси и време. Мините и рафинерията удвояват добива на своя ресурс на всяко ниво. Лабораторията и корабостроителницата отключват други неща.
  • Корабите летят. Скаутът проучва околността и докладва какво има на всяка планета.
  • Играта е на ходове. Точките идват от сгради, кораби и открития. Краят е при определен брой точки или ходове.

Ако ресурсите не стигат, строежът трябва да откаже и да каже какво липсва. Това е работа за свое изключение.

Идеята

Ресурсът

Ресурсът има количество (amount) и скорост на добив (harvestRate). Всеки ход harvest() добавя скоростта към количеството. Мината вика boost() и скоростта се удвоява.

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

Четирите вида се различават само по име и по скорост:

class Metal extends Resource {
public Metal(double amount) {
super(amount, 1.0);
}
@Override
public String getName() {
return "метал";
}
}

Gas е с 0,5, Crystal с 0,2, Uranium с 0,1.

По-точно, за любопитните

number не може да пази 0,1 точно. Ако събереш 0,1 десет пъти, получаваш 0,9999999999999999, а не 1,0. Тогава проверката „имаш ли 1 уран“ би казала „не“. Затова Resource.#round закръгля всяко ново количество до десети. В истински игри количествата често са цели числа, например в десети от единицата, точно за да няма такива изненади.

let total = 0;
for (let i = 0; i < 10; i++) {
total += 0.1;
}
console.log(total);
Изход
0.9999999999999999
Math.round закръгля само до цяло число, затова #round умножава по 10, закръгля и дели. static #round е скрит метод на самия клас: вика се като Resource.#round(...), а отвън не се вижда.

Виж го

Ресурсите на планетата

Planet от миналия урок получава четири ресурса и метод, който ги добива наведнъж:

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("метал %.1f, газ %.1f, кристал %.1f, уран %.1f", metal.getAmount(), gas.getAmount(), crystal.getAmount(), uranium.getAmount());
}
public void printResources() {
System.out.println(getName() + ": " + resourcesText());
}

Пет хода добив, после удвояване на метала и още един ход:

Planet home = new Planet("Нова Терра", 100, 0, 0, 1);
for (int turn = 1; turn <= 5; turn++) {
home.harvestAll();
}
home.printResources();
home.getMetal().boost();
home.harvestAll();
home.printResources();
Изход
Нова Терра: метал 5.0, газ 2.5, кристал 1.0, уран 0.5
Нова Терра: метал 7.0, газ 3.0, кристал 1.2, уран 0.6

Пробвай

Колко газ?

Какво ще отпечата програмата?

const gas = new Gas(0);
gas.harvest();
gas.boost();
gas.harvest();
gas.boost();
gas.harvest();
console.log(`${gas.amount} ${gas.getName()}, добив ${gas.harvestRate}`);
Отговор

Идеята

Строежът струва

Когато ресурсите не стигат, строежът трябва да откаже. Викащият иска да знае какво и колко липсва, затова изключението е свое и носи числото:

class NotEnoughResourcesException extends Exception {
private double missing;
public NotEnoughResourcesException(String purpose, String resource, double missing) {
super(String.format("за %s трябват още %.1f %s", purpose, missing, resource));
this.missing = missing;
}
public double getMissing() {
return missing;
}
}

Плащането е в Planet. Първо проверява четирите ресурса, после харчи:

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

#check е скрит метод: # пред името на метод значи същото като пред поле. Вика се само отвътре, като this.#check(...).

Пробвай

Намери грешката

Колонията има 20 метал и никакъв кристал. Лабораторията струва 12 метал и 5 кристал. Строежът е отказан, но металът е станал 8. На кой ред е проблемът?

Избери реда с грешката.

Идеята

Сградата

Всяка сграда има име, ниво, време за строеж и цена в четирите ресурса. Строежът и надграждането са общи за всички сгради, затова са в абстрактния BaseBuilding:

public String getRequiredBuilding() {
return null;
}
public void construct(Planet target) throws NotEnoughResourcesException {
if (target.findBuilding(name) != null) {
throw new IllegalStateException(name + " вече е построена");
}
String required = getRequiredBuilding();
if (required != null && target.findBuilding(required) == null) {
throw new IllegalStateException(name + " иска първо " + 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 + " още не е построена");
}
double factor = Math.pow(2, level);
planet.spend(name + " ниво " + (level + 1), metalRequired * factor, crystalRequired * factor, gasRequired * factor, uraniumRequired * factor);
level++;
}
public abstract void applyEffect();
  • construct отказва, ако сградата вече я има или ако липсва сградата, от която зависи. Тези откази са с готовото изключение за неподходящ момент: в JavaScript това е обикновен Error. После плаща и записва сградата в планетата.
  • upgrade струва двойно повече на всяко ниво: ниво 2 е 2 пъти цената, ниво 3 е 4 пъти.
  • applyEffect е абстрактен: всяка сграда сама знае какво прави, когато е готова. Както при getKind, в родителя само хвърля грешка.
По-точно, за любопитните

getRequiredBuilding() връща null: по подразбиране сградата не зависи от нищо. Наследникът, който зависи, го презаписва.

Идеята

Мини и зависимости

Мините се различават само по това кой ресурс ускоряват. Общото е в 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("Метална мина", 1, 6, 2);
}
@Override
protected Resource resourceOf(Planet planet) {
return planet.getMetal();
}
}

Метална и Кристална мина се строят веднага. Лабораторията иска Кристална мина, а Газовата рафинерия и Корабостроителницата искат Лаборатория:

class ResearchLab extends BaseBuilding {
public ResearchLab() {
super("Лаборатория", 0, 2, 12, 4, 0, 0);
}
@Override
public String getRequiredBuilding() {
return "Кристална мина";
}
@Override
public void applyEffect() {
// Отключва други сгради, а ресурсите не се променят.
}
}

План от три сгради с 10 метал и 3 кристал. Лабораторията е отказана заради зависимостта, Кристалната мина заради метала:

Planet home = new Planet("Нова Терра", 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() + " е готова.");
} catch (NotEnoughResourcesException e) {
System.out.println("Отказано: " + e.getMessage());
} catch (IllegalStateException e) {
System.out.println("Отказано: " + e.getMessage());
}
}
home.printResources();
System.out.println("Добив на метал: " + home.getMetal().getHarvestRate());
Изход
Отказано: Лаборатория иска първо Кристална мина
Метална мина е готова.
Отказано: за Кристална мина трябват още 4.0 метал
Нова Терра: метал 4.0, газ 0.0, кристал 1.0, уран 0.0
Добив на метал: 2.0

catch в JavaScript е един за всички грешки. Тук всяка грешка, която стига до него, е отказ на строежа, затова печата съобщението, каквото и да е. Ако по пътя може да има и други грешки, провери вида с instanceof и хвърли нататък останалите, както tryOrbit в миналия урок.

const home = new Planet("Нова Терра", 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} е готова.`);
} catch (e) {
console.log(`Отказано: ${e.message}`);
}
}
home.printResources();
console.log(`Добив на метал: ${home.metal.harvestRate}`);
Стъпка по стъпка, с думи
  1. На Нова Терра има 10 метал и 3 кристал. Металът се добива с 1.0 на ход.
  2. Планът е масив от 3 сгради. Всяка има цена в метал и кристал.
  3. Лаборатория иска Кристална мина, а такава още няма. construct() хвърля Error, преди да похарчи нещо.
  4. Метална мина струва 6 метал и 2 кристал. Стигат: остават 4 метал и 1 кристал. applyEffect() удвоява добива на метал.
  5. Кристална мина иска 8 метал, а има 4. Проверката хвърля NotEnoughResourcesError и нищо не е похарчено.
  6. Отказите не похарчиха нищо. Остава цената само на построената сграда, а добивът на метал е 2.

Идеята

Кораби и скаут

SpaceShip от миналия урок получава тип, скорост, товароносимост и сила на оръжията. Започва от родната планета и лети с flyTo, както преди. Скаутът е кораб със сензори: explore проверява кои планети са в обсега му и докладва ресурсите им.

class Scout extends SpaceShip {
private double sensorRange;
public Scout(String name, Planet home) {
super(name, "Скаут", 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() + " откри " + planet.getName() + ": " + planet.resourcesText());
}
}

Обсегът се мери с разстоянието от упражнение 2 в миналия урок. Методът връща колко планети е открил, за да може играта да даде точки за тях.

Виж го

Цялата игра

Game пази родната планета, другите планети, хода и точките. Командите идват от играча, по една на ред. Сградите отнемат ходове, а през всеки ход планетата добива:

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(createBuilding(rest));
} else if (verb.equals("надгради")) {
upgrade(rest);
} else if (verb.equals("скаут")) {
buildScout();
} else if (verb.equals("проучи")) {
explore();
} else if (verb.equals("чакай")) {
waitTurns(1);
} else {
System.out.println("Непозната команда: " + command);
}
} catch (NotEnoughResourcesException e) {
System.out.println("Отказано: " + e.getMessage());
} catch (IllegalStateException e) {
System.out.println("Отказано: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println("Отказано: " + e.getMessage());
}
}

Отказите тук са три вида: NotEnoughResourcesError, Error за неподходящ момент и RangeError за непозната сграда. Единственият catch печата съобщението на всеки от тях.

Всяка сграда носи 10 точки, надграждането 10 по новото ниво, скаутът 15, а всяка открита планета 5. Играта свършва при 80 точки или 40 хода. Цикълът на програмата:

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("Край: " + game.getPoints() + " точки за " + game.getTurn() + " хода.");
Изход
> строй лаборатория
Отказано: Лаборатория иска първо Кристална мина
ход 0, точки 0 | метал 40.0, газ 0.0, кристал 15.0, уран 0.0
> строй метална мина
Метална мина е готова.
ход 1, точки 10 | метал 35.0, газ 0.5, кристал 13.2, уран 0.1
> строй кристална мина
Кристална мина е готова.
ход 2, точки 20 | метал 29.0, газ 1.0, кристал 12.4, уран 0.2
> строй лаборатория
Лаборатория е готова.
ход 4, точки 30 | метал 21.0, газ 2.0, кристал 9.2, уран 0.4
> надгради метална мина
Метална мина е на ниво 2.
ход 5, точки 50 | метал 11.0, газ 2.5, кристал 5.6, уран 0.5
> строй корабостроителница
Отказано: за Корабостроителница трябват още 4.0 метал
ход 5, точки 50 | метал 11.0, газ 2.5, кристал 5.6, уран 0.5
> чакай
ход 6, точки 50 | метал 15.0, газ 3.0, кристал 6.0, уран 0.6
> строй корабостроителница
Корабостроителница е готова.
ход 9, точки 60 | метал 12.0, газ 2.5, кристал 3.2, уран 0.9
> скаут
Отказано: за скаут трябват още 0.1 уран
ход 9, точки 60 | метал 12.0, газ 2.5, кристал 3.2, уран 0.9
> чакай
ход 10, точки 60 | метал 16.0, газ 3.0, кристал 3.6, уран 1.0
> скаут
Скаут-1 е готов.
ход 12, точки 75 | метал 14.0, газ 2.0, кристал 1.4, уран 0.2
> проучи
Скаут-1 откри Кобалт: метал 500.0, газ 0.0, кристал 40.0, уран 0.0
Скаут-1 откри Хелия: метал 0.0, газ 300.0, кристал 0.0, уран 20.0
Открити планети: 2
ход 13, точки 85 | метал 18.0, газ 2.5, кристал 1.8, уран 0.3
Край: 85 точки за 13 хода.
ask е функцията за вход от урока за входа. Когато входът свърши, тя връща undefined, и break спира цикъла.

С този вход:

Вход
строй лаборатория
строй метална мина
строй кристална мина
строй лаборатория
надгради метална мина
строй корабостроителница
чакай
строй корабостроителница
скаут
чакай
скаут
проучи
По-точно, за любопитните
  • Отказите не местят хода: сградата не е започната, затова играчът може веднага да опита друго.
  • Проучванията (технологиите от правилата) са в старото задание само като план: клас Research с име, ниво и цена, с методи за проучване и вдигане на ниво. Те и конфликтите (пирати, атака с weaponPower) са следващата стъпка, ако искаш да продължиш сам.

Упражнение

Колонията расте

Реши задачите в редактора си, с класовете от урока.

1. 100 метал възможно най-бързо. Нова Терра започва с 40 метал и 15 кристал. Прочети плана от входа, команда на ред, изпълни го и после чакай, докато металът стигне 100. Отпечатай на кой ход е станало. С този план е на ход 21. Намери по-бърз.

Вход
строй метална мина
надгради метална мина
Изход
Метална мина е готова.
Метална мина е на ниво 2.
100 метал на ход 21: ход 21, точки 30 | метал 101.0, газ 10.5, кристал 13.2, уран 2.1

2. Товарен кораб. Напиши CargoShip, наследник на SpaceShip, с товароносимост 50. harvestResource(planet) товари от метала на планетата колкото се побере, но не повече от наличното. unload(planet) го разтоварва. На Кобалт има 120 метал. Корабът Мулето прави три курса Нова Терра – Кобалт – Нова Терра.

Изход
Мулето пристигна при Кобалт
Мулето натовари 50.0 метал от Кобалт
Мулето пристигна при Нова Терра
Мулето разтовари 50.0 метал на Нова Терра
Мулето пристигна при Кобалт
Мулето натовари 50.0 метал от Кобалт
Мулето пристигна при Нова Терра
Мулето разтовари 50.0 метал на Нова Терра
Мулето пристигна при Кобалт
Мулето натовари 20.0 метал от Кобалт
Мулето пристигна при Нова Терра
Мулето разтовари 20.0 метал на Нова Терра
Нова Терра: метал 120.0, газ 0.0, кристал 0.0, уран 0.0
Кобалт: метал 0.0, газ 0.0, кристал 0.0, уран 0.0

3. Симулация на 10 хода. Без играч: играта има поредица сгради (Метална мина, Кристална мина, Лаборатория, Газова рафинерия). На всяка стъпка опитва да построи следващата. Ако не става, чака един ход. След всяка стъпка печата състоянието, докато ходовете станат 10.

Изход
Метална мина е готова.
ход 1, точки 10 | метал 35.0, газ 0.5, кристал 13.2, уран 0.1
Кристална мина е готова.
ход 2, точки 20 | метал 29.0, газ 1.0, кристал 12.4, уран 0.2
Лаборатория е готова.
ход 4, точки 30 | метал 21.0, газ 2.0, кристал 9.2, уран 0.4
Газова рафинерия е готова.
ход 6, точки 40 | метал 15.0, газ 3.0, кристал 7.0, уран 0.6
ход 7, точки 40 | метал 17.0, газ 4.0, кристал 7.4, уран 0.7
ход 8, точки 40 | метал 19.0, газ 5.0, кристал 7.8, уран 0.8
ход 9, точки 40 | метал 21.0, газ 6.0, кристал 8.2, уран 0.9
ход 10, точки 40 | метал 23.0, газ 7.0, кристал 8.6, уран 1.0

Още задачи

  • Уранова мина. Добави UraniumMine, която зависи от Газовата рафинерия.
  • Точки за ресурси. Давай по 1 точка за всеки 50 добити метала.
  • Пирати. На всеки 10 хода пирати отнемат 20% от метала, ако на планетата няма кораб със сила на оръжията поне 10.

Тест

Провери се

1. Металът се добива с 1,0 на ход. Строиш Метална мина и я надграждаш до ниво 3. Колко метал на ход дава планетата?
2. Защо spend първо проверява всичките четири ресурса и чак после харчи?
3. Кога свършва играта в урока?
4. Лабораторията иска първо Кристална мина. С какво construct отказва в JavaScript?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·