Классический сапёр на html5 и LibCanvas
В этой статье я пошагово расскажу, как писать самый обычный, классический сапёр при помощи Html5 Canvas, AtomJS, и тайлового движка LibCanvas.
Воспользуемся стандартным шаблоном для «старта» нашего приложения. Важно не забывать подключать js-файлы после создания соответствующих классов.
Я нарисовал две картинки — мины и флага. Всё остальное мы будем делать «вручную» прям в приложении. Объединил их в один спрайт для уменьшения количества запросов и предзагружу перед тем, как стартовать приложение. В коде так же можно увидеть нарезку при помощи atom.ImagePreloader:
Отрисовка
Во время игры мы можем видеть такое:
1. Числа от 1 до 8.
2. Закрытая ячейка
3. Открытая, но пустая ячейка
4. Флажок
После её окончания — следующее:
1. Все мины
2. Если подорвались на одной из них — она выделена
3. Если где-то неверно поставили флаг
Итого, 8 + 3 + 3 = 14 разных состояний. Опишем их все:
Теперь по очереди добавляем методы отрисовки. Пустая клетка — просто красим:
Мина и флаг — это просто картинки на пустой клетке:
Мина, на которой мы подорвались отрисовывается с красным фоном:
Закрытая ячейка отрисовывается тоже достаточно просто — градиент от тёмного к светлому, с верхнего-левого угла в нижний-правый.
Наша реализация позволяет нам менять размер ячеек и они будут в любом случае отлично выглядеть:
Генератор мин
Как видим, отрисовка полностью готова. Теперь нам достаточно сделать простое действие и клетка поменяет свой внешний вид.
Удалим наш дебаг-код и создадим инстанс генератора:
Для начала научимся разбрасывать по полю мины. Конечно, было бы неплохо учитывать всякие сомнительные ситуации, но пока у нас для него одно требование — сгенерировать поле после первого клика пользователя, так, чтобы тот не попадался сразу же на мину.
Следующий шаг — это добавить api-метод, который будет вызываться для генерации этих мин и заносить их в индекс для быстрого доступа. Создадим двумерный хеш со значениями 1, где мина есть и 0, где мины нету. Нам важно использовать именно Integer, причину мы увидим ниже. Теперь у нас есть быстрый метод isMine для определения, есть ли мина по координате. Метод isReady будет использоваться, чтобы узнать внешним классам, сгенерировано ли уже минное поле.
Следующий шаг — сделать получение значения клетки, если там мины нет. Алгоритм очень прост — берём всех соседей, которые не выходят за рамки поля, считаем суму их значений. Именно в этом месте то, что мина есть Integer нам и пригодилось.
Взаимодействие с пользователем
Теперь опишем открытие клетки. Для начала, открываем только те клетки, которые закрыты. Нечего взаимодействовать с всякими статичными цифрами и флагами. Во-вторых, проверяем, готовы ли наши мины и, если нет — запускаем генератор.
Проигрышь отображаем так — проходим все клетки, где у нас было закрыто и на самом деле была мина — отрисовываем мину. Где у нас стоял флаг, а на самом деле мины нету — отображаем ошибку. Так же блокируем методы open и close после проигрыша.
Как это сделано: пишем «Сапера» за 4 минуты
От переводчика: этот пост — перевод оригинальной статьи Маки Чиза, опытного кодера, который не только пишет классные программы, но и демонстрирует возможности различных языков своим коллегам, как новичкам, так и профессионалам.
«Сапер» — веселая игра, многие из нас в нее играют. Может быть, вы хотите сделать «Сапера» сами?
Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».
Уникальность игры в том, что она очень простая и при этом весьма увлекательная. В «Сапере» нет никакой хитрой геймплейной механики, вы просто нажимаете на квадратики, надеясь, что под ними нет мины.
Предлагаю попробовать написать «Сапера» на Processing. Это отличный инструмент, который позволяет создавать графические приложения на Java. Ссылка на него вот здесь.
Прежде чем начать, скажу, что этот туториал рассчитан на тех, кто уже знает Java. Опыт работы с Processing необязателен.
Итак, начинаем. Первый шаг — определение состояния игры. Я решил реализовать это вот так.
Здесь, кажется, имеет смысл все, кроме вот этого участка: int[][] mines. Почему сетка мин является целым числом, а не булевым? Дело в том, что это позволяет легко подсчитать, сколько мин находится рядом с определенной позицией.
Этот код определяет, сколько мин находится рядом с определенным участком. После того, как мы уберем исключения, мы получим что-то похожее на это:
Главная задача самой игры — раскрывать квадраты, начиная с точки х, у.
В итоге мы имеем следующий алгоритм:
Если вы хотите узнать о визуализации, читайте дальше.
Здесь все должно быть понятно, но я объясню сложные части.
Итак, у нас есть переменная cellSize, определяющая количество пикселей в каждом квадрате.
Таким образом мы создаем поле со сторонами gridW x gridH, где размеры каждого квадрата будут равняться значению cellSize.
Затем мы возвращаем переменные в изначальное состояние.
Для инициализации поля:
И далее включаем реакцию на клики мышью.
И функция отображения, вызываем ее один раз для каждого кадра.
И все, вот и наш «Сапер».
Игра выглядит просто, но в целом она полностью функциональна. И помните: «Сапер» вызывает привыкание!
Алгоритмы логики бота для игры «Сапёр»
Основной алгоритм
Основной алгоритм состоит в следующем. Неизвестные ячейки (класс Cell), прилегающие к одной открытой ячейке формируются в группу (класс Group), в которую записывается также значение ячейки, к которой она прилегает.
На рисунке обозначены четыре группы, некоторые из которых пересекаются, а некоторые и вовсе содержат другие группы. Обозначим (123,1) — группа состоит из ячеек 1,2 и 3, и при этом в них находится 1 мина. (5678,2) — в четырех ячейках находятся 2 мины.
Если нет достоверного решения
Вероятность наступления события А, состоящего в появлении хотя бы одного из событий А1, А2. Аn, независимых в совокупности, равна разности между единицей и произведением вероятностей противоположных событий. А=1- (1-A1)*(1-A2)*. *(1-An)
В смежных ячейках после применения этой формулы результат равен 1-(1-0,57)*(1-0,28)=0,69.
4/(0,485+0,485+0,485+0,373+0,373+0,373+0,373)=1,357
0,485*1,357=0,658 0,373*1,357=0,506
2/(0,169+0,169+0,169+0,506+0,506+0,506+0,506)=0,79
0,169*0,79=0,134 0,506*0,79=0,4
Последние ходы
На заключительном этапе игры большую роль играет количество не помеченных мин. Зная это количество можно перебором подставлять их в неизвестные ячейки, и отмечать подходящие варианты. В процессе перебора подходящих вариантов для каждой ячейки считаем количество пометок. Разделив получившиеся значения на общее число пометок получаем вероятность нахождения мин для каждой ячейки. Например в этой ситуации, имеющей, казалось бы только одно достоверное решение последний метод (LastTurns) нашел 3 ячейки с 0% вероятности нахождения мины.
LastTurns(9,21) проверил 144 подходящих комбинаций из 293930 возможных и нашел 3 ячеек с минимальной вероятностью 0 %
C точки зрения сложности понимания идеи этот метод самый легкий, поэтому подробно разбирать его пока не буду.
Вывод
Создание сапера при помощи модуля Tkinter
День добрый. Почти каждый начинающий программист стремится к созданию своей первой игры. Спустя пол года ленивого кропотливого обучения я решился написать сапера. Языком написания был выбран Python, модулем для добавления интерфейса tkinter, потому как уже имелся опыт работы с ним. Этот пост будет полезен скорее начинающим кодерам, но если вы итак все знаете, можете написать свои советы по улучшению кода в комменты.
Приступим. Первым делом нужно было определиться, что будет собой представлять клетка. Самое выгодное решение — создать класс поля:
Теперь надо создать интерфейс для настройки игры:
В итоге получаем вот такое вот окно:
Теперь нужно прописать функцию bombcounter
Теперь приступаем к основной части, написанию функции игры:
Все, что здесь происходит, это заполнение массива self.around. Мы рассматриваем различные случаи и на выходе получаем верный ответ. Если есть варианты, как сделать это проще, я приму их во внимание.
Итак. Сейчас у нас написаны функции: открытия клетки, заполнения массива around, начала игры и получения значения насчет размера игрового поля и кол-ва мин. Но до сих пор нет функции для установки мин. Исправляемся:
И вторая важная для нас функция: setValue()
На этом заканчивается основная часть. Игра может работать прямо сейчас, но без установки флажка и определения победы/проигрыша. Тут все просто. Установка флажка:
Функции lose() и winer() просты и не требуют объяснений. Если будет нужно, напишу в комменты.
Пишем сапёр на Unity. Настройка
Логические игры часто включают в себя клеточные поля, при этом клетки имеют определенные свойства и модели поведения. В этой серии уроков мы покажем вам, как создать простую версию классической игры Сапёр. Прилагаем список статей:
В первой части серии мы будем строить само игровое поле, сделав которое вы сможете написать свою игру, использующую клеточное поле. В качестве игрового движка будем использовать Unity, который можно скачать по этой ссылке. Вы должны иметь хоть какое-то представление о нем, но если вы новичок, то прочитайте нашу серию уроков по созданию арканоида в четырех частях:
Правила игры
Цель игры — найти все мины и не взорваться. Мины спрятаны в клетках, а сами поля бывают разных размеров от 9×9 (легкий уровень) до 16×30 (сложный уровень). Впрочем, никто не запрещает использовать любой понравившийся размер поля.
При нажатии на клетку вы «раскрываете» её. Если там мина — вы проиграли. Если же мины есть в рядом стоящих клетках, то на месте клетки, на которую вы нажали, появляется число, означающее количество мин вокруг нее. А если вокруг безопасно, то все близлежащие «безопасные» клетки раскроются.
Если вы уверены, что в какой-то клетке находится мина, смело жмите на нее правой кнопкой — клетка отметится флажком и вы не сможете «раскрыть» клетку, пока не снимите флажок. Это делается для того, чтобы вы случайно не открыли клетку, в которой точно находится мина.
После того, как все клетки с минами будут отмечены, вы выигрываете. Попробуйте поиграть в это демонстрационное приложение — именно такую игру в результате мы и должны получить.
Базовая клетка
Создайте новый проект Unity, добавьте на сцену кубик (Cube) и назовите его Tile. Перетащите его в папку с проектом для того, чтобы превратить его в префаб. Кубик пока ничего не умеет, но мы воспользуемся им, чтобы построить игровое поле, а затем добавим ему новых возможностей.
Генератор клеток
Создайте новый пустой объект и назовите его Grid. Так же как и кубик, сделайте его префабом (перетащив в папку с проектом). Этот объект — наш будущий генератор клеток, который и будет создавать игровое поле.
Создайте новый сценарий (в качестве языка программирования будем на этот раз использовать JavaScript) и также назовите его Grid. Прикрепите его к нашему генератору и пропишите в скрипте:
Сохраните скрипт и перетащите наш кубик Tile в поле Tile Prefab компонента Script объекта Grid. У вас должно получится вот так:
Названия переменных говорящие: numberOfTiles позволяет задать количество клеток на игровом поле, а distanceBetweenTiles задает расстояние между клетками.
В настоящий момент генератор клеток ничего не делает. Давайте добавим несколько строчек кода в метод CreateTiles:
Если вы нажмете на кнопку Play, то увидите нечто подобное:
Функция CreateTiles создает копии префаба кубика (столько раз, сколько мы задали) и помещает их в линию, где расстояние между кубиками равно distanceBetweenTiles. Попробуйте подобрать оптимальное расстояние, чтобы будущее поле выглядело красиво.
Но для Сапёра нам нужно поле в виде сетки, а не линии. Чтобы достичь этого, добавьте в сценарий объекта Grid новую переменную, которая будет отвечать за количество кубиков в строке:
public var tilesPerRow: int = 4;
И перепишем метод CreateTiles:
Запустив игру, вы увидите такую картину:
Вы наверняка поняли, что значение numberOfTiles должно делиться на tilesPerRow нацело, иначе полученное поле будет неправильным и некрасивым. Но наша реализация игры будет правильно работать и в случае неправильного поля.
Добавление мин
Теперь, когда мы создали основу, давайте поработаем с минами. Создайте новый сценарий, назовите его Tile и прикрепите его к префабу Tile. Добавьте строчку с объявлением переменной:
public var isMined: boolean = false;
Этот параметр нам и скажет, есть ли в клетке мина. Далее нам нужно позволить генератору создавать новый объект, который мы только что создали. Для этого измените тип переменной tilePrefab с GameObject на Tile в скрипте объекта Grid:
public var tilePrefab: Tile;
А теперь добавим новые переменные:
И не забудем про инициализацию:
И немного изменим команду Instantiate:
А в конце сценария выполним метод AssignMines. Вот так будет выглядеть измененный метод CreateTiles:
Метод AssignMines случайным образом задаст некоторым клеткам мины:
Как работает функция AssignMines? Дело в том, что все созданные клетки в методе CreateTiles помещаются в массив tilesAll. И уже в методе AssignMines они копируются в массив tilesUnmined. Далее случайным образом отбирается numberOfMines плиток. Параметр isMined отобранных плиток устанавливается в true, а они сами удаляются из массива tilesUnmined и помещаются в tilesMined.
На данный момент обычные клетки никак не отличаются от тех, которые с минами. Этот момент мы обязательно исправим, но вы уже сейчас можете проверить демо-игру, в которой можете по-своему настроить поле и количество мин (красные кубики).
Дизайн плиток
Сейчас наши клетки выглядят как кубики (по сути это и есть кубики). Давайте изменим их внешний вид.
В исходных файлах вы найдете 3D модель puzzleObjects.fbx. Скопируйте файл в папку с проектом для дальнейшего использования (убедитесь в том, что он импортируется с размером, установленным в 1):
Перейдите в настройки префаба Tile и поменяйте значение поля Mesh Filter на tileImproved.
И здесь же сбросьте значения компонента Box Collider, нажав на него правой кнопкой мыши и выбрав Reset.
И наконец, присвойте объекту новый материал, чтобы он не имел стандартный белый цвет.
Обратите внимание, мы поменяли параметры префаба лишь раз, но изменениям будут подвергнуты все созданные из него объекты. В этом и есть преимущество этих объектов.
Добавление чисел
Поверните текст так, чтобы он лежал на клетке, установите значение текста в 0, а его размер измените так, чтобы он не выглядел размытым.
Теперь мы должны получить доступ к тексту из кода. Добавьте следующую строчку в скрипт объекта Tile:
public var displayText: TextMesh;
Перетащите объект текста в новое поле компонента Script объекта Tile:
Вывод
И на этом наша статья подходит к концу. Мы создали функциональную основу для игры в жанре головоломок. Эта основа может быть использована вами не только для Сапёра.
В следующей части этой серии мы добавим больше возможностей клеткам и доведем игру до ума. Оставайтесь с нами!