...

суббота, 24 августа 2019 г.

Куда катится Сеть

Лирика

В последнее время ловлю себя на том, что в своих рассуждениях о глобальных тенденциях, которые собираюсь где-то систематизировано изложить, всё чаще проскакивает мысль вроде «а вот в наше время молодёжь ...». Даже не буду ссылаться на Папирус Присса и весь тот легендариум, который обычно сопровождает реминисценции на эту тему — если и есть в нашем мире что-то стабильное и незыблемое, так это рассуждения старшего поколения о том, что младшее уже какое-то «не такое», как они привыкли.

Жаловаться на это смысла нет, да и некому. Более того, нашим детям предстоит надстраивать этот мир, входить в него, жить и творить в нём. И кто как не родившиеся в нём и в него, могут знать, что им нужнее и в чём они там будут лучше нас.

Но это всё лирическая преамбула. На самом деле я хотел бы сегодня окинуть ретроспективным взглядом историю развития Интернета. Но не в набивших оскомину технических подробностях, о которых вам на любом уроке информатики расскажут — ARPANET и всё прочее, нет. Интересен будет интегральный взгляд пользователей на Всемирную Сеть. Причём взгляд не профессионалов, а тех, кто далёк от её технических аспектов.


Физика

Итак, дело в том, что IT-специалисты обладают некой инерционностью мышления и привычек. Они зачастую предпочитают пользоваться инструментами, принципы действия которых морально (но не технологически!) устарели; и многие даже могут внятно обосновать, почему это на самом деле удобнее, чем то, чем и как пользуются «простые смертные». Закавыка тут, конечно же, в определении «удобства», но я сейчас не об этом. Заранее прошу прощения за некоторые вольности в хронологии и упрощения в трактовках.

Вот характерный пример — электронная почта. Пока ещё более-менее привычная всем штука, не будем о грустных мыслях о том, что она постепенно отмирает и замещается иными средствами связи. Достаточно большое количество бизнес-процессов и личных коммуникаций всё ещё идёт через электронную почту, и зачастую это доходит до абсурда.
В представлении современного пользователя использование электронной почты предельно упрощено — надо «просто зайти» куда-то, где живёт та часть вашего адреса, которая «после собачки» @ и там вам покажут удобный и отсортированный по категориям список писем, список категорий слева и обязательное хвастовство на тему сколько гигабайт для вашей почты совершенно бесплатно этот конкретный сервис вам с радостью подарил, лишь бы вы смотрели именно его рекламу. Там же будут удобные кнопочки для создания нового сообщения, адресная книга и всё прочее.

С точки же зрения человека, который свой первый электронный адрес зарегистрировал ещё в середине 1990-х и досконально знает все процессы, которые происходят от момента нажатия на кнопку «Отправить» до показа вашего сообщения адресату — за каждым из этих упрощений стоит небольшая технологический «рывок», отражающийся на восприятии всего взаимодействия с почтой.

Вот то, что любое ваше устройство в любой момент времени связано с Интернетом и то, что ваша электронная почта, то есть все письма со всеми вложениями хранятся не на вашем устройстве, а удалённо, на сервере — это данность относительно нового времени. В былые времена приходилось сначала запускать специальную отдельную программу для работы с электронной почтой, в ней устанавливать связь с сервисом, потом по этой связи скачивать (!) всю пришедшую для меня почту локально на мой компьютер (с сервера она при этом удалялась), потом отключать связь, а потом уже в этой же программе просматривать, что же там пришло, но уже в оффлайне. Кстати, такие программы есть и сейчас и все основные сервисы электронной почты поддерживают эту технологию до сих пор. И удобнее это кажется тем, для кого удобство заключается в возможности максимально тонкой настройки рабочего процесса и окружения под свои потребности и представления, а не в готовности покорно пользоваться шаблонами, которые нарисовал какой-нибудь дядя Субраманян в Gmail и где пик дружественности интерфейса — возможность переключить тему с чёрно-белой на бело-чёрную.

Но электронная почта — лишь один, хоть и характерный, пример. На самом же деле схожие процессы происходят буквально со всем, что есть в Сети.


Web-арифметика

Лет пятнадцать назад среди аналитиков, упражняющихся на эту тему, было модно рассуждать о так называемом Web 2.0. Я вкратце опишу, что это такое, но начать надо с того, что было до него. В Web 1.0, то есть в изначально появившейся всемирной Сети, информация была организована на усмотрение администраторов сайтов. Была, так сказать, resource-centric.

Попробуйте представить себе Интернет без Google. И без Yandex, Rambler, Bing, DDG и всех прочих поисковиков. Поиска в Интернете вообще нет, как такового. Страшно? То-то же. Как искать информацию? Существовали специальные сайты-каталоги. Куда информация вносилась вручную (!). Например, создал ты сайт, посвящённый смешным котикам и пишешь в такой каталог сообщение — разместите, пожалуйста, ссылку на мой сайт в категории «Домашние животные» — подкатегория «Котики» — подкатегория «Смешные». Его размещают. А пользователи заходят и смотрят — вот, в сети есть целых три сайта, посвящённых смешным котикам и ещё два — не смешным. Сайты-каталоги, представляете себе? Тоже целых три или четыре штуки на всю Сеть.

Да, потом появился поиск, и каталогизация исчезла — сайтов и информации стало появляться слишком много и слишком быстро, люди уже не справлялись. Но это всё ещё был Web 1.0 — то есть сеть, в которой новый контент появляется на новых сайтах, которые наполняются их владельцами. Пользователи выступали исключительно потребителями информации. Это было что-то вроде печатных газет — что будет опубликовано, решает редакция газеты исходя из того, что написали авторы или принесли репортёры, и письма читателей не публиковались в газетах автоматически.

Так вот, Web 2.0 наступил тогда, когда сайты стали площадками для генерации контента, а генераторами контента стали сами пользователи. Началось всё это с имиджборд, форумов и веб-чатов, продолжилось в современных соцсетях и видеосервисах — всё, что вы в них видите, написано, снято или создано кем-то, кто не имеет к самому сервису никакого отношения, это всё такие же, как и вы, пользователи. Сеть стала user-centric, если можно так выразиться, то есть построенной вокруг пользователя и его контента, а не вокруг ресурса. Вот эта смена парадигмы и называется Web 2.0.

С формальной точки зрения, Web 2.0 продолжается и сейчас. Это, конечно же, не мешает фантазировать на тему, какой могла бы быть следующая кардинальная смена парадигмы и какие черты будут у грядущего Web 3.0. Лет 5 назад я даже написал на Habr статью, где изложил казавшиеся мне тогда вполне реалистичными предположения на эту тему. Как сейчас понимаю — под впечатлением от моего тогдашнего увлечения одноранговыми сетями вышло чересчур наивно и натянуто, за что меня читатели вполне справедливо раскритиковали и насовали минусов.

На этот раз я воздержусь от столь глобальных прогнозов, а попробую обратить ваше внимание на отдельные признаки. Хоть и незначительные, но они, на мой взгляд, выступают предвестниками будущей эпохи, следующего этапа эволюции Сети.

Все эти признаки характеризует то, что я назову «девальвацией понятий», то есть таким процессом, когда сущности и понятия, задуманные в технологическом процессе, как отдельные, с течением времени искажаются, исчезают или заменяют собой друг друга.


Первый признак

Заключается в том, что обычные пользователи вообще подразумевают под словом «Интернет». Останови первого встречного и задай такой вопрос, и он выдаст какую-то банальщину типа «всемирная сеть». Но вот на уточняющий вопрос, из чего Интернет состоит, вас, скорее всего, ждёт ответ, похожий на статистику топ-10 самых посещаемых веб-сайтов.

Между тем, академическое определение компьютерной сети, хоть всемирной, хоть локальной, более скучно — это совокупность узлов, связанных между собой каналами передачи информации. Любой информации, прошу заметить. Здесь нет и не может быть никаких оговорок, ограничивающих определение компьютерной сети применяемыми в ней протоколами, опубликованными сервисами да и их назначением в общем случае. Интернет одинаково хорошо работает для игрока в Minecraft, для ведущего Telegram-канала, для скачивающего через BitTorrent любимый фильм ну и для завсегдатая Facebook, конечно же. А между тем, всё перечисленное является существенно отличными друг от друга технологиями.

Вы наверняка уже догадались, к чему я веду. Да, Тим Бернерс-Ли и его WWW в начале 1990-х годов. Обратили ли вы внимание, когда Интернет перестали называть всемирной паутиной (world-wide web)? Или, когда из адресов сайтов стали массово пропадать префиксы «www»? Вот ровно тогда, когда произошла та самая девальвация понятия, и один-единственный сервис из многих стал синонимичным самому Интернету. И сейчас почти никто уже толком не помнит, что такое это самое «www» и зачем оно было нужно.

И что вы думаете, процесс девальвации понятия Интернет на этом закончился? Как бы не так. Вот вам исследование 2015 года, в котором среди жителей Мьянмы, Индонезии, Филиппин и Тайланда был проведён опрос — пользуются ли они Интернетом и пользуются ли они Facebook. В результате пользователей Facebook оказалось значительно больше пользователей Интернета.

Или вот, менее очевидное явление из совсем свежих новостей здесь же на Хабре. Оказывается, в более чем половине (!) случаев по результатам поиска в Google никто не переходит! Хватает, видимо, и тех фрагментов информации, которую он выдаёт на самой странице результатов поиска. С такой тенденцией как скоро Google станет синонимом Интернета для людей, не искушённых в технических деталях работы поискового сервиса?


Второй признак

Это девальвация самого понятия адреса веб-сайта. Это произошло относительно недавно, когда для доступа на интересующий ресурс уже не надо было трепетно вбивать в адресную строку какой-нибудь www.ebay.com, следя за раскладкой и за тем, чтобы не вбить запятую вместо точек. Сначала в браузерах параллельно со строкой адреса появилась строка поиска. Потом какая-то светлая голова додумалась вообще совместить эти строки в одну. Сейчас, смотря на свежую версию FireFox, я вижу на стартовой странице целых три идентичных по своим функциям строки поиска. Чтобы уж наверняка.

Ну и всё — пошли анекдоты о том, как незадачливые пользователи «гуглят яндекс». То есть, в буквальном смысле, вбивают в строку поиска слово «гугл», а в появившемся Google уже пишут «яндекс» и переходят куда надо. И сопротивляются попыткам как-то нормализовать подобный техпроцесс, мол, «мы так привыкли».

В массовом сознании произошёл отрыв сайта и его контента от адреса. Вводить адрес стало не нужно. В глазах пользователей Интернет стал больше семантическим, чем доменным.


Третий признак

Наконец, дело времени нынешнего, когда мобильные устройства с постоянным доступом к Сети по своей численности прочно заняли лидирующие позиции впереди своих старших братьев — планшетов, ноутбуков и десктопов. Тут можно не углубляться в историю, а просто привести в пример пару ситуаций. Дочка интересуется, как ей на компе поиграть в ту же любимую игру, которую она перед этим скачала на свой телефон с Android. И я далеко не сразу нашёл ответ, который был бы в равной степени и убедительным и лишённым ненужных технических подробностей. А ещё недавно настраивал сестре купленный ею новый ноутбук и на мой вопрос, какой софт ей на него установить, кроме стандартного набора, последовал уверенный ответ — «Instagram и Facebook». При этом она вполне современный, уверенный и грамотный пользователь.

От такого ответа я сначала опешил, а потом глубоко задумался — а ведь в этом есть своя логика, в контексте того, что написано выше. Интернет как Сеть сначала становится паутиной адресуемых ресурсов одного сервиса, вытесняя из массового восприятия все остальные. Потом эти ресурсы теряют привязку к адресам, то есть к семантическому свойству, отличающему их от всех остальных, таким образом становясь для многих синонимом самой Сети. Потом вопрос «каким браузером вы пользуетесь для работы в Интернете» начинает вызывать у отвечающего заметные трудности ещё как минимум лет десять назад, когда название браузера перестало для пользователя играть существенную роль в работе с ресурсом. Наконец, эта работа приобретает характер такой однородности, при которой стираются границы между приложением и посредником в виде самого браузера.


Итого

Таким образом, популярные ресурсы становятся настолько абстрагированными и «облачными» что, может быть, вообще избавятся от посредников доступа в недалёком будущем? Как бы это могло выглядеть с точки зрения сегодняшних технологий?

Let's block ads! (Why?)

Настройка состава JUnit5 тестов с помощью application.properties

Представьте себе ситуацию, когда ваш проект должен компилироваться в различных окружениях.

Теперь представьте, что не все тесты должны проходить в этих окружениях — кажому свой набор тестов.

