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

Epic Cosmos 1: обектите в космоса

Проблем

Космическа надпревара

Човечеството вече може да лети до далечни планети. Корпорации и държави се надпреварват за ресурсите им. Ти управляваш една от мисиите и искаш да станеш най-силната сила в космоса.

Правилата на Epic Cosmos:

  • Точки се трупат с откриване на нови планети и обекти, добив на ресурси, строеж на сгради и кораби, научни открития.
  • Обектите са звезди, планети и спътници. Звездата дава енергия. Някои планети имат редки ресурси, а атмосферата им решава какво може да се строи. Спътникът е за връзка и разузнаване.
  • Ресурсите са нужни за строеж, проучвания и поддръжка на базите и корабите.
  • Сградите добиват ресурси, проучват технологии, пазят базата и правят кораби.
  • Технологиите отключват нови сгради, кораби и умения.
  • Конфликти: в космоса не си сам. Има пирати и други играчи.
  • Край: играта свършва, когато някой стигне определен брой точки или след определен брой ходове. Печели този с най-много точки.

Преди сградите и корабите ти трябва самият свят: какви обекти има, къде са и кой около кого може да обикаля. Това е урокът. Ресурсите, сградите и корабите са в следващия.

Идеята

Всичко в космоса е SpaceObject

Звездата, планетата и спътникът имат общо: име, координати x, y, z и маса. Общото отива в абстрактен клас:

abstract class SpaceObject {
private String name;
private double coordinateX;
private double coordinateY;
private double coordinateZ;
private double mass;
public SpaceObject(String name, double coordinateX, double coordinateY, double coordinateZ, double mass) {
this.name = name;
this.coordinateX = coordinateX;
this.coordinateY = coordinateY;
this.coordinateZ = coordinateZ;
this.mass = mass;
}
public String getName() {
return name;
}
public double getCoordinateX() {
return coordinateX;
}
public double getCoordinateY() {
return coordinateY;
}
public double getCoordinateZ() {
return coordinateZ;
}
public double getMass() {
return mass;
}
public abstract String getKind();
public void displayInfo() {
System.out.printf("%s %s: (%.1f, %.1f, %.1f), маса %.2f%n", getKind(), name, coordinateX, coordinateY, coordinateZ, mass);
}
protected void moveTo(double x, double y, double z) {
coordinateX = x;
coordinateY = y;
coordinateZ = z;
}
}
  • Координатите са в милиони километри от Слънцето, а масата е в земни маси: Земята е 1, Слънцето е 333 000.
  • displayInfo() е общ за всички. Видът (getKind()) е абстрактен: всеки наследник казва сам какъв е.
  • #name и другите полета с # са наистина скрити: отвън не се стига до тях, а се четат през get name(). Само че # не се вижда и в наследниците, а protected в JavaScript няма. Затова _moveTo е с долна черта: уговорка за „само за наследниците“, която JavaScript не пази.

JavaScript няма абстрактни класове и методи. Най-близкото е getKind() в родителя да хвърля грешка: наследникът, който не го напише, ще разбере при първото извикване. Обект от такъв клас се създава без проблем:

class Comet extends SpaceObject {}
const comet = new Comet("Халей", 0, 0, 0, 0);
try {
comet.displayInfo();
} catch (e) {
console.log(e.message);
}
Изход
Comet трябва да напише getKind()

Звездата е най-простият наследник:

class Star extends OrbitableSpaceObject {
public Star(String name, double coordinateX, double coordinateY, double coordinateZ, double mass) {
super(name, coordinateX, coordinateY, coordinateZ, mass);
}
@Override
public String getKind() {
return "Звезда";
}
}

Конструкторът се наследява: new Star(...) вика този на родителя със същите аргументи. Затова Star пише само вида си.

Трите вида обекти в един масив от SpaceObject. Цикълът не знае кой какъв е, а всеки казва вида си:

