Към съдържанието
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.
  • display_info() е общ за всички. Видът (get_kind()) е абстрактен: всеки наследник казва сам какъв е.
  • _move_to започва с долна черта: това е уговорката за „само за класа и наследниците му“. Python не забранява викането отвън, но долната черта казва „не пипай“. Същото важи за полетата self._name и останалите, а отвън се четат през @property.

Абстрактният клас наследява ABC, а абстрактният метод е с @abstractmethod над него и pass в тялото. Python не дава да се създаде обект от клас, който още има абстрактен метод без тяло:

class Comet(SpaceObject):
pass
try:
comet = Comet("Халей", 0, 0, 0, 0)
except TypeError:
print("TypeError: Comet няма get_kind и не може да стане обект")
Изход
TypeError: Comet няма get_kind и не може да стане обект

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

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 "Звезда";
}
}

Конструкторът се наследява: 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;
}
}
  • Обикалящите са в списък, който расте сам. Брояч не трябва: len(self._orbiters) казва колко са.
  • Около една звезда има място за 10 обекта: CAPACITY е на класа, общ за всички обекти от него. Ако мястото се запълни, add_orbiter хвърля RuntimeError: готовото изключение, когато действието не става в този момент.
  • get_orbiters() връща копие на списъка.
По-точно, за любопитните
  • get_orbiters() връща копие, а не самия списък. Иначе код отвън може да добави с append и да заобиколи проверката за място.
  • Star наследява OrbitableSpaceObject, а не направо SpaceObject. Така звездата получава и обикалящите, и всичко от SpaceObject.

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

sun = Star("Слънце", 0, 0, 0, 333000)
Planet("Земя", 149.6, 0, 0, 1).orbit(sun)
copy = sun.get_orbiters()
copy.append(Planet("Марс", 227.9, 0, 0, 0.11))
print(len(copy), len(sun.get_orbiters()))
Изход
Земя обикаля около Слънце
2 1

Идеята

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

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

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

Python няма отделна дума за интерфейс. Договорът е абстрактен клас само с абстрактни методи, а класът, който го изпълнява, го наследява до родителя си: class Planet(OrbitableSpaceObject, Orbiter). Python позволява да наследиш няколко класа наведнъж.

Ако правилото е нарушено, 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() + ": планета обикаля само звезда");
}
}
  • isinstance(target, Star) проверява типа. След проверката target се ползва като звезда: нова променлива не трябва.
  • Ако проверката не мине, изключението спира метода. Планетата не се добавя никъде.
  • Satellite има същия orbit, само че проверява за Planet.
По-точно, за любопитните

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

Виж го

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

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

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

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

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

Пробвай

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

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

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

def try_orbit(orbiter, target):
try:
orbiter.orbit(target)
except NotValidSpaceObjectError as e:
print(f"Отказано: {e}")
sun = Star("Слънце", 0, 0, 0, 333000)
earth = Planet("Земя", 149.6, 0, 0, 1)
mars = Planet("Марс", 227.9, 0, 0, 0.11)
moon = Satellite("Луна", 150, 0, 0, 0.012)
try_orbit(earth, sun)
try_orbit(mars, earth)
try_orbit(moon, earth)
print(f"Около Слънце: {len(sun.get_orbiters())}")
print(f"Около Земя: {len(earth.get_orbiters())}")
Отговор

Идеята

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

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

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

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

@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
Отказано: Скиталец не може да лети до Слънце: звездата ще го изгори

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

По-точно, за любопитните
Един клас може да наследи няколко договора. Спътник за разузнаване, който може и да лети, би бил class Probe(SpaceObject, Orbiter, Fly).

Пробвай

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

Луната обикаля около Слънцето, а не трябва. На кой ред в 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. Разстояние. Напиши def 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. Цялото дърво. Промени display_info така, че звездата да показва и планетите си, а всяка планета и спътниците си, с два интервала отстъп на ниво. Подсказка: параметър за отстъпа със стойност по подразбиране, 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, но не пише get_kind. Какво става при Comet("Халей", 0, 0, 0, 0)?
2. Защо get_orbiters() връща копие на списъка, а не самия списък?
3. Спътник опитва moon.orbit(sun). Какво става?
4. Защо Orbiter в Python е абстрактен клас само с един абстрактен метод?

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