И предпочтительней настроить выбор, какие тесты должны выполняться, в… файле application.properties — кажому тесту свой переключатель "вкл/выкл".

Звучит здорово, не правда ли?

Тогда добро пожаловать под кат, где мы все это и реализуем с помощью SpringBoot 2 и JUnit 5.

Сперва давайте выключим JUnit 4, который поставляется в SpringBoot 2 по-умолчанию, и включим JUnit 5.

Для этого внесем изменения в pom.xml:

<dependencies>
    <!--...-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.3.2</version>
        <scope>test</scope>
    </dependency>
    <!--...-->
</dependencies>

Мы хотим аннотировать каждый тест простой аннотацией со свойством, указывающим на то, включен ли тест или нет. Напомню, что значения этого свойства мы собираемся хранить в файле application.properties.


Аннотация

Создадим аннотацию:

@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(TestEnabledCondition.class)
public @interface TestEnabled {
    String property();
}

Обработка аннотации

Без обработчика аннотации не обойтись.

public class TestEnabledCondition implements ExecutionCondition {

    @Override
    public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
        Optional<TestEnabled> annotation = context.getElement().map(e -> e.getAnnotation(TestEnabled.class));

        return context.getElement()
                        .map(e -> e.getAnnotation(TestEnabled.class))
                        .map(annotation -> {
                            String property = annotation.property();

                            return Optional.ofNullable(environment.getProperty(property, Boolean.class))
                                    .map(value -> {
                                        if (Boolean.TRUE.equals(value)) {
                                            return ConditionEvaluationResult.enabled("Enabled by property: "+property);
                                        } else {
                                            return ConditionEvaluationResult.disabled("Disabled by property: "+property);
                                        }
                                    }).orElse(
                                            ConditionEvaluationResult.disabled("Disabled - property <"+property+"> not set!")
                                    );
                        }).orElse(
                                ConditionEvaluationResult.enabled("Enabled by default")
                        );
    }
}

Необходимо создать класс (без аннотации Spring-а @Component), который реализует интерфейс ExecutionCondition.

В этом классе необходимо реализовать один метод этого интерфейса — ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context).

Этот метод принимает контекст выполняемого JUnit теста и возвращает условие — должен ли тест быть запущен или нет.

Прочитать подробней по условное выполнение тестов JUnit5 можно в официальной документации.

Но как нам проверить значение свойства, которое прописано в application.properties в таком случае?


Получение доступа к контексту Spring из контекста JUnit

Вот таким образом мы можем получить окружение Spring, с которым был запущен наш JUnit тест, из ExtensionContext.

Environment environment = SpringExtension.getApplicationContext(context).getEnvironment();

Можете взглянуть на полный код класса TestEnabledCondition.

Давайте создадим несколько тестов и попробуем управлять их запуском:

@SpringBootTest
public class SkiptestApplicationTests {

    @TestEnabled(property = "app.skip.test.first")
    @Test
    public void testFirst() {
        assertTrue(true);
    }

    @TestEnabled(property = "app.skip.test.second")
    @Test
    public void testSecond() {
        assertTrue(false);
    }

}

Наш application.properties файл при этом выглядит так:

app.skip.test.first=true
app.skip.test.second=false

Итак...

Результат запуска:

Писать перед каждым тестом полные названия свойств из application.properties — утомительное занятие. Поэтому резонно их префикс вынести на уровень класса тестов — в отдельную аннотацию.

Создадим annotation для хранения префиксов — TestEnabledPrefix:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestEnabledPrefix {
    String prefix();
}

Обработка и использование аннотации TestEnabledPrefix

Приступим к обработке новой аннотации.


Давайте создадим вспомогательный класс AnnotationDescription

С помощью этого класса мы сможем хранить имя свойства из application.properties и его значение.

public class TestEnabledCondition implements ExecutionCondition {

    static class AnnotationDescription {
        String name;
        Boolean annotationEnabled;
        AnnotationDescription(String prefix, String property) {
            this.name = prefix + property;
        }
        String getName() {
            return name;
        }
        AnnotationDescription setAnnotationEnabled(Boolean value) {
            this.annotationEnabled = value;
            return this;
        }
        Boolean isAnnotationEnabled() {
            return annotationEnabled;
        }
    }

    /* ... */
}

Нам этот класс пригодится, т.к. мы собираемся использовать lambda-выражения.


Создадим метод, который извлечет нам значение свойства "префикс" из аннотации класса TestEnabledPrefix

public class TestEnabledCondition implements ExecutionCondition {

    /* ... */

    private AnnotationDescription makeDescription(ExtensionContext context, String property) {
        String prefix = context.getTestClass()
                .map(cl -> cl.getAnnotation(TestEnabledPrefix.class))
                .map(TestEnabledPrefix::prefix)
                .map(pref -> !pref.isEmpty() && !pref.endsWith(".") ? pref + "." : "")
                .orElse("");
        return new AnnotationDescription(prefix, property);
    }

    /* ... */

}

И теперь проверим значение свойства из application.properties по имени, указанном в аннотации теста

public class TestEnabledCondition implements ExecutionCondition {

    /* ... */

    @Override
    public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
        Environment environment = SpringExtension.getApplicationContext(context).getEnvironment();

        return context.getElement()
                .map(e -> e.getAnnotation(TestEnabled.class))
                .map(TestEnabled::property)
                .map(property -> makeDescription(context, property))
                .map(description -> description.setAnnotationEnabled(environment.getProperty(description.getName(), Boolean.class)))
                .map(description -> {
                    if (description.isAnnotationEnabled()) {
                        return ConditionEvaluationResult.enabled("Enabled by property: "+description.getName());
                    } else {
                        return ConditionEvaluationResult.disabled("Disabled by property: "+description.getName());
                    }
                }).orElse(
                        ConditionEvaluationResult.enabled("Enabled by default")
                );

    }

}

Полный код класса доступен по ссылке.


Использование новой аннотации

Теперь применим нашу аннотацию к тест-классу:

@SpringBootTest
@TestEnabledPrefix(property = "app.skip.test")
public class SkiptestApplicationTests {

    @TestEnabled(property = "first")
    @Test
    public void testFirst() {
        assertTrue(true);
    }

    @TestEnabled(property = "second")
    @Test
    public void testSecond() {
        assertTrue(false);
    }

}

Теперь наш код тестов стал чище и проще.


Хочу выразить благодарность пользователям reddit-а за их советы:

1) dpash за совет
2) BoyRobot777 за совет


PS

Статья является авторским переводом. Английский вариант опубликован в README.md файле рядом с кодом проекта.

Let's block ads! (Why?)

Проектируем космическую ракету с нуля. Часть 3 — Ужепочти-решение задачи двух тел

[Перевод] Новости из мира OpenStreetMap № 473 (06.08.2019-12.08.2019)

Logo

Подробный анализ правок в OpenStreetMap в Северной Корее 1 | style Mapbox – data OpenstreetMap contributors

Картографирование


  • На MapRoulette появилась новая задача: проверить до сих пор нетронутые дороги TIGER в штате Вашингтон. Они были импортированы в OSM 12 лет назад, но несмотря на это более 15 тысяч дорог до сих пор не проверены.
  • Юджин Элвин Виллар и другие члены филиппинского сообщества OSM с радостью заявили, что они будут использовать искусственный интеллект для улучшения качества карты Филиппин в OpenStreetMap. Подробнее об их инициативе Tabang-AI можно прочитать в WikiOSM.

Сообщество


  • Знаете ли вы, что когда каритируете в OSM, то вносите свой вклад в создание «классического вопроса в каноне цифровой картографии: изучение конкретного примера OpenStreetMap»? Нет? Мы тоже не знали, пока не прочитали статью Клэнси Уилмотта "Картирование с": Политика (контр)классификации в OpenStreetMap". Ее автор попробовал посмотреть на систему тегирования OpenStreetMap через призму критической картографии и пришел к выводу, что OSM следует стать более «амбивалентным и расплывчатым».
  • Региональный центр по картированию ресурсов в целях развития (РЦКРРР) провел курсы по использованию инструментов, данных и приложений с открытым исходным кодом. В нем приняли участие представители Королевства Эсватини, Ботсваны, Намибии, Южной Африки, Лесото, Замбии и Зимбабве. Сухит Ананд разрешил использовать всем его учебные и исследовательские материалы.
  • Пользователь Gunmac рассказал о том, как он провел в одной из индийских школ урок по географии
  • Винченцо Петито опубликовал анализ. дорожной сети в итальянском регионе Лигурия, который был выполнен им совместно с коллегами, до и после краха моста Моранди в Генуе. При проведении исследования использовались данные OSM. С целью повышения производительности библиотека JGraphT была модернизирована: теперь она поддерживает параллельные вычисления.
  • На сайте Christian Quest вы можете посмотреть по годам (с 2007 по 2011) каким был OSM. Правда, в качестве картостиля доступен только французский.
  • [1] Северная Корея является, пожалуй, одним из самых сложных мест в мире для картографирования. Пользователь Wonyoung So сделал достаточно подробный анализ правок OSM в Северной Корее, также он интересовался мотивацией картографов и тем, как они картируют.
  • Что такое “гео-информация, созданная волонтерами"? Именно над этим понятием и размышлял Кристоф Хорманн.
  • На филиппинской картографической конференции «Pista ng Mapa», которая состоялась в начале августа, несколько мероприятий были посвящены теме искуственного интеллекта. Арди Орден из компании Thinkin Machines рассказал об их проекте Map the Gap. Также прошел картатон, в ходе которого отрисовывались окрестности города Думагете (там проходила конференция), с помощью онлайн-редактора RapiD от Facebook'a и менеджера задач HOTOSM, использующих технологию ИИ. На закрытии конференции организаторы объявили о своей инициативе по отрисовке сельской местности на Филиппинах — Tabang-AI (с себуанского — сотрудничество, помощь). Предполагается, что они это будут делать с помощью ИИ.

Фонд OpenStreetMap


  • Фонд OpenStreetMap проводит расширенный опрос о сообществах, как местных, так и глобальном.

События


  • Бенни из Гейдельбергского Института геоинформационных технологий, рассказывает о семинаре по OSM в университете Йены, который был организован сотрудниками его института и членами исследовательской группы GIScience.
  • В начале августа в городе Думагете (Филиппины) состоялась первая филиппинская картографическая конфереция «Pista ng Mapa», которая была организована местными сообществами OpenStreetMap и FOSS4G. Один из местных активистов — GOwin — написал об этом событии подробный пост в своем дневнике на сайте OSM. За 2,5 дня конференцию посетило 166 человек.
  • Гейдельбергский Институт геоинформационных технологий на дне открытых дверей Федерального министерства иностранных дел Германии (16 и 17 августа) представил доклад (автоматический перевод) на тему гуманитарной помощи. Среди прочего, обсуждалась работа некоммерческой организации Missing Maps, которая работает с OSM.

Гуманитарный OSM


  • Гуманитарная команда OpenStreetMap (HOT) опубликовала свой стратегический план на 2019-2021 годы. HOT хочет начать работать еще быстрее, повысить качество своих данных и отрисовать в OSM ту территорию на планете, где проживает один миллиард человек.
  • Эммануил Курума написал пост о двухнедельном картатоне, который был организован GeoSynapse и некоммерческой организацией «Врачи без границ» с целью отрисовки дорог, тропинок и рек в гвинейских префектурах Сангиана и Курусса. В мероприятии приняло участие порядка 30 картографов из организации «GeoSynapse Гвинея».

Переходим на OSM


  • Кристиан Нуэссли из Schutz & Rettung Zürich (Гражданская защита Цюриха, Швейцария) в швейцарском списке рассылки сообщил (автоматический перевод), что они уже как год используют данные OpenStreetMap в своей системе управления. Также эта организация публично заявила, что она будет совершенствовать карту OSM (даже создала специальную страницу в WikiOSM), а потому интересуется мнением соощества по этому поводу. В списке рассылки есть и другие примеры того, как OSM используется аварийными службами в других странах.

Открытые данные


  • Корпорация «Лондонский транспорт» (Transport for London) подготовила большую базу данных велосипедной инфраструктуры, находящейся в Большом Лондоне, и опубликовала ее в виде открытых данных. Сейчас компания CycleStreets из Кембриджа проверяет качество этих данных и возможность их использования в OpenStreetMap. В ближайшее время они должны сообщить о своих выводах.

Лицензии


  • Рабочая группа по лицензированию фонда OSM собрала в один документ все требования по указанию атрибуции OSM. В настоящее время идет разбор некоторых случаев использования, которые вроде бы за последние 7 лет стали привычными. Сообщение Саймона Пула в списке рассылки «OSM-Talk» по этому поводу вызвало дискусссию.