SpaceObject[] objects = {
new Star("Слънце", 0, 0, 0, 333000),
new Planet("Земя", 149.6, 0, 0, 1),
new Satellite("Луна", 150, 0, 0, 0.012),
};
for (SpaceObject object : objects) {
object.displayInfo();
}
Изход
Звезда Слънце: (0.0, 0.0, 0.0), маса 333000.00
Планета Земя: (149.6, 0.0, 0.0), маса 1.00
Спътник Луна: (150.0, 0.0, 0.0), маса 0.01

Идеята

Около кого се обикаля

Около звездата обикалят планети, около планетата обикалят спътници. И звездата, и планетата трябва да помнят кой обикаля около тях. Това е OrbitableSpaceObject: абстрактен клас между SpaceObject и тях.

abstract class OrbitableSpaceObject extends SpaceObject {
private SpaceObject[] orbiters = new SpaceObject[10];
private int orbiterCount = 0;
public OrbitableSpaceObject(String name, double coordinateX, double coordinateY, double coordinateZ, double mass) {
super(name, coordinateX, coordinateY, coordinateZ, mass);
}
public void addOrbiter(SpaceObject orbiter) {
if (orbiterCount == orbiters.length) {
throw new IllegalStateException("Около " + getName() + " вече обикалят " + orbiters.length + " обекта");
}
orbiters[orbiterCount] = orbiter;
orbiterCount++;
}
public SpaceObject[] getOrbiters() {
SpaceObject[] result = new SpaceObject[orbiterCount];
for (int i = 0; i < orbiterCount; i++) {
result[i] = orbiters[i];
}
return result;
}
}
  • Обикалящите са в масив, който расте сам. Брояч не трябва: this.#orbiters.length казва колко са.
  • Около една звезда има място за 10 обекта: CAPACITY е на класа, общ за всички обекти от него. Ако мястото се запълни, addOrbiter хвърля обикновен Error: отделно изключение за неподходящ момент в JavaScript няма.
  • getOrbiters() връща копие на масива.
По-точно, за любопитните
  • getOrbiters() връща копие, а не самия масив. Иначе код отвън може да добави с push и да заобиколи проверката за място.
  • Star наследява OrbitableSpaceObject, а не направо SpaceObject. Така звездата получава и обикалящите, и всичко от SpaceObject.

Копието се вижда в изхода: добавеното в него не стига до звездата.

const sun = new Star("Слънце", 0, 0, 0, 333000);
new Planet("Земя", 149.6, 0, 0, 1).orbit(sun);
const copy = sun.getOrbiters();
copy.push(new Planet("Марс", 227.9, 0, 0, 0.11));
console.log(copy.length, sun.getOrbiters().length);
Изход
Земя обикаля около Слънце
2 1

Идеята

Правилото за орбитите

Планета обикаля около звезда. Спътник обикаля около планета. Всичко друго е невалидно. Обектите, които могат да обикалят, имат общ договор:

interface Orbiter {
void orbit(SpaceObject target) throws NotValidSpaceObjectException;
}

JavaScript няма интерфейси. Договорът е уговорка: всеки обект, който може да обикаля, има метод orbit(target). Кодът, който го ползва, просто го вика, без да пита какъв е обектът. Това се казва duck typing: „ако ходи като патица и кряка като патица, е патица“.

Ако правилото е нарушено, orbit хвърля свое изключение. Името казва точно какво е станало:

class NotValidSpaceObjectException extends Exception {
public NotValidSpaceObjectException(String message) {
super(message);
}
}

Ето как го пази планетата:

@Override
public void orbit(SpaceObject target) throws NotValidSpaceObjectException {
if (target instanceof Star star) {
star.addOrbiter(this);
System.out.println(getName() + " обикаля около " + star.getName());
} else {
throw new NotValidSpaceObjectException(getName() + " не може да обикаля около " + target.getName() + ": планета обикаля само звезда");
}
}
  • target instanceof Star проверява типа. След проверката target се ползва като звезда: нова променлива не трябва.
  • Ако проверката не мине, изключението спира метода. Планетата не се добавя никъде.
  • Satellite има същия orbit, само че проверява за Planet.
По-точно, за любопитните

