Я собираю микромышь — маленького автономного робота, который сам находит центр лабиринта 16×16. Детали заказаны и едут недели три, а руки чешутся уже сейчас. К счастью, самая интересная часть проекта — алгоритм поиска — вообще не требует железа. Нужен симулятор.

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

Что такое mms#

mms — симулятор соревнований Micromouse. Он рисует лабиринт, катает по нему мышь и разговаривает с вашим алгоритмом через обычные stdin и stdout. Из этого следует приятное: алгоритм можно писать на чём угодно — на Python, C++, Rust, хоть на shell-скрипте. Симулятору всё равно, он просто запускает вашу программу как подпроцесс и обменивается с ней строками.

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

Установка#

Никакой. Под Linux в релизах лежит готовый AppImage — самодостаточный образ, внутри которого уже упакованы Qt, плагины и сам движок:

$ ls -la ~/Downloads/mms-x86_64.AppImage
-rwxr-xr-x 1 user user 54793408 mms-x86_64.AppImage

Права на исполнение обычно уже стоят, но если нет:

chmod +x mms-x86_64.AppImage
./mms-x86_64.AppImage

Единственное требование — в системе должен быть FUSE 2, иначе AppImage не сможет себя смонтировать. Проверить:

$ ldconfig -p | grep libfuse.so.2
libfuse.so.2 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libfuse.so.2

Если пусто — sudo apt install libfuse2 на Debian и производных.

При запуске в консоль сыпется пара строк вида libpng warning: iCCP: known incorrect sRGB profile — это про цветовой профиль в иконках, на работу не влияет, игнорируйте.

Файл стоит переложить из «Загрузок» куда-нибудь в постоянное место — скажем, в ~/bin, — чтобы не снести случайно при уборке.

Подключаем свой алгоритм#

В правом верхнем углу есть панель Config, а в ней строка Mouse. Жмём в ней кнопку + и заполняем поля:

ПолеЗначение
Namemicromouse
Directoryпуть к каталогу с вашим кодом
Build commandоставляем пустым
Run commandpython3 -u main.py

Обратите внимание на флаг -u. Он обязателен, и это первая ловушка, на которой легко потерять вечер. Без него Python буферизует stdout: ваша программа честно отправляет команду, но та оседает в буфере и до симулятора не доходит. mms послушно ждёт ответа, программа ждёт ответа от mms — и всё замирает намертво, без единого сообщения об ошибке. Выглядит так, будто симулятор сломан.

Альтернатива, если не хочется зависеть от настроек запуска, — выставить PYTHONUNBUFFERED=1 или вызывать print(..., flush=True). Я делаю и то и другое: флаг в команде запуска и flush=True в самой функции отправки.

Окно mms с загруженным лабиринтом

Лабиринт загружен, но алгоритм ещё не запускали. Числа в клетках рисует сам симулятор — это истинное расстояние до центра, и в четырёх центральных клетках стоят нули. Удобная шпаргалка: видно, что ваш алгоритм должен насчитать в идеале. Справа вверху — выбор лабиринта и алгоритма.

Кнопка Build нужна только компилируемым языкам — для Python жмём сразу Run.

Настройки лежат в ~/.config/mackorone/mms.conf — обычный ini. Если хочется завести алгоритм не мышкой, а из скрипта, секция выглядит так:

[mouseAlgos]
1\name=micromouse
1\directory=/path/to/your/sim
1\buildCommand=
1\runCommand=python3 -u main.py
size=1

Только правьте файл при закрытом симуляторе: Qt перезаписывает настройки при выходе и затрёт ваши изменения.

Как устроен протокол#

Протокол текстовый и предельно простой: пишете строку в stdout — получаете строку в stdin. Обёртка на Python умещается в несколько функций:

import sys

def _send(command: str) -> None:
    print(command, flush=True)

def _ask(command: str) -> str:
    print(command, flush=True)
    return sys.stdin.readline().strip()

def wall_front() -> bool:
    return _ask("wallFront") == "true"

def move_forward() -> None:
    if _ask("moveForward") == "crash":
        raise RuntimeError("crash")

Команды делятся на три группы:

  • запросы о лабиринтеmazeWidth, mazeHeight;
  • датчикиwallFront, wallLeft, wallRight, отвечают true или false;
  • движениеmoveForward, turnLeft, turnRight, отвечают ack или crash;
  • отрисовкаsetColor, setText, clearAllColor, clearAllText.

Две ловушки, которые стоит знать заранее#

Первая: stdout занят протоколом. Обычный print() для отладки немедленно ломает связь с симулятором — ваша отладочная строка приходит туда, где mms ждёт команду. Отладочный вывод надо писать в stderr:

def log(*parts: object) -> None:
    print(*parts, file=sys.stderr, flush=True)

Всё, что программа пишет в stderr, mms показывает на вкладке Run Output — это видно на картинке в начале поста, там строка про размер лабиринта и целевые клетки. Рядом есть Build Output для вывода сборки и Simulator Logs, куда симулятор пишет уже своё.

Вторая, куда неприятнее: ответы обязательно надо вычитывать. Команды движения отвечают ack, и если этот ответ не прочитать, он останется в буфере. Следующий запрос датчика получит чужой ответ — протокол разъедется на одну строку и дальше уже не сойдётся. Команды отрисовки, наоборот, не отвечают ничего, и читать ответ после них нельзя — программа повиснет.

Коварство в том, что проявляется рассинхрон далеко от места, где возник, и выглядит как «мышь внезапно сошла с ума»: едет в стену, поворачивает не туда. Искать причину в алгоритме можно долго, потому что алгоритм-то исправен.

Лечится это тестом, который запускает ваш main.py подпроцессом и сам играет роль симулятора, отвечая на команды и проверяя, что программа спрашивает ровно то, что должна. У меня это единственный тест, который вообще ловит такой класс ошибок, — обычные юнит-тесты на алгоритм проходят при полностью разъехавшемся протоколе.

А лабиринтов-то нет#

Тут я и наткнулся на главный сюрприз. Скачиваете mms, запускаете, жмёте выбор лабиринта — и выбирать нечего. В релизной сборке лабиринтов нет вообще.

Я распаковал AppImage целиком, чтобы убедиться:

$ ./mms-x86_64.AppImage --appimage-extract
$ ls squashfs-root
32x32.png  AppRun  doc  lib  mms  mms.desktop  plugins  qt.conf  translations

Ни каталога с лабиринтами, ни единого файла .num или .maz. Только движок. В репозитории проекта лежат шесть примеров (src/resources/mazes/), но в сборку они не попадают, да и это именно примеры — example1example5 и пустой blank, а не соревновательные трассы.

Где брать настоящие#

README самого mms отсылает к коллекции micromouseonline/mazefiles, и это правильный адрес. Там больше пятисот лабиринтов с реальных соревнований, собранных за много лет:

mazefiles/
  classic/    522  соревновательные 16×16
  halfsize/    42  half-size 32×32
  training/    16  маленькие 8×8 и 10×5

Вся коллекция весит около полутора мегабайт — это простые текстовые файлы.

Начинать удобнее не с classic/, а с training/. Лабиринт 8×8 мышь проходит за секунды, ошибки видно сразу, и цикл «поправил — посмотрел» получается коротким. На полноразмерных 16×16 один прогон уже заметно дольше, а разглядеть в нём момент, где всё пошло не так, труднее.

Форматы#

mms понимает два формата, и оба — текстовые.

Map — картинка лабиринта символами. Столбы, горизонтальные стены ---, вертикальные |:

o---o---o---o
|           |
o   o---o   o
|   |   |   |
o---o---o---o

Клетка занимает 4 символа по горизонтали и 2 по вертикали, поэтому классический 16×16 — это ровно 65 символов в ширину и 33 строки. Полезная проверка: если файл не такого размера, он либо не 16×16, либо битый.

Важная деталь: для mms стеной считается любой символ кроме пробела, и проверяются только позиции стен, а не центры клеток. Благодаря этому файлы из mazefiles читаются напрямую, хотя используют o вместо + для столбов. А ещё в них в центрах клеток стоят метки S (старт) и G (цель) — симулятор их просто не видит, они нужны человеку и сторонним инструментам.

Num — по строке на клетку, шесть чисел:

X Y N E S W

Координаты клетки и по единице на каждую сторону: 1 если стена есть, 0 если нет. Формат менее наглядный, зато его тривиально генерировать из кода.

Оговорка, которую стоит прочитать#

Автор коллекции честно предупреждает в README: среди файлов есть ошибки и дубликаты, это не эталонный список. Часть лабиринтов переиспользовалась разными соревнованиями, поэтому одна и та же планировка встречается под разными именами.

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

Что в итоге#

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

И это, по-моему, главная ценность симулятора. Когда железо наконец приедет, отлаживать придётся моторы, энкодеры и датчики — то есть вещи, которые ломаются физически и чинятся паяльником. Тащить туда ещё и непроверенный алгоритм — верный способ не разобраться ни в чём.

Ссылки#

  • mackorone/mms — сам симулятор, там же описание протокола и форматов
  • micromouseonline/mazefiles — коллекция лабиринтов
  • Micromouse Book Питера Харрисона — по сути учебник по теме; раздел про решение лабиринта стоит прочитать до того, как писать свой алгоритм