Программное обеспечение


Программирование


  • Опубликовано исследование (автоматический перевод) о влиянии зеленых насаждений в городе на самочувствие жителей, которое было проведено учеными из мангейменского Центрального института психического здоровья совместно с исследователями геоинформатики из Гейдельбергского университета и Лаборатории психического здоровья Технологического института Карлсруэ. Участники исследования оценивали свое настроение в специальном мобильном приложении енсколько раз в день в течение недели. Испытуемые показали более высокий уровень благополучия в ситуациях, когда они были окружены зелеными насаждениями.
  • Фредерик Рамм предлагает ввести лимиты на загрузку в OSM API.По этому поводу имеется два тикета на GitHub: один в репозитории Rails port (уже закрыт), а другой — в репозитории Cgimap.
  • Оптимизация маршрута в целом решает «Задачу маршрутизации транспорта» (самым простым примером может служить широко известная «Задача коммивояжёра»). Более сложным примером может служить распределение товаров автопарком из нескольких транспортных средств в десятки мест. Задача усложняется за счет того, что у каждого актора есть свое временное окно, в которое он может работать: как у грузовика, так и у места разгрузки. В статье на «GIScience News Blog» рассматривается типичный сценарий распределения медицинских препаратов во время гуманитарной акции по ликвидации последствий одного из самых сильных тропических циклонов в Африке с использованием OpenRouteService и VROOM. и VROOM.
  • Пол Кернфельд рассказывает о преимуществах популярного формата PBF, в котором хранятся данные OSM. Мало того, что он гораздо компактнее, чем XML, так еще его структура позволяет работать с даннми намного быстрее.

Релизы


  • В новой версии оффлайн-навигатора Maps.Me, который использует данные из OSM, реализована поддержка Apple CarPlay. Это означает, что теперь Maps.Me можно запускать на автомобильных бортовых компьютерах, которые управляются этой ОС, и строить, например, маршруты.
  • В новой версии мобильного приложения StreetComplete v. 13.0 появилось несколько новый заданий, а также улучшены старые.

А знаете ли вы …


  • … о 7 лучших бесплатных спутниковых снимков в 2019 году?
  • … о списке проектов CartoCSS, которые можно использовать для стилизации данных в Tilemill?
  • … о списке совместных акций по редактированию?
  • … о роли ЦРУ в развитии Google Earth?

Другие «гео» события


  • Сообщество OpenStreetMap в Словении обратило внимание на самую большую печатную (автоматический перевод) карту города Любляны, сделанную на основе данных из OSM. Эта карта изготовлена компанией Tam-Tam для ориентирования людей по культурной жизни этого города. К сожалению, при этом не соблюдены должным образом условия атрибуции.
  • Компания Mapbox объявила об открытии филиала в Японии. Сейчас она совместно со своим японским партнером Zenrin'ом работает над новыми картами Японии для Yahoo!
  • Мэтт Браун целый месяц ходил по улицам Лондона с помощью Minecraft Earth, а потому он теперь точно знает, куда идет Microsoft с этой платформой.
  • Новая Зеландия не собирается мириться с тем, что она потеряла (если быть справедливым, то это случилось не в первый раз) титул страны, где есть самая кривая улица. На этот раз она уступила Уэльсу. Планы по возращению себе этого статуса включают: заливку бетона на Болдуин-Стрит в духе фильма «Англичанин, который поднялся на холм, а спустился с горы», что позволит юридически увеличить протяженность улицы или в противном случае поездку в Уэльс с лопатами.
  • Себастьян Грюнер рассказывает (автоматический перевод) о том, почему опасно полагаться на искусственый интеллект и автоматическое распознавание при создании карт. Бесчисленные небоскребы в Национальном парке Саксонская Швейцария, предложения проехаться по историческим маршрутам, которые сейчас официально закрыты по причине сохранения природы или маршрут для автомобиля, проложенный по лестнице — это лишь некоторые из результатов, с которыми может столкнуться каждый.
  • DC Rainmaker обновил свое руководство по добавлению бесплатных карт на устройства Garmin Edge Series. Теперь этот процесс стал намного проще благодаря сайту Garmin.OpenStreetMap.nl.
  • Европейское космическое агентство спонсировало разработку проекта Urban TEP. Это карта, отображающая влияние человека на окружающий мир, созданная с помощью данных, полученных с радиолокационных и оптических спутников. Исходная карта была создана на основе данных 2014-2015 гг. со спутников радиолокационного базирования Copernicus Sentinel-1 и мультиспектральных спутниковых снимков Landsat-8. Марк Алтавэл рассказывает, для каких целей могут пригодится эти данные.
  • Вы думаете, что вы — самый преданный картограф в мире? Скажите нам об этом, когда сможете победить семью Кассини. Джей Форман и Марк Купер-Джонс сделали видеоролик об истории первой в мире точной карты: Карты де Кассини.
  • В кампусе Массачусетского университета в городе Амхерст разработано программное обеспечение под названием DeepRoof, которое, по словам разработчиков, на данный момент с точностью в 91% высчитывает потенциал использования той или иной крыши для солнечных батарей. Достигается это путем использования широко доступных (и дешевых) спутниковых данных таких сервисов, например, как Google Earth.



Общение российских участников OpenStreetMap идёт в чатике Telegram и на форуме. Также есть группы в социальных сетях ВКонтакте, Facebook, но в них в основном публикуются новости.

Присоединяйтесь к OSM!



Предыдущие выпуски: 472, 471, 470, 469, 468

Let's block ads! (Why?)

«Прячь www»: почему разработчики мейнстрим-браузера снова отказались от отображения поддомена

Рассказываем о причинах этого решения разработчиков Chrome и реакции сообщества.


Фото — Pawel Loj — CC BY / Фото изменено

Не первый раз


В сентябре прошлого года вышел Chrome 69. Вместе с новой версией из адресной строки браузера исчезло наименование поддоменов www и m. При переходе на сайт отображался только домен, например example.com.

Это решение вызвало негативную реакцию. ИБ-специалисты отмечали, что нововведение приведет к путанице с похожими друг на друга адресами. Под давлением общественности разработчики браузера вернули отображение поддоменов.

Но как выяснилось, ненадолго, — в Chrome 76, который вышел в конце июля, www вновь пропал.

Поддомен www назвали тривиальным. В Google считают, что эта информация не нужна большей части интернет-пользователей при серфинге. В интервью Wired представитель ИТ-компании рассказал, что теперь людям станет проще читать и понимать URL-адреса. Полностью их по-прежнему можно просмотреть целиком — нужно просто начать редактировать ссылку. По мнению разработчиков браузера, этого вполне достаточно для комфортной работы.

Мнение сообщества


Новость породила бурное обсуждение на Hacker News, в социальных сетях и на тематических форумах. Как и в прошлый раз, подавляющее большинство комментариев — негативные. Одна из главных претензий — потенциальная путаница с адресами. Условно, если пользователь введет URL www[.]pool.ntp.org, то в адресной строке отобразится хост pool.ntp.org, на котором нет сайта — он отвечает за выдачу случайного адреса NTP-сервера.

Есть и более серьезные проблемы, связанные с информационной безопасностью. Хотя разработчики браузера говорят, что нововведение должно сократить риск фишинга, специалисты по ИБ с ними не согласны. Исключение поддоменов, наоборот, увеличивает риск фишинговых атак, так как URL различных ресурсов начинают выглядеть одинаково.

Ситуацию наглядно продемонстрировал специалист по защите информации Иен Кэррол (Ian Carroll) на примере браузера Safari. Он также скрывает URL сайта и вместо него отображает имя компании из EV-сертификата (для расширенного подтверждения организации и домена). Хакеры могут получить новый сертификат для подставной фирмы, название которой совпадает с названием «жертвы», а рядовой пользователь не заметит подмены в адресе.

Проблему признают и представители интернет-провайдеров. Сотрудник одного из западных операторов отметил, что ему довольно часто приходится объяснять клиентам, что www[.]domain.com и domain.com — это два разных домена, которые могут вести на разные ресурсы. Он убежден, что с нововведением Chrome ситуация только усугубится.


Фото — Pablo García Saldaña — Unsplash

Один из резидентов Hacker News высказал предположение, что разработчики браузера не просто заботятся об удобстве пользователей, но преследуют и иные цели.

Упрощение URL может быть первым шагом компании по продвижению технологии AMP (Accelerated Mobile Pages). Её задача — увеличить скорость загрузки веб-страниц. Однако страницы эти загружаются с серверов Google, что приводит к отображению в адресной строке другого домена (с приставкой amp). Однако позже компания сможет скрыть поддомен amp, чтобы не вызывать замешательство у пользователей.

Что дальше


Некоторые участники обсуждения считают, что прошлогодняя история повторится, и под давлением общественности разработчики Chrome вновь отменят изменения. Но есть основания полагать, что на этот раз все будет по-другому.

Год назад компанию упрекнули в том, что сперва ей следовало позаботиться о стандартизации, а уже затем реализовывать изменения на практике. Инженеры учли замечание — недавно они представили новый стандарт, в котором описали правила упрощения URL. Более того, в Google призвали пользоваться этим стандартом разработчиков других браузеров — возможно, в будущем их примеру последует больше организаций.

О чем мы пишем в наших блогах и социальных сетях:

Как настроить HTTPS — поможет SSL Configuration Generator
Есть мнение: технология DANE для браузеров провалилась

Как защитить виртуальный сервер в интернете
Получение OV и EV сертификата: что нужно знать?
Персональные данные: особенности публичного облака

Подборка книг для тех, кто уже занимается системным администрированием или планирует начать
Как работает техподдержка 1cloud




Мы в 1cloud.ru используем железо Cisco, Dell, NetApp. Оно располагается сразу в нескольких ЦОД: DataSpace (Москва), SDN (Санкт-Петербург), Ahost (Алма-Ата).

Let's block ads! (Why?)

Зачем совершать операции с валютой на бирже: 3 практических сценария

Изображение: Unsplash

По статистике, уровень финансовой грамотности в России растет. Сейчас он немногим ниже, чем, например, в Великобритании. Все больше людей экспериментирует со своими финансами, изучает различные способы их сохранения и приумножения активов. При этом есть и устоявшиеся паттерны поведения – например, покупка валюты с целью сбережения денег или их защиты в моменты колебаний курса рубля.

Однако, не все знают, что решить эти задачи можно не только с помощью банков, но и на бирже. Сегодня мы поговорим о том, зачем это может быть нужно, и какие плюсы есть у такого способа.

Примечание: описанные в статье способы использования валютного рынка биржи сопряжены с рисками неблагоприятного изменения цены активов, это важно понимать. Помимо этого, для совершения описанных в топике операций придется открыть брокерский счет – сделать это можно в режиме онлайн (это бесплатно, также можно открыть тестовый счет с виртуальными деньгами для экспериментов и изучения торгового софта).

Немного фактов: курс на бирже лучше, чем в банке


Один из самых распространённых способов использования биржевого валютного рынка — совершение операций с валютой по более выгодному курсу, чем могут предложить банки.

Это возможно потому, что Банк России устанавливает официальные курсы валют на основе биржевой цены, достигнутой в ходе торгов. А уже на основе этого курса банки и обменные пункты формируют свои цены покупки/продажи. Им нужно заработать, поэтому они никогда не смогут предложить цену, совпадающую с биржевой. В случае с долларом и евро разница в ценах может достигать десятков копеек.

Источник данных: Банки.ру

Мы создали специальный калькулятор для расчета выгоды, которую можно получить при покупке валюты с помощью брокера на бирже. Так при покупке $5000 выгода может превысить 50 тыс. рублей:

Важный момент: на бирже можно как покупать реальную валюту, так и использовать ее для спекуляций и реализаций различных торговых стратегий. Для этого существуют два режима совершения операций — поставочный и беспоставочный. В первом случае после завершения сделки покупатель получает реальную валюту, а во втором «поставка» не происходит.

Есть два режима расчетов в поставочном режиме – в режиме T+0 расчеты по сделке проходят в день ее осуществления, а при T+1 расчет происходит на следующий день. Важный момент – если операции на бирже совершаются с целью покупки и вывода валюты, у покупателя не возникает обязательств по уплате налогов.

Подавать декларацию и платить налоги нужно лишь в том случае, если операции осуществлялись с целью получения прибыли от курсовой разницы.

График изменения цены валютной пары с расчетами сегодня (USDRUB_TOD) на «Московской бирже».

