Слёрм Universe - Python на примере Minecraft
Словари
Практика 7
Наденьте солнцезащитные очки, подойдите к зеркалу и посмотрите, как вы круто выглядите. А всё потому, что в вашей голове теперь уместился ещё один навык - работа со списками. Ну и очки добавляют крутости, разумеется. Пазл из типов данных, которые мы должны были выучить в рамках этого курса сложился полностью:
  • Числа;
  • Вещественные числа;
  • Строки;
  • Логические значения (True или False);
  • Списки;
  • Кортежи;
  • None;
  • и... (барабанная дробь) словари!

И для обязательно закрепления наших знаний мы должны испытать работу словарей на практике. Ну что, поехали?!
Давайте решим следующую задачу:
Необходимо написать код, который будет удалять блоки на заданном участке в том случае, если игрок встанет на блок золота. Если после удаления блоков игрок окажется на блоке алмаза, то пусть все "исчезнувшие" блоки чудесным образом восстановятся. Итак, давайте используем полученные знания по теме Функции и напишем функцию, которая будет удалять блоки на заданных координатах и в заданных размерах по длине, высоте и ширине:
from mcpi.minecraft import Minecraft
mc = Minecraft.create()


def clear_blocks(coords, parameters):
    clear_blocks_list = []
    x, y, z = coords
    length_size, height_size, width_size = parameters
    for length_bias in range(length_size):
        for height_bias in range(height_size):
            for width_bias in range(width_size):
                clear_blocks_list.append({"coords": (x + length_bias, y + height_bias, z + width_bias),
                                          "block_id": mc.getBlock(x + length_bias, y + height_bias, z + width_bias)})
                mc.setBlock(x + length_bias, y + height_bias, z + width_bias, 0)
    return clear_blocks_list
Разберём код:
  • Мы назвали функцию clear_blocks;
  • В качестве аргументов наша функция clear_blocks() принимает следующие значения: кортеж с координатами, где мы хотим начать удаление блоков, (будет скопирован в переменную coords); кортеж с тремя габаритными размерами той площади, на которой мы хотим удалить блоки (будет скопирован в переменную parameters);
  • Внутри нашей функции мы предварительно создаём пустой список (да-да, именно список) clear_block_list, куда мы будем записывать для каждого блока отдельный словарь с его координатами и ID-номер этого блока;
  • Далее мы распаковываем переданный в нашу функцию кортеж coords с координатами начала удаления блоков в три переменные x, y, z;
  • После мы распаковываем переданный в нашу функцию кортеж parameters с габаритными размерами площади, из которой будут удаляться блоки в три переменные length_size, height_size, width_size;
  • Затем при помощи трёх циклов FOR мы ПЕРЕБИРАЕМ каждый отдельный блок на заданной площади;
  • Ну и наконец для каждого отдельного блока заданной площади мы записываем в словарь: В значение ключа "coords" кортеж с координатами блока и в значение ключа "block_id" его ID, полученное при помощи функции getBlock(). Далее этот словарь отправляем в один общий список clear_block_list при помощи метода списков append;
  • И в конце, после того, как функция удалит все блоки на заданной площади и запишет их координаты с ID во множество отдельных словарей в рамках одного списка clear_blocks_list, мы возвращаем значение этого списка при помощи return.

Итак, теперь наша функция не только удаляет блоки на заданной площади и на необходимых координатах, но и ещё возвращает итоговый список, в котором содержатся словари с их координатами и ID. К примеру, если мы добавим эту функцию в переменную, то она сначала просто выполнится и удалит необходимые блоки, а после запишет в эту переменную список со словарями, где записаны данные этих блоков. Как видите, наша функция умеет делать сразу две вещи. Очень важно, чтобы эти вещи были связаны между собой, тогда функцию можно назвать хорошей. К примеру, если функция удаляет блоки и шлёт сообщение в чат "Всем привет!", то это не лучший вариант. А вот если она, как в нашем случае, удаляет блоки и возвращает их координаты и ID, то такую функцию можно назвать действительно хорошей.
Давайте для теста работы функции попробуем записать её в какую-нибудь переменную и вывести значение этой переменной на экран:
all_clear_blocks = clear_blocks((-300, 75, 1000), (2, 2, 2))
print(all_clear_blocks)
Мы указали случайные координаты, взятые наугад, и небольшие габариты, чтобы не получить большой список данными удалённых блоков:
[{'coords': (-300, 75, 1000), 'block_id': 0}, {'coords': (-300, 75, 1001), 'block_id': 0}, {'coords': (-300, 76, 1000), 'block_id': 3}, {'coords': (-300, 76, 1001), 'block_id': 3}, {'coords': (-299, 75, 1000), 'block_id': 0}, {'coords': (-299, 75, 1001), 'block_id': 0}, {'coords': (-299, 76, 1000), 'block_id': 3}, {'coords': (-299, 76, 1001), 'block_id': 3}]
Теперь наша функция удалила блоки по заданным координатам и параметрам, а после вернула список со словарями в качестве элементов для каждого удалённого блока, где содержатся их координаты, записанные в кортеж (ключ "coords") и ID записанные в формате чисел (ключ "block_id"). Удалим этот "тестовый" код и поехали дальше!