В JavaScript никой не следи кой какво хвърля. Нито договорът, нито orbit казват, че може да хвърли, и никой не кара викащия да го хване. Програмата ще спре, ако никой не го хване, затова в играта винаги го хващаме. Името на изключението завършва на Error, както при вградените (TypeError).

Виж го

Кой около кого

const sun = new Star("Слънце", 0, 0, 0, 333000);
const earth = new Planet("Земя", 149.6, 0, 0, 1);
const moon = new Satellite("Луна", 150, 0, 0, 0.012);
tryOrbit(earth, sun);
tryOrbit(moon, earth);
tryOrbit(moon, sun);
Стъпка по стъпка, с думи
  1. Три обекта: звезда, планета и спътник, всеки със свои координати и маса.
  2. Планета около звезда: правилото го позволява, Земята тръгва по орбитата.
  3. Спътник около планета: позволено. Луната обикаля Земята и се движи с нея.
  4. Спътник около звезда: orbit хвърля NotValidSpaceObjectError, а catch в tryOrbit печата защо.

Земята минава проверката на звездата и се добавя към обикалящите я. Луната минава проверката на Земята. Когато Луната опита да обикаля около Слънцето, проверката за Planet не минава. orbit хвърля и Луната остава около Земята.

Изключението се хваща в малка помощна функция. Тя пробва орбитата и при отказ казва защо:

static void tryOrbit(Orbiter orbiter, SpaceObject target) {
try {
orbiter.orbit(target);
} catch (NotValidSpaceObjectException e) {
System.out.println("Отказано: " + e.getMessage());
}
}

catch в JavaScript няма вид в скобите: хваща всяка грешка, и тази от опечатка в кода. Затова tryOrbit проверява с instanceof. Отказът на орбитата го печата, а всяка друга грешка хвърля нататък с throw e.

Пробвай

Колко обикалят?

Марс опитва да обикаля около Земята.

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

function tryOrbit(orbiter, target) {
try {
orbiter.orbit(target);
} catch (e) {
if (!(e instanceof NotValidSpaceObjectError)) {
throw e;
}
console.log(`Отказано: ${e.message}`);
}
}
const sun = new Star("Слънце", 0, 0, 0, 333000);
const earth = new Planet("Земя", 149.6, 0, 0, 1);
const mars = new Planet("Марс", 227.9, 0, 0, 0.11);
const moon = new Satellite("Луна", 150, 0, 0, 0.012);
tryOrbit(earth, sun);
tryOrbit(mars, earth);
tryOrbit(moon, earth);
console.log(`Около Слънце: ${sun.getOrbiters().length}`);
console.log(`Около Земя: ${earth.getOrbiters().length}`);
Отговор

Идеята

Кораб, който лети

Корабите не обикалят, а летят от обект до обект. Това е друг договор:

interface Fly {
void flyTo(SpaceObject destination) throws NotValidSpaceObjectException;
}

SpaceShip наследява SpaceObject и има метод flyTo. До звезда не може да лети. До всичко друго лети и взима координатите на целта:

@Override
public void flyTo(SpaceObject destination) throws NotValidSpaceObjectException {
if (destination instanceof Star) {
throw new NotValidSpaceObjectException(getName() + " не може да лети до " + destination.getName() + ": звездата ще го изгори");
}
moveTo(destination.getCoordinateX(), destination.getCoordinateY(), destination.getCoordinateZ());
System.out.println(getName() + " пристигна при " + destination.getName());
}
SpaceShip ship = new SpaceShip("Скиталец", 149.6, 0, 0, 0);
Planet mars = new Planet("Марс", 227.9, 0, 0, 0.11);
Star sun = new Star("Слънце", 0, 0, 0, 333000);
try {
ship.flyTo(mars);
ship.displayInfo();
ship.flyTo(sun);
ship.displayInfo();
} catch (NotValidSpaceObjectException e) {
System.out.println("Отказано: " + e.getMessage());
}
Изход
Скиталец пристигна при Марс
Кораб Скиталец: (227.9, 0.0, 0.0), маса 0.00
Отказано: Скиталец не може да лети до Слънце: звездата ще го изгори