Для совершения операций на валютном рынке биржи понадобится открыть счёт – сделать это можно в режиме онлайн. Затем нужно будет установить торговый терминал (наши клиенты пользуются SMARTx), в котором уже осуществляется покупка и продажа валюты и других финансовых инструментов.

Обеспечение по другим сделкам


На бирже можно покупать не только акции или валюту, но и проводить операции с производными инструментами, например, фьючерсами и опционами. Гарантом исполнения обязательств сторонами таких сделок выступает биржа. Для этого она берет с каждого из участников специальные страховые депозиты – они называются гарантийным обеспечением (ГО).

В случае биржевого валютного рынка в качестве ГО инвесторы могут использовать купленную валюту – это очень удобно и упрощает процесс торговли.

В итоге покупателю не нужно сразу платить всю цену условного фьючерса, на счете замораживается лишь гарантийное обеспечение. Эта процедура проделывается и со счетом покупателя, и со счетом продавца в сделке.

Величину гарантийного обеспечения по каждому контракту высчитывает биржа. При этом, если на счете инвестора в какой-то момент средств станет меньше минимально допустимого уровня, то брокер направляет ему требование пополнить баланс, если же этого не происходит, то часть позиций будет закрыта принудительно (маржин-колл).

Таблица с указанием размера гарантийного обеспечения для торгующихся на Московской бирже фьючерсов

Чтобы избежать такой ситуации, трейдер должен держать на счете средства в количестве, значительно превышающем размер обеспечения — ведь если цена фьючерса сильно изменится, то его средств может не хватить для покрытия позиции. Гарантийное обеспечение замораживается на счете торговца до момента погашения сделки.

Альтернатива валютным вкладам в банке


Валютный рынок Московской биржи дает и возможность получать больший доход со своих валютных сбережений, чем предлагают банки со своими депозитами. К примеру, инвесторы могут использовать еврооблигации, в том числе российских компаний.

К примеру, доходность по облигациям иностранных компаний (Apple, Google и т.п.) достигает 6%.

Заключение


Валютный рынок биржи – гибкий инструмент, который позволяет не только получить возможность совершения операций по выгодному курсу, добиться более высокого процента по валютным сбережениям, но и упрощает процесс инвестирования. В следующих статьях мы уделим больше внимания именно инвестициям.

Полезные ссылки по теме инвестиций и биржевой торговли:


Let's block ads! (Why?)

[Из песочницы] Мини-справочник и руководство по Scrum

[Из песочницы] Управление несколькими шаговыми двигателями Nema 17 одновременно или NemaStepper

image

Всем привет.

Я думаю что вы, если работали с arduino+nema 17, знаете, что запустить несколько двигателей одновременно бывает очень затруднительно.

Есть разные способы решения этой проблемы, самый простой, пожалуй — использование библиотеки NemaStepper. Библиотека упрощает данную задачу во много раз, главное преимущество — она не останавливает выполнение программы. Устанавливается она также, как и все остальные библиотеки. Распространяется по MIT лицензии.
Ну что, давайте приступим. И начнем мы с подключения.

Мы будем использовать Simple Nema 17 с алиэкспресса за 500 рублей, драйвер L298N и arduino uno. Вот они:

image

image

image

  • В примере я буду показывать четыре подключенных драйвера к ардуине, хотя буду использовать только один.
  • В интернете есть много туториалов по подключению Nema 17, поэтому я не буду подробно расписывать это здесь.

Итак, подключаем мотор к драйверу:

image

image

Библиотека является объектно — ориентированной. Давайте рассмотрим пример включения одного мотора:

NemaStepper Stepper1(2, 3, 4, 5, 200, 10, false);
void Setup(){
   Stepper1.SetStepCount(100); //Запускаем вращение на 100 оборотов
}
void Update(){
   Stepper1.Step(); //Обновляем вращение
}


О всех методах библиотеки можно узнать из файлов исходного кода библиотеки (в шапке библиотеки есть описание).

*Подробнее о коде в примере.

А теперь переходим к примеру.

В библиотеке есть встроенный пример (на данный момент он там один), который позволяет управлять сразу тремя моторами с Serial.

Данный пример принимает на порт команды, указанные ниже.

Давайте его разберем.

Начнем с шапки — подключения библиотек:

//This file - example of NemaStepper library.
#include "NemaStepper.h"
String inString;
bool IsStepperEnabled = false;


Далее объявляются три мотора, со следующими параметрами:

1. Первый пин
2. Второй пин
3. Третий пин
4. Четвертый пин
5. Количество шагов за оборот — у большинства моторов Nema 17 это 200.
6. Стартовая скорость
7. Значение указывающее, нужно ли удерживать вал после остановки (при true драйверы превращаются в барбекю)

NemaStepper Stepper1(2, 3, 4, 5, 200, 10, false);
NemaStepper Stepper2(6, 7, 8, 9, 200, 10, false);
NemaStepper Stepper3(10, 11, 12, 13, 200, 10, false);


Далее инициализация порта:
void setup() {
  Serial.begin(9600);
}


Затем, ВАЖНО! В главном цикле нужно обновлять положение двигателей командой Step()
void loop() {
  if (IsStepperEnabled == true){
    Stepper1.Step();
    Stepper2.Step();
    Stepper3.Step();
  }
  GetCommandFromSerial();
}


Далее следует подпрограмма, которая получает данные с порта, включает/выключает моторы, задает скорость, тормоза, вращение.
void GetCommandFromSerial() {
  if (Serial.available() > 0) {  //если есть доступные данные
    int inChar = Serial.read();
    if (inChar == '/') {
      String command = ((String)inString[0] + (String)inString[1] + (String)inString[2]);
      String param;
      int len = inString.length();
      for (int i = 3; i < len; i++) {
        param = (String)param + (String)inString[i];
      }
      if (command == "MV1") {
        Stepper1.SetStepCount(param.toInt());
        Serial.println(param.toInt());
      }
      if (command == "MV2") {
        Stepper2.SetStepCount(param.toInt());
        Serial.println(param.toInt());
      }
      if (command == "MV3") {
        Stepper3.SetStepCount(param.toInt());
        Serial.println(param.toInt());
      }
      if (command == "SS1") {
        Stepper1.SetSpeed(param.toInt());
        Serial.println(param.toInt());
      }
      if (command == "SS2") {
        Stepper2.SetSpeed(param.toInt());
        Serial.println(param.toInt());
      }
      if (command == "SS3") {
        Stepper3.SetSpeed(param.toInt());
        Serial.println(param.toInt());
      }
      if (command == "SB1") {
        Stepper1.SetBrakes(param.toInt());
        Serial.println(param.toInt());
      }
      if (command == "SB2") {
        Stepper2.SetBrakes(param.toInt());
        Serial.println(param.toInt());
      }
      if (command == "SB3") {
        Stepper3.SetBrakes(param.toInt());
        Serial.println(param.toInt());
      }
      if (command == "EMS") {
        IsStepperEnabled = true;
        Serial.println(param.toInt());
      }
      if (command == "DMS") {
        IsStepperEnabled = false;
        Serial.println(param.toInt());
      }
      inString = "";
    } else {
      inString += (char)inChar;
    }
  }
}


И так, давайте попробуем загрузить ее в плату.

Загрузили?

Тогда заходим в монитор порта и вводим команды из кода.
Каждая команда заканчивается символом /.
Первые три символа — название команды.
То, что между названием и / — параметры.
Давайте включим моторы командой «EMS/» (Enable MotorS).
Затем укажем мотору 1 скорость 60 командой «SS160/» (Set Speed), где 60 — скорость.
И наконец, включим первый мотор командой «MV1100/», (MoVe) где 100 — количество оборотов.
Все работает. Ура.

Тоже самое с остальными моторами.

Ну и где взять библиотеку.

Библиотеку можно скачать, отблагодарив создателя, по ссылке, указав ей реальную цену:

Get NemaStepper

Спасибо за прочтение, надеюсь вам помогла моя статья.

Когда я искал решение моей проблемы, единственной подходящей библиотекой оказалась она.

Let's block ads! (Why?)

Natas Web. Прохождение CTF площадки, направленной на эксплуатацию Web-уязвимостей. Часть 4

[Из песочницы] Разработка многозадачной микроядерной ОС — Планировщик

После того, как вы прочитали базовые шаги по написанию Hello World ядра из цикла имеющихся на Хабре статей, самое время приступить к серьезной разработке самых базовых инструментов: аллокатора кучи и планировщика.

Честно говоря я долго думал стоит ли начинать писать статьи и делать видеоуроки на столь изьезженную тему. Но страсть к системному программированию и отсутствие структурированной информации на русском языке все же подтолкнули меня на этот эксперимент. Посему, если труд мой окажется востребованным, статьи планирую выпускать не реже чем раз в месяц и не чаще чем раз в неделю.
Свою ОС я мечтал написать еще в десятом классе. Книга Таненбаума обладает удивительным свойством. Всяк кто к ней прикоснется рано или поздно начинает мечтать написать свою операционную систему. Вот только я тогда ничего не написал, после того как понял что на мастдае невозможно нормально скомпилить и слинковать чистый бинарник с сишным кодом. Пришлось изучать Linux, поступать в универ, идти на работу. Все бы ничего. Да только мечта осуществилась лишь через десять лет. Сейчас, верстая скучные формочки на React, я оглядываюсь назад, вспоминаю, с какой страстью я просиживал часами за дизассемблером и отладчиком, снимал упаковщики, ломал крякмисы, на ночь вместо школьных романов зачитывался статьями мыщьха… и понимаю что моя жизнь могла бы сложиться иначе. Совсем иначе. Если бы только у меня был человек, который смог бы мне помочь в самом начале. Но время ушло. Программы ломать стало трудно. Ядро Linux разрослось до неимоверных размеров. Появились гиппервизоры. Ассемблер Intel стал настолько большим что взглянув на один список команд в мануале пропадает всякий энтузиазм что либо о нем изучать. Хлеб пришлось зарабатывать в вебе. Да. Мир системного программирования пережил свои золотые годы.

Посему всем школьникам, чей энтузиазм еще не иссяк, я посвящаю подробный туториал на тему как взять и с нуля написать простейшее многозадачное микроядро ядро со своей оболочкой. А всех, у кого он еще не появился, хочу предостеречь, что дело это неблагодарное и малополезное, хотя и жутко интересное. А у молодости, как вы знаете, свои заботы.

Поехали!

Данная статья содержит видеоурок снятый в моей домашней студии. Он посвещен непосредственно коду.

Приступим к разработке менеджера кучи.

extern void *kmalloc(size_t size);
extern void kfree(void *addr);


Для написания аллокатора кучи нужно хранить регионы свободных и занятых адресов. Проще всего это реализовать в виде направленного списка. Саму же реализацию направленого списка придется делать через статический массив.
struct slist_head_t
{
  struct slist_head_t *prev;
  struct slist_head_t *next;
  bool is_valid;
  void *data;
} attribute(packed);

struct slist_definition_t
{
  size_t base;
  u_int slots;
  size_t slot_size;
  struct slist_head_t *head;
  struct slist_head_t *tail;
};


Элементами такого ограниченного списка будут являться записи о регионах памяти. Причем между смежными регионами дырок быть не может. Если присутствует свободная память, она описывается отдельным элементом списка.
struct kheap_entry_t
{
  struct slist_head_t list_head; /* static (array placed) list */
  size_t addr;                   /* physical address */
  size_t size;                   /* memory block size */
  bool is_buzy;                  /* whether block used */
} attribute(packed);

struct kheap_entry_t kheap_blocks[KHEAP_MAX_ENTRIES];
struct slist_definition_t kheap_list = {
  .head = null,
  .tail = null,
  .slot_size = sizeof(struct kheap_entry_t),
  .slots = KHEAP_MAX_ENTRIES,
  .base = (size_t)kheap_blocks
};


В начале структуры kheap_entry_t стоит slist_head_t что позволяет безболезненно приводить тип записи кучи к элементу списка.

Теперь рассмотрим простейший алгоритм аллокатора кучи (kmalloc):

  1. Если нет свободных блоков, выделяем новый блок нужного размера (но не более чем максимальный размер кучи).
  2. В противном случае просматриваем все свободные блоки. Если мы нашли свободный блок, смотрим его размер. Если он достаточен, занимаем. Если есть излишек то создаем новый пустой блок размера излишка. Иначе, если же он недостаточен и последний, расширяем границу кучи (но не более максимума).

Алгоритм освобождения памяти будет похож (kfree):
  1. Найти блок адрес которого начинается с освобождаемого адреса. Проверить что он занят. Пометить как свободный.
  2. Если справа или слева есть свободный сосед обьединитьс с ним в один блок.