Отлично, функцию мы написали. Теперь давайте сделаем следующее - ЕСЛИ (if) под ногами нашего игрока золото, то мы запустим нашу функцию clear_blocks() и запишем список, который она возвращает в отдельную переменную:
while True:
    player_x, player_y, player_z = mc.player.getPos()
    gold_struct = mc.getBlock(player_x, player_y - 1, player_z) == 41
    if gold_struct:
        mc.setBlock(player_x, player_y - 1, player_z, 51)
        all_clear_blocks = clear_blocks((player_x + 10, player_y, player_z), (10, 10, 10))
Итак, после написанной нами функции мы создали бесконечный цикл WHILE, внутри которого:
  • Создали переменные player_x, player_y, player_z с координатами нашего игрока;
  • Создали переменную gold_struct, которой присваивается значение True, если под ногами нашего игрока находится золотой блок;
  • И далее добавили условие: ЕСЛИ (if) значение переменной gold_struct является истиной (True), ТО мы сжигаем блок под ногами нашего игрока (меняем блок золота на блок огня), а после записываем результат работы созданной нами функции clear_blocks() в переменную all_clear_blocks. В качестве аргументов функции all_clear_blocks мы указали кортеж с координатами, где мы собираемся начать удаление блоков и кортеж с габаритами расчищаемой площади (10 блоков в длину, 10 - в высоту и 10 - в ширину).
Теперь мы сделали так, чтобы когда наш игрок окажется на золотом блоке, то произойдёт удаление блоков на заданной площади, а после вернётся список с их данными в переменную all_clear_blocks.

И, наконец-то, закончим наш код добавлением последней операции в рамках нашего задания. Если под ногами нашего игрока окажется алмазный блок, то пусть удалённые ранее блоки на заданной площади вновь вернутся обратно чудесной магией Python! Для этого немного расширим наш код внутри цикла WHILE:
while True:
    player_x, player_y, player_z = mc.player.getPos()
    gold_struct = mc.getBlock(player_x, player_y - 1, player_z) == 41
    diamond_struct = mc.getBlock(player_x, player_y - 1, player_z) == 57
    if gold_struct:
        mc.setBlock(player_x, player_y - 1, player_z, 51)
        all_clear_blocks = clear_blocks((player_x + 10, player_y, player_z), (10, 10, 10))

    if diamond_struct:
        mc.setBlock(player_x, player_y - 1, player_z, 51)
        for restore_block in all_clear_blocks:
            restore_block_x, restore_block_y, restore_block_z = restore_block["coords"]
            restore_block_id = restore_block["block_id"]
            mc.setBlock(restore_block_x, restore_block_y, restore_block_z, restore_block_id)
        all_clear_blocks.clear()
  • Мы добавили ещё одну переменную diamond_struct, которой присваивается значение True, если под ногами нашего игрока находится алмазный блок;
  • И добавили ещё одно условие: ЕСЛИ (if) значение переменной diamond_struct является истиной (True), ТО мы сжигаем блок под ногами нашего игрока (меняем блок алмаза на блок огня), а после при помощи цикла FOR записываем в переменную restore_block для каждой отдельной итерации по элементу списка all_clear_blocks, в котором содержатся словари с данными удалённых блоков;
  • Теперь для каждой новой итерации цикла FOR переменная restore_block будет содержать словарь каждого отдельного блока, который нам необходимо восстановить. Сначала в теле FOR мы распаковываем из этого словаря кортеж с координатами восстанавливаемого блока в переменные restore_block_x, restore_block_y, restore_block_z по ключу "coords", а после его ID в переменную restore_block_id по ключу "block_id";
  • Ну и в конце каждой итерации цикла FOR мы восстанавливаем каждый отдельный блок при помощи функции setBlock(), в аргументы которой мы записываем извлечённые нами из словарей координаты и ID каждого удалённого блока;
  • После того, как цикл FOR восстановит все удалённые блоки, мы очищаем список с их данными при помощи метода clear.