Вторият displayInfo() не се изпълнява: изключението прескача до catch.

По-точно, за любопитните
extends приема само един родител. Спътник за разузнаване, който може и да лети, е class Probe extends SpaceObject с двата метода, orbit и flyTo: за duck typing това стига.

Пробвай

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

Луната обикаля около Слънцето, а не трябва. На кой ред в orbit на спътника е грешката?

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

Упражнение

Космосът в код

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

1. Слънчевата система. Опиши осемте планети с три масива: имена, разстояния от Слънцето (милиони км) и маси (земни маси). В цикъл създай всяка планета на x = разстоянието и я накарай да обикаля около Слънцето. После покажи всички обикалящи.

Планета Разстояние Маса
Меркурий 57,9 0,06
Венера 108,2 0,82
Земя 149,6 1
Марс 227,9 0,11
Юпитер 778,5 317,8
Сатурн 1432 95,2
Уран 2867 14,5
Нептун 4515 17,1
Изход
Меркурий обикаля около Слънце
Венера обикаля около Слънце
Земя обикаля около Слънце
Марс обикаля около Слънце
Юпитер обикаля около Слънце
Сатурн обикаля около Слънце
Уран обикаля около Слънце
Нептун обикаля около Слънце
Около Слънце обикалят 8 планети:
Планета Меркурий: (57.9, 0.0, 0.0), маса 0.06
Планета Венера: (108.2, 0.0, 0.0), маса 0.82
Планета Земя: (149.6, 0.0, 0.0), маса 1.00
Планета Марс: (227.9, 0.0, 0.0), маса 0.11
Планета Юпитер: (778.5, 0.0, 0.0), маса 317.80
Планета Сатурн: (1432.0, 0.0, 0.0), маса 95.20
Планета Уран: (2867.0, 0.0, 0.0), маса 14.50
Планета Нептун: (4515.0, 0.0, 0.0), маса 17.10

2. Разстояние. Напиши function distance(a, b) с теоремата на Питагор в три измерения: корен от сбора на квадратите на разликите по x, y и z. Земята е в (149,6; 0; 0), Марс е в (0; 227,9; 0), Луната е в (149,6; 0,4; 0), а Слънцето е в началото.

Изход
Земя – Марс: 272.6 млн. км
Земя – Луна: 0.4 млн. км
Слънце – Марс: 227.9 млн. км

3. Цялото дърво. Промени displayInfo така, че звездата да показва и планетите си, а всяка планета и спътниците си, с два интервала отстъп на ниво. Подсказка: параметър за отстъпа със стойност по подразбиране, indent = "", за да става и извикването без аргумент. OrbitableSpaceObject презаписва метода и вика родителя. Земята и Марс обикалят Слънцето, Луната обикаля Земята, Фобос обикаля Марс.

Изход
Земя обикаля около Слънце
Марс обикаля около Слънце
Луна обикаля около Земя
Фобос обикаля около Марс
Звезда Слънце: (0.0, 0.0, 0.0), маса 333000.00
Планета Земя: (149.6, 0.0, 0.0), маса 1.00
Спътник Луна: (150.0, 0.0, 0.0), маса 0.01
Планета Марс: (227.9, 0.0, 0.0), маса 0.11
Спътник Фобос: (227.9, 0.0, 0.0), маса 0.00

Още задачи

  • Двоен опит. Сега Земята може да се добави два пъти към обикалящите Слънцето. Направи така, че втори опит да хвърля NotValidSpaceObjectError.
  • Пълна звезда. Добави 11 планети към една звезда и хвани изключението при единадесетата.
  • Сонда. Направи Probe, която и обикаля, и лети (виж „По-точно“ при корабите).

Тест

Провери се

1. Comet наследява SpaceObject, но не пише getKind. Кога ще разбереш?
2. Защо getOrbiters() връща копие на списъка, а не самия списък?
3. Спътник опитва moon.orbit(sun). Какво става?
4. В JavaScript няма interface. Какво позволява на tryOrbit да работи и с Planet, и със Satellite?

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