Следующая статья будет посвящена реализации алгоритма кучи.

Напишем простейший планировщик. Задача будет выглядеть так:

struct task_t
{
  struct clist_head_t list_head;                 /* cyclic list */
  u_short tid;                                   /* task id */
  struct gp_registers_t gp_registers;            /* general purpose registers */
  struct op_registers_t op_registers;            /* other purpose registers */
  struct flags_t flags;                          /* processor flags */
  u_int time;                                    /* time of task execution */
  bool reschedule;                               /* whether task need to be rescheduled */
  u_short status;                                /* task status */
  int msg_count_in;                              /* count of incomming messages */
  struct message_t msg_buff[TASK_MSG_BUFF_SIZE]; /* task message buffer */
  void *kstack;                                  /* kernel stack top */
  void *ustack;                                  /* user stack top */
} attribute(packed);

struct clist_definition_t task_list = {
  .head = null,
  .slot_size = sizeof(struct task_t)
};


Мы уже умеем выделять память и поэтому организуем задачи как кольцевой список. Так удобнее брать для выполнения следующую задачу относительно текущей с нужным статусом (мы будем использовать статус TASK_RUNNING если задача выполняется). Для начала будем предполагать что все задачи выполняются в кольце защиты ядра (так проще отлаживать планировщик) и обойдемся одним стеком. В будущем прикрутим TSS.

С задачами разобрались, теперь само планирование. Добавляем в IDT обработчик таймера и разрешаем прерывание нужной линии IRQ в PIC. По прерыванию таймера (и в конце кода инициализации ядра) передаем управление планировщику, передав из прерывания таймера адрес возврата и адрес предварительно сохраненных регистров:

/*
 * Handle IRQ0
 * void asm_ih_timer(unsigned long *addr)
 */
asm_ih_timer:
  cli
  pushal
  mov %esp,%ebp
  mov %ebp,%ebx
  pushl %ebx # &®
  add $32,%ebx
  pushl %ebx # &ret addr
  call ih_timer
  mov %ebp,%esp
  popal
  sti
  iretl


В планировщике проверяем выполнялась ли какая либо задача в этот момент. Если да увеличиваем счетчик времени ее выполнения и смотрим не превышает ли оно квоту. Если нет спокойно возвращаемся. Если превышает, нужно перепланировать задачу. Сохраняем ее состояние (пригодится адрес сохраненных регистров и адрес возврата сохраненный в стеке прерыванием таймера).
  /* save task state */
  current_task->op_registers.eip = *ret_addr;
  current_task->op_registers.cs = *(u16 *)((size_t)ret_addr + 4);
  *(u32 *)(&current_task->flags) = *(u32 *)((size_t)ret_addr + 6) | 0x200;
  current_task->op_registers.u_esp = (size_t)ret_addr + 12;
  current_task->gp_registers.esp = current_task->op_registers.u_esp;
  memcpy(&current_task->gp_registers, (void *)reg_addr, sizeof(struct gp_registers_t));


Берем адрес стека новой задачи и формируем там фрейм возврата для команды iret. После чего вызываем ассемблерную функцию переключения контекста.
  /* prepare context for the next task */
  next_task->op_registers.u_esp -= 4;
  *(u32 *)(next_task->op_registers.u_esp) = (*(u16 *)(&next_task->flags)) | 0x200;
  next_task->op_registers.u_esp -= 4;
  *(u32 *)(next_task->op_registers.u_esp) = next_task->op_registers.cs;
  next_task->op_registers.u_esp -= 4;
  *(u32 *)(next_task->op_registers.u_esp) = next_task->op_registers.eip;
  next_task->gp_registers.esp = next_task->op_registers.u_esp;
  next_task->op_registers.u_esp -= sizeof(struct gp_registers_t);
  memcpy((void *)next_task->op_registers.u_esp, (void *)&next_task->gp_registers, sizeof(struct gp_registers_t));
  
  /* update current task pointer */
  current_task = next_task;

  /* run next task */
  asm_switch_context(next_task->op_registers.u_esp);


Само переключение контекста выглядит так:
#
# Switch context
# void asm_switch_context(u32 esp)
#
asm_switch_context:
  mov 4(%esp),%ebp # ebp = esp
  mov %ebp,%esp
  popal
  sti
  iretl


Планировщик готов.

Код полностью смотри в видеоуроке, прилагаемом к статье. Там рассматривается не только планировщик но еще и процесс загрузки, компиляции, и настройки IDT для тех кто подзабыл:

Let's block ads! (Why?)

Протестующие в Гонконге обнаружили, что Telegram показывает телефонный номер независимо от настроек конфиденциальности


Схема эксплоита. Слева направо: 1) скрипт генерирует контакт-лист с 10 000 телефонных номеров по порядку, 2) добавляется в группу протестующих, 3) Telegram сообщает, какие пользователи из контакт-листа уже есть в группе, 4) скрипт генерирует новый контакт-лист и повторяет вышеописанные действия, пока не переберёт все номера

«Защищённый» мессенджер Telegram с шифрованием коммуникаций предположительно помогает сохранить анонимность пользователей — и поэтому широко используется участниками протестных акций в Гонконге. Но в последние дни среди них поднялась настоящая паника: по каналам распространяется сообщение, что у полиции есть способ определять телефонные номера пользователей Telegram.

Компания Telegram признала баг с утечкой конфиденциальных данных. Она также пояснила, что это не баг, а фича.
Схема предположительных действий полиции показана на скриншотах вверху:

  1. Полиция генерирует контакт-лист с тысячами телефонных номеров по порядку.
  2. Добавляется в группу протестующих.
  3. Telegram сообщает, какие пользователи из контакт-листа уже есть в группе.

Эту схему можно легко автоматизировать для перебора большого количества телефонных номеров.

Активисты говорят, что таким способом можно узнать номера телефонов даже в том случае, если пользователь указал в настройках Telegram никому не показывать свой телефонный номер.

Привязка к телефонному номеру — один из главных недостатков мессенджера, который пытается сохранить анонимность пользователей. У властей есть богатые инструменты для деанонимизации пользователей по их телефонным номерам, для отслеживания их перемещений, определения других телефонных номеров, которые всегда находятся рядом и так далее.

Основную информацию правоохранительные органы могут сразу запросить у телекоммуникационной компании.

После появления информации об эксплоите Teegram информацию проверили и подтвердили несколько специалистов по информационной безопасности.


«Конфиденциальность телефонных номеров [Telegram] обсуждалась с начала этого года, — говорит Чу Ка-Чонг (Chu Ka-cheong), директор Интернет-общества Гонконга и один из инженеров-программистов, которые независимо подтвердили эту ошибку. — Мы знали, что установка конфиденциальности номера в значение «Мои контакты» позволит людям из контакт-листа видеть ваш номер, поэтому активисты всегда просили людей установить настройку «Никто», ожидая, что это скроет номер телефона в публичной группе. До сегодняшнего дня мы не знали, что установка «Никто» по-прежнему позволит пользователям, которые сохранили свой номер телефона в адресной книге, сопоставить номер телефона с общедоступными членами группы. Это стало открытием для всех нас».


Установка «Никто» по-прежнему позволит пользователям, которые сохранили свой номер телефона в адресной книге, сопоставить номер телефона с общедоступными членами группы

После раскрытия этого бага пользователи начали активно покидать группы протестующих в Telegram. Активисты беспокоятся, что это затруднит координацию будущих демонстраций и действий.

Чу сказал, что на данный момент нет обходного пути, чтобы избежать этой утечки данных. Протестующие советуют перейти на одноразовые SIM-карты, зарегистрированные анонимно или на чужое имя вместо свои основных телефонных номеров. Но очевидно, что основная масса пользователей не сможет этого сделать.

К сожалению, для многих пользователей уже слишком поздно.

«Мы подозревали, что некоторые спонсируемые правительством злоумышленники использовали эту ошибку и использовали её для вычисления гонконгских протестующих, в некоторых случаях создавая непосредственную опасность для жизни протестующих», — говорит Чу о так называемых «титушках», то есть бандитах, которые приехали в Гонконг из материкового Китая и выполняет неформальные задания спецслужб, в том числе разбираясь с активистами.

Чу Ка-Чонг говорит, что Telegram сейчас является основным каналом коммуникации, и отказаться от него очень сложно: «Переход на другое приложение, такое как Signal, не является для нас жизнеспособным вариантом, — сказал он. — Потому что способ общения протестующих сильно зависит от поддержки очень больших групп […], у которых Telegram имеет действительно хорошую поддержку», — сказал Чу.

«С другой стороны, группы Signal или Wire ограничены несколькими сотнями человек, а Signal в любом случае показывает всем ваш номер телефона. Некоторые из нас уже используют Signal и Wire в небольшой закрытой группе, но общественные обсуждения и объявления будут по-прежнему сильно полагаются на Telegram».


Вчера издание ZDNet обратилось за комментариями к Telegram, и компания изучила проблему: «У нас есть защитные меры, чтобы предотвратить импорт слишком большого количества контактов — именно для предотвращения такого сценария», — сказал представитель Telegram [фактически подтверждая, что баг с утечкой конфиденциальных данных является фичей]. — Наши данные показывают, что бот на скриншотах был заблокирован для импорта контактов через две секунды — и ему удалось успешно импортировать 85 контактов (а не 10 000). После того, как вы получите запрет на импорт контактов, вы можете добавить максимум пять новых номеров в день. Остальные контакты, которые вы добавляете, будут выглядеть так, как будто они не используют Telegram, даже если используют».

Однако специалисты говорят, что это ограничение можно обойти. Злоумышленник с богатыми ресурсами, вроде правительственной спецслужбы, может легко использовать несколько ботов, а не один — и в конечном итоге импортируют всю последовательность телефонных номеров, которую хочет охватить.

Суть проблемы в том, что сами пользователи не ожидали такого подвоха от мессенджера. Они ожидали, что настройка «Никто» запретит просмотр их телефонных номеров, независимо от того, были они в списке контактов или нет.

Но Telegram сказал, что эта конкретная настройка работает не так, и она никогда так не работала: «Нет никакой ошибки: так же, как WhatsApp или Facebook Messenger, мессенджер Telegram основан на телефонных контактах. Это означает, что вы должны иметь возможность видеть свои контакты, которые также используют приложение, — говорится в сообщении компании. — Настройки номера телефона контролируют видимость номера телефона для пользователей, у которых НЕТ вашего номера (в отличие от WhatsApp, который показывает ваш номер телефона всем в любой группе)».

Таким образом, Telegram подтверждает, что как только ваш номер телефона будет добавлен в любой список контактов, этот человек (злоумышленник) сможет ассоциировать его с «анонимным» ником пользователя, независимо от настроек.

Telegram предупреждает пользователей, что настройка «Никто» на самом деле действует не так, как они думают. И это не ошибка, всё работает как положено.

Получается, что разработчики Telegram (как и большинства остальных мессенджеров) умышленно пожертвовали анонимностью пользователей ради роста социального графа. Через адресные книги пользователям легче устанавливать контакты со своими знакомыми. Это помогает «вирусному» распространению мессенджера и росту аудитории.

Минутка заботы от НЛО


Этот материал мог вызвать противоречивые чувства, поэтому перед написанием комментария освежите в памяти кое-что важное:

Let's block ads! (Why?)

Корабль «Союз МС-14» с роботом FEDOR (Skybot F-850) не смог пристыковаться к МКС

24 августа 2019 года в расчетный период времени 08:30-08:45 мск корабль «Союз МС-14» не смог осуществить процедуру стыковки с Международной космической станцией.

Сближение корабля «Союз МС-14» со станцией и попытка причаливания к исследовательскому модулю «Поиск» проводились в автоматическом режиме под контролем специалистов ЦУП и космонавтов Роскосмоса Алексея Овчинина и Александра Скворцова на борту МКС.

Повторная попытка стыковки «Союза МС-14» с МКС состоится не ранее понедельника 26 августа 2019 года.
Продолжение этой публикации: "Успешно запущен на орбиту корабль «Союз МС-14» с роботом FEDOR (Skybot F-850)".

Транспортный пилотируемый корабль (ТПК) «Союз МС-14», который должен был пристыковаться к Международной космической станции в 08:30 по Москве, не смог этого сделать в запланированное время, как сообщили из Центра управления полетами.

Стыковка должна была состояться в 08:30, однако к этому времени корабль «Союз МС-14» находился в 100 метрах от станции.

Стыковка должна была пройти в автоматическом режиме, «Союз МС-14» приблизился к МКС, совершил облет станции и встал напротив причального модуля, однако, затем на трансляции было видно, что системе автоматической стыковки «Курс» не удалось стабилизировать корабль и начать сближение со станцией. После этого корабль с дистанции 96 метров начал отходить от станции, после чего трансляция прекратилась.