Однако, в коде есть грубая ошибка! Попытайтесь её найти! Мы уже сталкивались с похожей ситуацией в практике по СПИСКАМ. Дело в том, что если после запуска кода мы СНАЧАЛА наступим на золотой блок, чтобы произошло удаление блоков на заданной нами площади, а ПОСЛЕ встанем на алмазный блок, чтобы удалённые блоки обратно восстановились, то код выполнится без ошибок. Однако, если мы поступим наоборот и сначала встанем на алмазный блок, то код попытается восстановить из списка all_clear_blocks блоки, которые ранее НЕ УДАЛЯЛИСЬ. Тогда и списка all_clear_blocks,, соответственно, НЕ СУЩЕСТВУЕТ. На практике по спискам мы нашли изящный способ спастись от такой проблемы введением дополнительный переменной, в которой будет отражаться событие, если наш игрок побывал на блоке с золотом:
was_on_gold_struct = False
while True:
    player_x, player_y, player_z = mc.player.getPos()
    gold_struct = mc.getBlock(player_x, player_y - 1, player_z) == 41
    diamond_struct = mc.getBlock(player_x, player_y - 1, player_z) == 57
    if gold_struct:
        mc.setBlock(player_x, player_y - 1, player_z, 51)
        all_clear_blocks = clear_blocks((player_x + 10, player_y, player_z), (10, 10, 10))
        was_on_gold_struct = True

    if diamond_struct and was_on_gold_struct:
        mc.setBlock(player_x, player_y - 1, player_z, 51)
        for restore_block in all_clear_blocks:
            restore_block_x, restore_block_y, restore_block_z = restore_block["coords"]
            restore_block_id = restore_block["block_id"]
            mc.setBlock(restore_block_x, restore_block_y, restore_block_z, restore_block_id)
        all_clear_blocks.clear()
        was_on_gold_struct = False
  • Перед бесконечным циклом WHILE мы создали переменную was_on_gold_struct, в которой мы будем фиксировать нахождение нашего игрока на блоке с золотом. Изначальное её значение False;
  • Далее в теле условия if golds_struct: (ЕСЛИ игрок был на золотом блоке) мы меняем значение переменной was_on_gold_struct на True, указывая на то, что игрок действительно был на золотом блоке и, соответственно, после этого произошло удаление блоков на заданной площади и создание списка all_clear_blocks со словарями, в которых содержатся их данные с ID и координатами;
  • А после меняем условие нахождения нашего игрока на алмазном блоке. Теперь оно звучит так: ЕСЛИ (if) под ногами нашего игрока алмазный блок (diamond_struct) И (and) наш игрок ранее стоял на блоке с золотом (was_on_gold_struct), ТО ТОЛЬКО ТОГДА мы восстанавливаем блоки, которые ДЕЙСТВИТЕЛЬНО были удалены;
  • После того, когда наконец все блоки будут восстановлены, в конце условия мы меняем значение переменной was_on_gold_struct на значение False. То есть теперь мы вновь будем восстанавливать блоки лишь тогда, когда игрок снова окажется на золотом блоке и снова произойдёт удаление блоков на заданной площади.

Отлично! Мы полностью выполнили задание и теперь можем проверять работу кода. Давайте выполним его. Но перед этим сразу предупрежу вас о том, что функция getBlock(), при помощи которой мы записываем ID каждого удаляемого блока в их словарь работает ОЧЕНЬ медленно. Поэтому не рекомендую тестировать код c большими габаритными значениями "расчищаемой" площади. В нашем примере длина, высота и ширина такой площади составляет по 10 блоков. Даже такая площадь будет "расчищаться" не быстро. Итак, поехали:
Что произошло после того, как мы оказались на блоке с золотом:
И после чего мы встали уже на алмазный блок:
Отлично, мы смогли восстановить все удалённые блоки! Наш код отлично сработал, а мы повторили работу с функциями и попрактиковались со списками. Переходите к самостоятельному заданию, скоро увидимся!

Итоговый код, полученный в рамках данной практики:
from mcpi.minecraft import Minecraft
mc = Minecraft.create()


def clear_blocks(coords, parameters):
    clear_blocks_list = []
    x, y, z = coords
    length_size, height_size, width_size = parameters
    for length_bias in range(length_size):
        for height_bias in range(height_size):
            for width_bias in range(width_size):
                clear_blocks_list.append({"coords": (x + length_bias, y + height_bias, z + width_bias),
                                          "block_id": mc.getBlock(x + length_bias, y + height_bias, z + width_bias)})
                mc.setBlock(x + length_bias, y + height_bias, z + width_bias, 0)
    return clear_blocks_list

was_on_gold_struct = False
while True:
    player_x, player_y, player_z = mc.player.getPos()
    gold_struct = mc.getBlock(player_x, player_y - 1, player_z) == 41
    diamond_struct = mc.getBlock(player_x, player_y - 1, player_z) == 57
    if gold_struct:
        mc.setBlock(player_x, player_y - 1, player_z, 51)
        all_clear_blocks = clear_blocks((player_x + 10, player_y, player_z), (10, 10, 10))
        was_on_gold_struct = True

    if diamond_struct and was_on_gold_struct:
        mc.setBlock(player_x, player_y - 1, player_z, 51)
        for restore_block in all_clear_blocks:
            restore_block_x, restore_block_y, restore_block_z = restore_block["coords"]
            restore_block_id = restore_block["block_id"]
            mc.setBlock(restore_block_x, restore_block_y, restore_block_z, restore_block_id)
        all_clear_blocks.clear()
        was_on_gold_struct = False