Лабораторная работа №1.

Тема работы: «Поисковые алгоритмы для решения головоломки Sokoban»

Цели работы

  1. Ознакомление с синтаксисом и основными операциями языка Python.
  2. Изучение и разработка поискового алгоритма для решения головоломки Sokoban.

Пояснения к работе

Необходимый минимум информации о языке Python

Языком дисциплины является Python – один из самых распространённых в мире языков программирования. Плюсами языка являются:

  • относительная лёгкость освоения;
  • обширная стандартная библиотека и огромная база сторонних библиотек;
  • мультипарадигменность;
  • распространённость в научном сообществе.

Для научно-исследовательских задач рекомендуется установить платформу Anaconda.

Рабочей версией Python для этой дисциплины выберем версию 3.4 и выше.

Основы синтаксиса

Python отличается от большинства языков программирования одной интересной особенностью: в нём вложенные блоки кода выделяются не ключевыми словами (begin/end в Pascal) и не специальными символами ("{" и "}" в C/C++), а равными отступами. Такой подход даёт коду программы хорошо узнаваемый аккуратный вид и добавляет лаконичности. За исключением этой детали большинство конструкций Python покажутся знакомыми любому, кто имел дело с современными высокоуровневыми языками.

Объявление переменных

Операция присваивания в Python – это обычный знак равенства:

a = 5             # целочисленная переменная
b = 3.14          # переменная с плавающей точкой
c = "String-42"   # строковая переменная

Обратите внимание на решётку – в Python так начинается комментарий.

Элементарные операции
result_1 = a + b     # результат любой операции с участием целочисленной переменной и
                     # переменной с плавающей точкой -- значение с плавающей точкой

b *= 3.14            # операция "умножить на значение и записать в ту же переменную";
                     # есть аналоги для всех арифметических операторов

c = c + "; new string"   # для строк оператор "+" склеивает строки
Вывод в консоль*
print(result_1)     # результатом выполнения этой строки будет число 8.14, выведенное на экран

Метод format замены шаблонов в строке делает вывод сложных конструкций более удобным, чем классическая конкатенация. Сравните:

degrees = 27
drinks  = ["квас", "чай"]
print("На улице {0} градусов, поэтому рекомендуем выбирать {1}.".format(degrees, drinks[0]))
print("На улице " + str(degrees) + " градусов, поэтому рекомендуем выбирать " + drinks[0] + ".")

Обе команды выведут один и тот же текст, но первая выглядит аккуратнее.

Импортирование библиотек
import math         # такой вариант импортирует все модули, содержащиеся в библиотеке
math.sin(math.pi)   # так можно использовать импортированный указанным способом модуль

Если из библиотеки требуется только одна-две функции или несколько классов, их можно импортировать с указанием их имени:

from math import sqrt

Если какой-либо модуль называется длинно и неудобно, а использовать его нужно часто, пригодится возможность создания синонимов (alias) для импортированных модулей:

import matplotlib.pyplot as plt
Условная конструкция

Полная форма условной конструкции:

if a > c:
    print(a)
elif a == c:
    print("Hi!")
else:
    print(c)