По состоянию на 08:37 по Москве, корабль «Союз МС-14» отошел от станции на 150 метров.

На данный момент корабль «Союз МС-14» отведен на безопасное расстояние от МКС примерно на 300 метров.

Специалисты ЦУПа пытаются устранить проблему, чтобы начать новую попытку стыковки.

Причиной нештатной ситуации могла стать неполадка в системе автоматической стыковки корабля «Курс».

При причаливании «Союза МС-14» к станции на дальности 100 метров, возможно, произошел отказ одного полукомплекта системы сближения «Курс-НА» на корабле, он был автоматически переключен на второй, но и тут возникли проблемы, после чего ЦУП дал команду на увод корабля от станции.

Заседание государственной комиссии в зале переговоров ЦУПа началось сразу после того, как транспортный пилотируемый корабль (ТПК) «Союз МС-14» не смог состыковаться с Международной космической станцией (МКС), госкомиссия должна установить причины произошедшего и принять решение о дальнейших действиях.

Среди участников заседания — гендиректор Ракетно-космической корпорации «Энергия» Николая Севастьянов, начальник Центра подготовки космонавтов Павел Власов, директор ЦУПа Максим Матюшин, начальник головного научного института Роскосмоса ЦНИИмаш Сергей Коблов.

Специалисты в ЦУПе решили не проводить повторную попытку стыковки «Союза МС-14» в течение субботы.

По расчетам баллистиков, в течение дня корабль будет находиться на безопасном расстоянии от станции, поэтому в субботу повторная стыковка проводиться не будет.

Повторная попытка стыковки «Союза МС-14» с МКС состоится не ранее понедельника, по сообщению NASA.

Новость будет дополняться по мере поступления информации.

Программой полета была предусмотрена двухсуточная схема сближения транспортного корабля с Международной космической станцией.

Это был первый более чем за 30 лет пуск пилотируемого корабля без экипажа. Последний раз это был тестовый «Союз ТМ» в мае 1986 года.

Полет корабля «Союз МС-14» проходил в беспилотном режиме, что позволило в несколько раз увеличить полезную нагрузку за счет отсутствия части приборов и агрегатов, необходимых для обеспечения работы экипажа.

Беспилотный корабль «Союз МС» — не новая модификация пилотируемого корабля. От обычного серийного корабля этот вариант «Союза МС» отличает модернизированная система управления движением и навигации (СУДН) и соответствующая доработка отдельных бортовых систем.

На данный момент робот FEDOR (Skybot F-850) стал официально космонавтом, так как он выполнил следующие условия:

— поднялся над Землей выше 100 км;
— совершил полный виток вокруг Земли.

Кстати, в США астронавтами считают всех, кто поднялся над Землей выше 50 миль (80 км), а по нормам Международной авиационной федерации FAI граница космоса чуть выше 100 км, в России космонавтом официально признают того, кто совершил полный виток вокруг Земли.

Робот Федор или FEDOR (Final Experimental Demonstration Object Research) — антропоморфный робот, который разработан НПО «Андроидная техника» и Фондом перспективных исследований (ФПИ), и должен заменить человека в местах повышенного риска, а сейчас одна из его модификаций находится в космосе на орбите Земли на пути к МСК.

Характеристики робота FEDOR:

  • рост почти 190 см;
  • вес до 160 кг (в зависимости от используемых модулей);
  • мощность до 13,5 кВт (20 лошадиных сил), АКБ — внешняя типа ранец или питание по кабелю;
  • состоит из 15 тысяч деталей;
  • программное обеспечение: операционная система реального времени, разработанная в Санкт-Петербурге на базе Linux, управляющее ПО на C++, C# и Python;
  • работа в автономном режиме до 1 часа;
  • оснащен двумя камерами, тепловизором, микрофоном, GPS, ГЛОНАСС, 15-ю лазерами дальномерного типа и специальной системой для определения положения своего тела;
  • видеоканалы стереоскопической системы технического зрения робота могут работать как совместно, так и раздельно (совместная работа каналов позволяет определять расстояние до объектов, а раздельная — решать не менее двух функциональных задач одновременно, например, выполнять рабочие действия двумя разными инструментами одновременно);
  • режимы работы: автономный, копирующий (в точности повторяет действия оператора);
  • механика робота позволяет воспроизвести практически любые движения человеческого тела, а программное обеспечение, включая пополняемые библиотеки, позволяет расширять профессиональные навыки робота.

Космическая версия робота FEDOR в отличии от гражданской претерпела доработки:

  • уменьшен вес робота;
  • дораротан плечевой пояс (чтобы увеличить зазор по диаметру люка, который был шесть-семь сантиметров и в тестах оказалось, что неудобно его извлекать);
  • заменена штатная аккумуляторная батарея, чтобы соответствовать требованиям безопасности космических полетов;
  • установлены новые приводы;
  • конструкция робота проверена на вибростенде.

На официальном сайте государственной корпорации по космической деятельности «Роскосмос» выложили в открытый доступ 42 фотографии робота FEDOR в высоком качестве.

На этих фотографиях робот FEDOR (Skybot F-850) запечатлен на Байконуре в монтажно-испытательном корпусе «Энергия», где специалисты РКК «Энергия» проводили доработку и тестирование его систем.

Качество фотографий настолько хорошее, что можно рассмотреть надписи на шильдиках робота FEDOR и почти каждый винтик.

Видно, какие кабели и в какие разъемы подключаются для его электропитания и управления.

Правда, разъемы не дублированы, судя по фото, и если сломаются или повредятся, то может возникнуть проблема с использованием робота FEDOR далее.

Эмблема миссии робота FEDOR (Skybot F-850) на МКС создана по мотивам фрески Микеланджело «Сотворение Адама».

На эмблеме изображены кисти рук робота и человека в скафандре, тянущиеся друг к другу на фоне космоса. Рядом показаны корабль «Союз» и Международная космическая станция. Эмблема подписана сверху и снизу, соответственно, Skybot F-850 и «Союз МС-14».

Let's block ads! (Why?)

пятница, 23 августа 2019 г.

Написание змейки на ipad (pythonista)

… или как убить время имея ipad и больше ничего...

Привет!

О чем речь?


К сожалению, планшеты пока не заменяют компьютеры. Но покодить в поездке/полете это же жизненно необходимо. Поэтому я поискал какие ide есть под ipad, и собственно сегодня буду делать игрульку на Pythonista.

Что будем делать?


Простейшие программы, например кристаллики (да да, те самые, в которые вы играете в метро). Тетрис, змейка, fill — любой новичок, немного разобравшись, напишет их за 30 минут. Под катом — скриншоты, туториал, код.
Вот несколько скриншотов с того, что я наляпал:

Дисклеймер

Эта статья не только исключительно для новичков (но знающих python) и не позволит создавать world of tanks за десять минут и вообще какое-либо готовое приложение, но и автор не ручается за абсолютно красивый и правильный код с точки зрения религии программирования (хотя старается). А еще что-то стырено из примеров к pythonista и документации.


Весь код будет приведен в конце

Познакомимся с графикой в pythonista


Импорт
from scene import *
import random



Сразу создадим сцену:
class Game(Scene):
    def setup(self):
        self.background_color = "green"

run(Game(), LANDSCAPE)


Ну и сразу запустим. У вас должен был получиться зеленый экран. Давайте сделаем какую-нибудь классную штуку, добавив в класс Game метод update (который сам вызывается системой), а в него изменение цвета фона.
class Game(Scene):
    # Ранее описанные методы
    def update(self):
        self.background_color = (1.0, 1.0, (math.sin(self.t) + 1) / 2)


Теперь у нас экран плавно меняется с желтого на белый и обратно.

Теперь создадим какой-нибудь объект. Создаем его также в методе setup:


class Game(Scene):
    def setup(self):
        self.background_color = "white"
        mypath = ui.Path.rect(0, 0, 50, 50)
        self.obj = ShapeNode(mypath)
        self.obj.color = "purple" #А еще можно указывать как в html, например #FF00FF. Или в tuple, то есть (1.0, 0.0, 1.0). А еще в конец можно приписать alpha, то есть прозрачность
        self.add_child(self.obj)
    
    def update(self):
        self.obj.position = (500 + 200 * math.sin(self.t), 500 + 200 * math.cos(self.t))


Мы задали линию (mypath), создали по ней ShapeNode, указали ей цвет, а затем указали родителя (по сути одно и то же — указать родителя при создании, то есть ShapeNode(*..., parent=self) либо self.add_child(obj)).

Ну а в Game.update() мы меняем позицию объекта (tuple), причем оно в пикселях и считается от левого нижнего угла.

Заметьте, нам не нужно перерисовывать сцену (хотя можно). Объекты и ноды, родитель которого — сцена (или какой-то ее дочерний объект) перерисовываются сами
Последнее, что мы пройдем в этом разделе — touch_began (а также touch_moved и touch_ended). Несложно догадаться, метод ловит нажатия на экран. Давайте опробуем:
class Game(Scene):
    def touch_began(self, touch):
        mypath = ui.Path.oval(0, 0, 50, 50)
        obj = ShapeNode(mypath)
        obj.color = (0.5, 0.5, 1.0)
        self.add_child(obj)
        obj.position = touch.location


При каждом нажатии на экран мы создаем кружочек, место клика — touch.location.

Готовы писать змейку


Механизм игры

Змейка — массив квадратиков, когда двигается голова — второй кусочек пододвигается на место первого, третий — на место второго и т. д. Чтобы узнать пересечение головы с хвостом, мы будем сравнивать ее с каждым кусочком хвоста


Убираем весь написанный код, потому что хотим сделать более менее красиво (но, разумеется, делать будем велосипеды).

Для начала давайте создадим класс PhyObj:

class PhyObj:
    def __init__(self, path, color, parent):
        self.graph_obj = ShapeNode(path, parent=parent)
        self.parent = parent
        self.graph_obj.color = color
    
    def setpos(self, x, y):
        self.graph_obj.position = (x, y)
    
    def getpos(self):
        return self.graph_obj.position
    
    def move(self, x, y):
        self.graph_obj.position += (x, y)


Думаю, тут все тривиально. Внутри самого класса мы создали ноду, а также описали некоторые методы, которые будут делать наш код более читаемым.
Холивары

Лично я предпочитаю сначала создавать низкоуровневые классы, у которых пусть будет много дублирующихся методов и свойств, зато потом уже код в высокоуровневых становится очень красивым и читаемым.


PhyObj — это самый низкоуровневый объект в нашей игре, по сути — это абстрактный физический объект.

Описание змейки


Теперь надо бы описать змейку, а так как она состоит из кусочков, опишем сначала один:

class Tile(PhyObj):
    def __init__(self, parent, size, margin=4):
        super().__init__(ui.Path.rect(0, 0, size[0] - margin, size[1] - margin), "#66FF66", parent)
    
    def die(self):
        self.graph_obj.color = "red"



В конструкторе мы вызываем метод родителя класса и придаем себе форму и цвет. margin нужен чтобы квадратики не слипались и создавали некоторую сеточку.
class Game(Scene):
    def setup(self):
        self.tile = Tile(self, (40, 40))
        self.tile.setpos(100, 100) 


Должно было получится:

А вот к примеру зачем нужен margin:

class Game(Scene):
    def setup(self):
        tile1 = Tile(self, (40, 40))
        tile1.setpos(100, 100)
        tile2 = Tile(self, (40, 40))
        tile2.setpos(140, 100)


Отлично, теперь из этих кусочков надо склеить змею. Нам понадобится метод инициализации и метод move.

Для начала создадим инициализацию:


class Snake:
    def __init__(self, length, width, initpos, parent):
        self.width = width # Это ширина каждой клетки
        self.tiles = [Tile(parent, (width, width)) for i in range(length)] # Здесь мы создаем массив из наших клеток
        for i, tile in enumerate(self.tiles):
            tile.setpos(initpos[0] + i * self.width, initpos[1]) #Ну а здесь мы ставим позицию каждой клетке


Давайте сразу попробуем ее нарисовать:
class Game(Scene):
    def setup(self):
        self.snake = Snake(10, 40, (200, 200), self)


Ну и добавим метод move.

class Snake:
    def move(self, x, y):
        for i in range(len(self.tiles) - 1, 0, -1):
            self.tiles[i].setpos(*self.tiles[i - 1].getpos())
        self.tiles[0].move(x * self.width, y * self.width)


Сначала двигаем последнюю к предпоследней, потом предпоследнюю к предпредпоследней… потом вторую к первой. А первую на (x, y).

Собственно, змейка у нас двигается прям хорошо. Попробуем:

class Game(self):
# <...>
    def update(self):
        self.snake.move(0, 1)


Если вы успели увидеть, то уползла она как надо. Дело в том, что update вызывается очень часто (и кстати необязательно с одинаковым интервалом), поэтому нам нужно считать, сколько времени прошло с последнего вызова и ждать, пока его «накопится» достаточно.

Короче, делаем:

class Game(Scene):
    def time_reset(self):
        self.last_time = self.t
    
    def time_gone(self, t):
        if self.t - self.last_time > t:
            res = True
            self.time_reset()
        else:
            res = False
        return res

    def setup(self):
        self.snake = Snake(10, 40, (200, 200), self)
        self.time_reset() 

    def update(self):
        if self.time_gone(0.3):
            self.snake.move(0, 1)


time_gone возвращает True, если прошло больше времени, чем t. Теперь змейка будет передвигаться каждые 0.3 секунды. Получилось?

Управление


Теперь нужно сделать управление, то есть поворот во все четыре стороны:

class Game(Scene):
# <...>
    def setup(self):
        # <...>
        self.dir = (0, 1) # это направление по умолчанию

    def update(self):
        if self.time_gone(0.3):
            self.snake.move(*self.dir)


А теперь надо сделать обработку touch_began, чтобы понять в какую область ткнул юзер. На самом деле это оказалось не так интересно, как я думал, поэтому тут можно просто скопировать:
class Game(Scene):
# <...>
    def touch_began(self, touch):
        ws = touch.location[0] / self.size.w
        hs = touch.location[1] / self.size.h
        aws = 1 - ws
        if ws > hs and aws > hs:
            self.dir = (0, -1)
        elif ws > hs and aws <= hs:
            self.dir = (1, 0)
        elif ws <= hs and aws > hs:
            self.dir = (-1, 0)
        else:
            self.dir = (0, 1)


Ну вот, попробуйте теперь поворачивать

Основной механизм проработан, осталось сделать проверку на столкновения и яблочки.

Столкновение с хвостом


Начнем с проверки и добавим в змейку метод find_collisions
class Snake:
# <...>
    def find_collisions(self):
        for i in range(1, len(self.tiles)):
            if self.tiles[i].getpos() == self.tiles[0].getpos():
                return self.tiles[i], self.tiles[0]
        return False


Теперь мы можем получить пару клеток, которые пересеклись. Хотелось бы покрасить их в красный, добавим метод die в Tile:
class Tile(PhyObj):
# <...>
    def die(self):
        self.graph_obj.color = "red"


Добавим проверку в update и изменим setup:
class Game(Scene):
# <...>
    def setup(self):
        self.snake = Snake(30, 40, (200, 200), self) # Создаем змейку
        self.time_reset() 
        self.dir = (0, 1) 
        self.game_on = True #А запущена ли игра?
    
    def update(self):
        if not self.game_on: #Если не запущена, выходим
            return
        col = self.snake.find_collisions() #Есть ли коллизии?
        if col:
            for tile in col:
                tile.die() # Красим все в красный
            self.game_on = False # Останавливаем игру
        if self.time_gone(0.3):
            self.snake.move(*self.dir)


Что получилось у меня:

Осталось сделать яблочки.

Удлинение и яблочки


Сделаем удлинение змейки, причем чтобы оно выглядело красиво, новое звено будет добавлено в соответствии с последними двумя, то есть:

Добавим в змею методы:

class Snake:
# <...>
    def find_dir(self, x1, y1, x2, y2):
        if x1 == x2 and y1 > y2:
            return (0, 1)
        elif x1 == x2 and y1 < y2:
            return (0, -1)
        elif y1 == y2 and x1 > x2:
            return (1, 0)
        elif y1 == y2 and x1 < x2:
            return (-1, 0)
        else:
            assert False, "Error!"
        
    def append(self):
        if len(self.tiles) > 1:
            lastdir = self.find_dir(*self.tiles[-1].getpos(), *self.tiles[-2].getpos())
        else:
            lastdir = (-self.parent.dir[0], -self.parent.dir[1])
        self.tiles.append(Tile(self.parent, (self.width, self.width)))
        x_prev, y_prev = self.tiles[-2].getpos()
        self.tiles[-1].setpos(x_prev + lastdir[0] * self.width, y_prev + lastdir[1] * self.width)


find_dir находит направление, в которое направлен кончик хвоста нашей героини. append, несложно догадаться, добавляет ячейку. Добавим еще метод snake_lengthen в Game:

class Game(Scene):
# <...>
    def snake_lengthen(self):
        self.snake.append()
        self.time_reset()


Последняя строка нужна чтобы змейка немножко подождала, а пользователь успел увидеть, что кусочек добавился.

Чтобы узнать пересекается ли что-то со змеей, добавим в нее метод intersect

class Snake:
# <...>
    def getpos(self):
        return self.tiles[0].getpos()
    
    def intersect(self, x, y):
        return self.getpos() == (x, y)


Ура, остается только создать яблоко. Собственно, опишем яблоко за один заход:

class Apple(PhyObj):
    def __init__(self, width, size, parent):
        super().__init__(ui.Path.oval(0, 0, size[0], size[1]), "#55AAFF", parent)
        self.parent = parent
        self.width = width
        self.dislocate()
    
    def dislocate(self):
        a = random.randint(2, int(self.parent.size.w / self.width) - 2)
        b = random.randint(2, int(self.parent.size.h / self.width) - 2)
        self.setpos(a * self.width, b * self.width)


Такой странный рандом нужен чтобы уместить наше яблочко по сетке. Тогда не нужно будет искать расстояние между мордой и яблоком и сравнивать его тыры-пыры. Просто на ифах. Пойдем в update и добавим в конец этой функции очень простые строки:
class Game(Scene):
# <...>
    def setup(self):
        self.apple = Apple(40, (50, 50), self) #Число 40 - ширина сетки, должно совпадать с вторым аргументом в Snake()
        # <...>

    def update(self):
        # <...>
        if self.snake.intersect(*self.apple.getpos()):
            self.snake_lengthen()
            self.apple.dislocate()


Ну вроде все, теперь змея удлиняется если попадает в яблоко и умирает, если стукается сама о себя.

Бонус


Можно сделать звуковые эффекты:
import sound

class Game(Scene):
# <...>
    def snake_lengthen(self):
        self.snake.append()
        self.time_reset()
        sound.play_effect('arcade:Powerup_1', 0.25, 0.8)



Сделать плавное движение:
class Game(Scene):
# <...>
    def setup(self):
        self.game_on = False
        self.GLOBAL_TIMING = 0.2
        self.GLOBAL_WIDTH = 40
        self.apple = Apple(self.GLOBAL_WIDTH, (50, 50), self)
        self.snake = Snake(10, self.GLOBAL_WIDTH, (200, 200), self)
        self.time_reset()
        self.dir = (0, 1) 
        self.game_on = True

class Snake:
# <...>
    def move(self, x, y):
        for i in range(len(self.tiles) - 1, 0, -1):
            self.tiles[i].setpos(*self.tiles[i - 1].getpos(), self.parent.GLOBAL_TIMING)
        self.tiles[0].move(x * self.width, y * self.width, self.parent.GLOBAL_TIMING)

class PhyObj:
    def __init__(self, path, color, parent):
        self.graph_obj = ShapeNode(path, parent=parent)
        self.parent = parent
        self.graph_obj.color = color
        self.pos = self.graph_obj.position
    
    def setpos(self, x, y, t=0.0):
        self.pos = (x, y)
        self.graph_obj.run_action(Action.move_to(x, y, t)) # Плавное движение к x, y в течении времени t
    
    def getpos(self):
        return self.pos
    
    def move(self, x, y, t=0.0):
        self.pos = (self.pos[0] + x, self.pos[1] + y)
        self.graph_obj.run_action(Action.move_by(x, y, t)) 



Иначе говоря, мы изменили логику position PhyObj. Раньше мы ориентировались на позицию графического элемента, а теперь есть отдельное поле логической позиции (то есть той, что используется для логики игры), и позиция графического элемента теперь свободна и может быть как-то изменена по-своему. А именно, используя Action, мы оставляем ей параллельный поток, где она и двигается.

Такая плавная змея получилась:

Ну и наконец label с длиной змеи:

class Game(Scene):
# <...>
    def setup(self):
        self.game_on = False
        self.GLOBAL_TIMING = 0.2
        self.GLOBAL_WIDTH = 40
        self.apple = Apple(self.GLOBAL_WIDTH, (50, 50), self)
        self.snake = Snake(30, self.GLOBAL_WIDTH, (200, 200), self)
        self.time_reset()
        self.dir = (0, 1)
        self.label = LabelNode("", font=("Chalkduster", 20), parent=self, position=(self.size.w / 2, self.size.h - 100)) #Размещаем ее по центру
        self.update_labels()
        self.game_on = True
    
    def update_labels(self):
        self.label.text = "Length: " + str(len(self.snake.tiles))
    
    def update(self):
        if not self.game_on:
            return
        col = self.snake.find_collisions()
        if col:
            for tile in col:
                tile.die()
            self.game_on = False
        if self.time_gone(self.GLOBAL_TIMING):
            self.snake.move(*self.dir)
        if self.snake.intersect(*self.apple.getpos()):
            self.snake_lengthen()
            self.apple.dislocate()
           self.update_labels() #Обновляем тут


Друзья, спасибо за внимание! Если что-то непонятно, спрашивайте. А если будет интересно — продолжу, еще есть что рассказать (но эта статейка и так длинновата).

Весь код змейки

from scene import *
import random
import math
import sound

class PhyObj:
    def __init__(self, path, color, parent):
        self.graph_obj = ShapeNode(path, parent=parent)
        self.parent = parent
        self.graph_obj.color = color
        self.pos = self.graph_obj.position
    
    def setpos(self, x, y, t=0.0):
        self.pos = (x, y)
        self.graph_obj.run_action(Action.move_to(x, y, t))
    
    def getpos(self):
        return self.pos
    
    def move(self, x, y, t=0.0):
        self.pos = (self.pos[0] + x, self.pos[1] + y)
        self.graph_obj.run_action(Action.move_by(x, y, t))

class Tile(PhyObj):
    def __init__(self, parent, size, margin=4):
        super().__init__(ui.Path.rect(0, 0, size[0] - margin, size[1] - margin), "#66FF66", parent)
    
    def die(self):
        self.graph_obj.color = "red"

class Snake:
    def __init__(self, length, width, initpos, parent):
        self.width = width
        self.tiles = [Tile(parent, (width, width)) for i in range(length)]
        for i, tile in enumerate(self.tiles):
            tile.setpos(initpos[0] + i * self.width, initpos[1])
        self.parent = parent
    
    def move(self, x, y):
        for i in range(len(self.tiles) - 1, 0, -1):
            self.tiles[i].setpos(*self.tiles[i - 1].getpos(), self.parent.GLOBAL_TIMING)
        self.tiles[0].move(x * self.width, y * self.width, self.parent.GLOBAL_TIMING)
    
    def find_collisions(self):
        for i in range(1, len(self.tiles)):
            if self.tiles[i].getpos() == self.tiles[0].getpos():
                return self.tiles[i], self.tiles[0]
        return False
        
    def find_dir(self, x1, y1, x2, y2):
        if x1 == x2 and y1 > y2:
            return (0, 1)
        elif x1 == x2 and y1 < y2:
            return (0, -1)
        elif y1 == y2 and x1 > x2:
            return (1, 0)
        elif y1 == y2 and x1 < x2:
            return (-1, 0)
        else:
            assert False, "Error!"
        
    def append(self):
        if len(self.tiles) > 1:
            lastdir = self.find_dir(*self.tiles[-1].getpos(), *self.tiles[-2].getpos())
        else:
            lastdir = (-self.parent.dir[0], -self.parent.dir[1])
        self.tiles.append(Tile(self.parent, (self.width, self.width)))
        x_prev, y_prev = self.tiles[-2].getpos()
        self.tiles[-1].setpos(x_prev + lastdir[0] * self.width, y_prev + lastdir[1] * self.width)
    
    def getpos(self):
        return self.tiles[0].getpos()
    
    def intersect(self, x, y):
        return self.getpos() == (x, y)

class Apple(PhyObj):
    def __init__(self, width, size, parent):
        super().__init__(ui.Path.oval(0, 0, size[0], size[1]), "#55AAFF", parent)
        self.parent = parent
        self.width = width
        self.dislocate()
    
    def dislocate(self):
        a = random.randint(2, int(self.parent.size.w / self.width) - 2)
        b = random.randint(2, int(self.parent.size.h / self.width) - 2)
        self.setpos(a * self.width, b * self.width)