Можно опускать как elif, так и else ветки условной конструкции. Обратите внимание на сдвиги! Все вложенные блоки обязательно сдвигаются на один и тот же отступ (по стандарту форматирования PEP 8, принятому в Python-сообществе, величина сдвига составляет 4 пробела. Ваш редактор кода, скорее всего, уже настроен на использование кнопки Tab для автоматической вставки четырёх пробелов.

Кроме того, заметьте, что все строки с ключевыми словами (if, elif, else) оканчиваются двоеточием. Оно показывает, что дальше начинается вложенный блок кода.

Допустимые операторы в условии:

  • операторы сравнения: >, <, >=, <=, != ("не равно"), == ("равно");
  • логические операторы and, or и not.

Константами истинности являются True и False.

Циклы

В Python есть два основных типа циклов:

Скажем, чтобы напечатать значения в диапазоне от 0 до 15, можно написать следующее выражение:

for i in range(16):
    print(i)

Класс range генерирует целые числа от 0 до аргумента. Обратите внимание, что само значение аргумента не включается в последовательность.

Если вам требуется не только само значение элемента, но и его позиция в составном типе, пользуйтесь функцией enumerate:

for i, elem in enumerate([4, 7, 9]):
    print("Элемент {0} имеет номер позиции {1}".format(elem, i))

Пример цикла while:

k = 0
while k < 10:
    print("Это строка номер {0}".format(k))
    k += 1
Функции

Функции объявляются ключевым словом def. После него следует название функции, за которым в скобках через запятую перечислены аргументы. Тип аргументов не указывается. В конце строки ставится двоеточие.

def move(direction, state):
    # некоторая обработка аргументов
    return new_state

После ключевого слова return прописывается возвращаемая переменная. Строки, находящиеся на том же уровне вложенности, что и return, и расположенные ниже неё не выполняются.

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

def say_hi():
    print("Hi!")

В Python есть слегка урезанная форма лямбда функций, которые, однако, могут пригодиться:

f = lambda x: x * 25
print("Результат выполнения лямбда-функции с аргументом, равным 10: {}".format(f(10)))
Классы
Структуры данных
Список (list)

Список – самый распространенный тип данных в Python. В чём-то он напоминает динамические массивы, однако его "изнанка" совсем другая, а способов использования гораздо больше. Список задаётся перечнем элементов в квадратных скобках. Типы элементов могут быть какими угодно, даже другими списками:

list_example = ["I'm the first element (index 0)", 10, 3.1415926, -1, 'k', [1, 2, 3]]

Индексация начинается с 0. Вы можете обратиться к элементу списка, используя квадратные скобки:

print(list_example[2])     # Выведет 3.1415926

В Python работают срезы, возвращающие списки элементов:

print(list_example[1:3])    # Выведет [10, 3.1415926]. Заметьте, последний индекс не включается!
print(list_example[:3])     # Выведет ["I'm the first element (index 0)", 10, 3.1415926]
print(list_example[1:])     # Выведет [10, 3.1415926, -1, 'k', [1, 2, 3]]

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

print(list_example[5][1])     # Выведет 2

Длину списка (и любого другого составного типа данных) можно проверить функцией len:

print(len(list_example))     # Выведет 6

Список может менять размеры в процессе работы.

Основные методы работы со списками описаны в документации в п. 4.6.1 и п. 4.6.3.

Кортеж (tuple)

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

tuple_example = ("Tuple", 10, "can", -1, "not", 24.1, "be", 123, "changed.")
Строка (str)

Со строками вы уже познакомились чуть выше. Строка во многом ведёт себя как кортеж, то есть является неизменяемой, и при этом к её элементам можно обращаться в квадратных скобках. В строке могут содержаться специальные управляющие символы, такие как '\n' (переход на новую строку) или '\t' (символ табуляции).

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

s = "Важные исследования задерживаются из-за того, что в одной области неизвестны результаты, уже давно ставшие классическими в смежной области"

Метод, разбивающий строку по указанному разделителю на подстроки:

L = s.split(',')

Результатом будет список вида

["Важные исследования задерживаются из-за того", " что в одной области неизвестны результаты", " уже давно ставшие классическими в смежной области"]

Метод, возращающий True, если вся строка содержит только числовые символы:

print(s.isdigit())    # Результатом будет False

Метод, возвращающий позицию подстроки в строке:

print(s.find('ы'))    # Результатом будет 4, т.к. подстрока 'ы' встречается в строке на 4-й позиции

Если подстрока не найдена, метод find вернёт -1.

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

Множество (set)
Словарь (dict)

Словарь — одна из самых интересных и удобных структур данных, которые есть в Python. Если вам знаком формат JSON, структура и синтаксис словаря практически ему идентичны. Словарь содержит пары «ключ-значение». По ключу можно обратиться к значению. Ключом может быть любой неизменяемый тип данных.

Объявить пустой словарь можно так:

my_dict = dict()

или так

my_dict = {}

Непустой словарь выглядит, к примеру, так:

my_dict = {"First": 1, "Second": [1, 2, 3], "Fox": "rabbit", 5: '#'}

Так можно вывести значение, соответствующее ключу:

print(my_dict["Fox"])

Генераторы

Работа с файлами

Головоломка Sokoban

Sokoban (Soko-Ban, яп. 倉庫番 — «кладовщик») была создана в 1981 году Хироюки Имабаяси, и издана в 1982 году для компьютеров серии NEC PC-8801. Головоломка часто попадала в фокус внимания исследователей. Например, было доказано, что в двумерном варианте игра является NP-трудной. Термин "NP-трудность" означает, что любая задача из класса NP за полиномиальное время может быть сведена к этой задаче.

Рис. 1. Процесс решения уровня головоломки *Sokoban*

Основная задача головоломки – размещение ящиков по указанным местам на уровне. Агент (игрок; мы будем часто называть игроков "агентами") может перемещаться в четырёх направлениях: вверх, вниз, влево, вправо. При перемещении агент может двигать ящик, если переместится в его направлении, находясь к нему вплотную (см. рисунок 1).

Для головоломки создано множество разнообразных уровней.

В основном, уровни хранятся в текстовом формате со стандартизированными обозначениями основных элементов:

Символ Обозначение
# Стена
. Пустая цель
@ Игрок на полу
+ Игрок на цели
$ Ящик на полу
* Ящик на цели
- Пол

Таким образом, уровень является решённым, если на нём не осталось ни одного символа "$".

Для хранения уровней часто используют так называемый Run-Length Encoding (RLE, последовательное кодирование), при котором уровень записывается в строку. Например, уровень

###
#.###
#*$ #
# @ #
#####

запишется как

3#|#.3#|#*$-#|#--@#|5#

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

Решение уровня можно представить в виде последовательности перемещений агента. Традиционно используют первые буквы английских слов, обозначающих направления: u (up, вверх), d (down, вниз), l (left, влево), r (right, вправо).

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

ludrrul

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

Поисковые алгоритмы

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

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

Рассматриваемые алгоритмы поиска:

  1. Поиск в ширину (Breadth-first search)
  2. Поиск в глубину (Depth-first search)
  3. Жадный поиск по первому наилучшему совпадению (Greedy best-first search)
  4. Алгоритм поиска A*

Ход работы

  1. Разработать функцию для считывания из файла уровня Sokoban, заданного в формате RLE. Функция должна возвращать список строк, соответствющих строкам декодированного уровня.
  2. Разработать функцию для изменения состояния уровня в зависимости от отправленной команды (u, d, l, r).
  3. Реализовать функцию случайного поиска решения. Фукнция должна возвращать последовательность команд из п. 2.
  4. Реализовать алгоритм, соответствующий варианту (см. варианты). Фукнция должна возвращать последовательность команд из п. 2. Функция должна выводить следующую статистику:
    • количество посещённых узлов
    • время выполнения алгоритма в секундах
  5. Проверить работу алгоритма на пяти различных уровнях, хотя бы два из которых содержат более 3 ящиков .

Варианты

  1. Поиск в ширину (Breadth-first search) [Учебник за 11 класс ->]
  2. Поиск в глубину (Depth-first search) [Учебник за 11 класс ->]
  3. Жадный поиск по первому наилучшему совпадению (Greedy best-first search) [И.А. Бессмертный. Искусственный интеллект, c.31-35 ->]
  4. Алгоритм поиска A* [И.А. Бессмертный. Искусственный интеллект, c.31-35 ->]

Варианты 1-2 дадут максимум 8 баллов, варианты 3-4 – 10 баллов. Выбор варианта пройдёт на основе результатов опроса на занятии.

Содержание отчета

Контрольные вопросы

Литература