class Game(Scene):
    def snake_lengthen(self):
        self.snake.append()
        self.time_reset()
        sound.play_effect('arcade:Powerup_1', 0.25, 0.8)
    
    def time_reset(self):
        self.last_time = self.t
    
    def time_gone(self, t):
        if self.t - self.last_time > t:
            res = True
            self.time_reset()
        else:
            res = False
        return res
    
    def setup(self):
        self.game_on = False
        self.GLOBAL_TIMING = 0.2
        self.GLOBAL_WIDTH = 40
        self.apple = Apple(self.GLOBAL_WIDTH, (50, 50), self)
        self.snake = Snake(30, self.GLOBAL_WIDTH, (200, 200), self)
        self.time_reset()
        self.dir = (0, 1)
        self.label = LabelNode("", font=("Chalkduster", 20), parent=self, position=(self.size.w / 2, self.size.h - 100))
        self.update_labels()
        self.game_on = True
    
    def update_labels(self):
        self.label.text = "Length: " + str(len(self.snake.tiles))
    
    def update(self):
        if not self.game_on:
            return
        col = self.snake.find_collisions()
        if col:
            for tile in col:
                tile.die()
            self.game_on = False
        if self.time_gone(self.GLOBAL_TIMING):
            self.snake.move(*self.dir)
        if self.snake.intersect(*self.apple.getpos()):
            self.snake_lengthen()
            self.apple.dislocate()
            self.update_labels()
    
    def touch_began(self, touch):
        ws = touch.location[0] / self.size.w
        hs = touch.location[1] / self.size.h
        aws = 1 - ws
        if ws > hs and aws > hs:
            self.dir = (0, -1)
        elif ws > hs and aws <= hs:
            self.dir = (1, 0)
        elif ws <= hs and aws > hs:
            self.dir = (-1, 0)
        else:
            self.dir = (0, 1)
    
run(Game(), LANDSCAPE)



Код кристалликов WhiteBlackGoose edition
Забавно, что после того, как я ее сделал, я обнаружил что-то ОЧЕНЬ похожее в примерах из самой pythonista. Но у меня чуть больше фич :)

from scene import *
from math import pi
from random import uniform as rnd, choice, randint
import sys
import random
A = Action
sys.setrecursionlimit(1000000)

colors = ['pzl:Green5', "pzl:Red5", "pzl:Blue5"] + ["pzl:Purple5", "pzl:Button2"] + ["plf:Item_CoinGold"]
global inited
inited = False
class Explosion (Node):
    def __init__(self, brick, *args, **kwargs):
        Node.__init__(self, *args, **kwargs)
        self.position = brick.position
        for dx, dy in ((-1, -1), (1, -1), (-1, 1), (1, 1)):
            p = SpriteNode(brick.texture, scale=0.5, parent=self)
            p.position = brick.size.w/4 * dx, brick.size.h/4 * dy
            p.size = brick.size
            d = 0.6
            r = 30
            p.run_action(A.move_to(rnd(-r, r), rnd(-r, r), d))
            p.run_action(A.scale_to(0, d))
            p.run_action(A.rotate_to(rnd(-pi/2, pi/2), d))
        self.run_action(A.sequence(A.wait(d), A.remove()))

class Brick (SpriteNode):
    def __init__(self, brick_type, *args, **kwargs):
        img = colors[brick_type]
        SpriteNode.__init__(self, img, *args, **kwargs)
        self.brick_type = brick_type
        self.is_on = True
        self.lf = True
        self.enabled = True
    
    def destroy(self):
        self.remove_from_parent()
        self.is_on = False
    
    def mark(self):
        self.lf = False
    
    def demark(self):
        self.lf = True

class Game(Scene):
    def brickgetpos(self, i, j):
        return (self.Woff + j * self.W, self.Hoff + i * self.H)
    
    def brick(self, ty, i, j):
        b = Brick(ty, size=(self.W, self.H), position=self.brickgetpos(i, j), parent=self.game_node)
        b.rotation = random.random()
        return b

    def random_brick_type(self):
        if random.random() < 0.992:
            return random.randint(0, 3)
        else:
            if random.random() < 0.8:
                return 5
            else:
                return 4
    def setup(self):
        FONT = ('Chalkduster', 20)
        self.score_label = LabelNode('Score: 0', font=FONT, position=(self.size.w/2-100, self.size.h-40), parent=self)
        self.score = 0
        self.last_score_label = LabelNode('Delta: +0', font=FONT, position=(self.size.w/2-300, self.size.h-40), parent=self)
        self.last_score = 0
        #self.avg_label = LabelNode('Speed: +0/s', font=FONT, position=(self.size.w/2+100, self.size.h-40), parent=self)
        #self.max_label = LabelNode('Peak: +0/s', font=FONT, position=(self.size.w/2+300, self.size.h-40), parent=self)
        #self.max_speed = 0
        self.game_time = 120
        self.timel = LabelNode('Time: ' + str(self.game_time) + "s", font=FONT, position=(self.size.w/2+300, self.size.h-40), parent=self)
        self.gems = [0 for i in colors]
        self.effect_node = EffectNode(parent=self)
        self.game_node = Node(parent=self.effect_node)
        self.l = [0 for i in colors]
        self.lt = [0 for i in colors]
        for i in range(len(colors)):
            R = 50 if i == 6 else 35
            self.l[i] = Brick(i, size=(R, R), position=(40, self.size.h-100-i*40), parent=self.game_node)
            self.lt[i] = LabelNode(": 0", font=FONT, position=(self.l[i].position[0] + 40, self.l[i].position[1]), parent=self)
        self.WB = 30
        self.HB = 30
        self.W = 900 // self.WB
        self.H = 900 // self.HB
        self.colcount = 4
        self.Woff = (int(self.size.w) - self.W * self.WB + self.W) // 2
        self.Hoff = self.H + 10

        self.net = [[self.brick(self.random_brick_type(), i, j) for i in range(self.HB)] for j in range(self.WB)]
        
        #self.touch_moved = self.touch_began
        self.start_time = self.t
        self.game_on = True
        
        global inited
        inited = True
    
    def demark(self):
        for bricks in self.net:
            for brick in bricks:
                brick.demark()
    
    def howfar(self, x, y):
        alt = 0
        for i in range(y):
            if not self.net[x][i].is_on:
                alt += 1
        return alt
    
    def update(self):
        global inited
        if not inited:
            return
        self.game_on = self.t - self.start_time < self.game_time
        if self.game_on:
            self.timel.text = "Time: " + str(round(self.game_time - (self.t - self.start_time))) + "s"
        else:
            self.timel.text = "Game over"
        #if speed > self.max_speed:
        #    self.max_speed = speed
        #    self.max_label.text = "Peak: +" + str(round(self.max_speed)) + "/s"
    
    def gravity(self, x, y):
        alt = self.howfar(x, y)
        if alt == 0:
            return
        
        self.net[x][y].destroy()
        self.net[x][y - alt] = self.brick(self.net[x][y].brick_type, y, x)
        self.net[x][y - alt].position = self.net[x][y].position
        self.net[x][y - alt].rotation = self.net[x][y].rotation
        self.net[x][y - alt].enabled = False
        self.net[x][y - alt].run_action(A.sequence(A.move_to(*self.brickgetpos(y - alt, x), 0.2 * alt ** 0.5, TIMING_EASE_IN_2), A.call(lambda: self.enable_cell(x, y - alt))))
    
    def enable_cell(self, x, y):
        self.net[x][y].enabled = True
    
    def fall(self):
        for x in range(self.WB):
            for y in range(self.HB):
                if self.net[x][y].is_on:
                    self.gravity(x, y)
    
    def update_scores(self):
        self.score += self.last_score
        self.score_label.text = "Score: " + str(self.score)
        self.last_score_label.text = "Delta: +" + str(self.last_score)
        self.last_score = 0
    
    def update_cells(self):
        for i in range(self.WB):
            for j in range(self.HB):
                if not self.net[i][j].is_on:
                    self.net[i][j] = self.brick(self.random_brick_type(), j + self.HB, i)
                    self.net[i][j].enabled = True
                    self.net[i][j].run_action(A.sequence(A.move_to(*self.brickgetpos(j, i), 0.2 * self.HB ** 0.5, TIMING_EASE_IN_2), A.call(lambda: self.enable_cell(i, j))))
    
    def inbounds(self, x, y):
        return (x >= 0) and (y >= 0) and (x < self.WB) and (y < self.HB)
    
    def bomb(self, x, y, radius):
        score = 0
        bc = 0
        for i in range(round(4 * radius ** 2)):
            rad = random.random() * radius
            ang = random.random() * 2 * pi
            xp, yp = x + sin(ang) * rad, y + cos(ang) * rad
            xp, yp = int(xp), int(yp)
            if self.inbounds(xp, yp):
                score += self.explode(xp, yp)
        self.fall()
        self.give_score(round(score / 1.7), self.brickgetpos(y, x))
    
    def laser(self, x, y):
        score = 0
        coords = []
        for i in range(self.HB):
            for j in range(-1, 1 + 1, 1):
                coords.append((x + j, i))
        for i in range(self.WB):
            coords.append((i, y))
        for i in range(-self.HB, self.HB):
            coords.append((x + i, y + i))
        for i in range(-self.WB, self.WB):
            coords.append((x - i, y + i))
        bc = 0
        for x, y in coords:
            if not self.inbounds(x, y):
                continue
            score += self.explode(x, y)
        self.fall()
        self.give_score(score, self.brickgetpos(y, x))
    
    def getty(self, x, y):
        if not self.inbounds(x, y) or not self.net[x][y].is_on:
            return -1
        else:
            return self.net[x][y].brick_type
    
    def popupt(self, text, position_, font_=("Arial", 30), color_="white"):
        label = LabelNode(text, font=font_, color=color_, parent=self, position=position_)
        label.run_action(A.sequence(A.wait(1), A.call(label.remove_from_parent)))
    
    def give_score(self, count, xy):
        self.last_score = int(count ** 2.5)
        size = 10
        if self.last_score > 50000:
            size = 60
        elif self.last_score > 20000:
            size = 40
        elif self.last_score > 10000:
            size = 30
        elif self.last_score > 5000:
            size = 25
        elif self.last_score > 2000:
            size = 20
        elif self.last_score > 1000:
            size = 15
        if self.last_score > 0:
            self.popupt("+" + str(self.last_score), xy, font_=("Chalkduster", int(size * 1.5)))
        self.update_scores()
    
    def touch_began(self, touch):
        if not self.game_on:
            return
        x, y = touch.location
        x, y = x + self.W / 2, y + self.H / 2
        W, H = get_screen_size()
        x, y = x, y
        x, y = int(x), int(y)
        x, y = x - self.Woff, y - self.Hoff
        x, y = x // self.W, y // self.H
        if not self.inbounds(x, y):
            return
        
        count = self.react(self.net[x][y].brick_type, x, y, True)
        self.demark()
        if self.getty(x, y) in [0, 1, 2, 3]:
            if count >= 2:
                self.react(self.net[x][y].brick_type, x, y)
                self.fall()
                self.give_score(count, touch.location)
        elif self.getty(x, y) == 4:
            self.bomb(x, y, 5 * count)
        elif self.getty(x, y) == 5:
            self.explode(x, y)
            self.fall()
        self.update_cells()
    
    def explode(self, x, y):
        if self.net[x][y].is_on:
            self.net[x][y].destroy()
            self.gems[self.net[x][y].brick_type] += 1
            s = str(self.gems[self.net[x][y].brick_type])
            self.lt[self.net[x][y].brick_type].text = " " * len(s) +  ": " + s
            self.game_node.add_child(Explosion(self.net[x][y]))
            return True
        else:
            return False
    
    def react(self, col, x, y, ignore=False):
        if self.inbounds(x, y) and self.net[x][y].brick_type == col and self.net[x][y].is_on and self.net[x][y].lf and self.net[x][y].enabled:
            if not ignore:   
                self.explode(x, y)
            else:
                self.net[x][y].mark()
            r = 1
            r += self.react(col, x + 1, y + 0, ignore)
            r += self.react(col, x - 1, y - 0, ignore)
            r += self.react(col, x + 0, y + 1, ignore)
            r += self.react(col, x - 0, y - 1, ignore)
            return r
        else:
            return 0
    
    def destroy_brick(self, x, y):
        self.net[x][y].destroy()
        
run(Game(), LANDSCAPE, show_fps=True)



Демонстрация работы кристалликов:

Let's block ads! (Why?)