Jump to content
  • 0

Делаем игру на JavaScript


Great Rash
 Share

Question

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

Ну да ладно, оставим лирику. Одно время я любил играть в Сапера (Mine Sweeper), я думаю все, кто хоть немного знаком с компьютерами, знают эту логическую игру. Вот я и подумал, а не написать ли мне своего сапера! Эта игра показалась мне достаточно простой для реализации, ведь там нет никакой супер физики, нет искуственного интеллекта и прочих сложных для новичка вещей. Сказано - сделано! Итак, я представляю вашему вниманию урок по написанию вашей первой игры, встречайте - Сапёр! (бурные овации, крики "браво!") ;)

Хватит слов, приступаем...

Базовая верстка

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

Вот так будет выглядеть наша страничка:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ru">

<head>
<title>Мой Сапёр</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8" />

<style type="text/css">
* {
margin: 0;
padding: 0;
}

html,
body {
font-family: "Trebuchet MS", Veradna, Tahoma, Arial, sans-serif;
font-size: 11px;
}

.game {
padding: 5px;
}

.game-cont {
border: 0;
}

.game-cont td {
padding: 5px;
vertical-align: top;
border: #9cb0ca 3px outset;
background: #bfd3ed;
}

.game-menu {
width: 150px;
}

.game-menu input {
width: 144px;
border: #9cb0ca 3px inset;
}

input.game-start-button {
width: 150px;
height: 26px;
margin-top: 1.5em;
border: #9cb0ca 3px outset;
background: #bfd3ed;
}

.game-stats {
text-align: center;
}

.game-board {
border: 0;
border-spacing: 0;
border-collapse: collapse;
}

.game-board td {
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
border: #9cb0ca 1px solid;
}
</style>

<script type="text/javascript">
// тут будем писать скрипт
</script>
</head>

<body>



</body>
</html>

Плюс для работы нашего сапера нам потребуется навешивать события. Для этого можно использовать связку element.addEventListener() и element.attachEvent(), но у них есть свои проблемы. По этому я погуглил и нашел неплохой урок по созданию библиотеки для работы с добавлением и удалением обработчиков событий. Почитать этот, несомненно, полезный и нужный урок вы можете тут. Настоятельно рекомендую всем ознакомиться. Так же там в конце статьи есть ссылка на скачивание библиотеки event.js. В данной библиотеке решено большинство проблем, связанных с кроссбраузерной обработкой событий, поэтому в нашем проекте мы будем использовать ее.

Подключаем библиотеку в наш проект:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ru">

<head>
<title>Мой Сапёр</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8" />

<style type="text/css">
* {
margin: 0;
padding: 0;
}

html,
body {
font-family: "Trebuchet MS", Veradna, Tahoma, Arial, sans-serif;
font-size: 11px;
}

.game {
padding: 5px;
}

.game-cont {
border: 0;
}

.game-cont td {
padding: 5px;
vertical-align: top;
border: #9cb0ca 3px outset;
background: #bfd3ed;
}

.game-menu {
width: 150px;
}

.game-menu input {
width: 144px;
border: #9cb0ca 3px inset;
}

input.game-start-button {
width: 150px;
height: 26px;
margin-top: 1.5em;
border: #9cb0ca 3px outset;
background: #bfd3ed;
}

.game-stats {
text-align: center;
}

.game-board {
border: 0;
border-spacing: 0;
border-collapse: collapse;
}

.game-board td {
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
border: #9cb0ca 1px solid;
}
</style>

<!-- Подключаем event.js -->
<script src="event.js" type="text/javascript"></script>

<!-- А тут уже будет наш код -->
<script type="text/javascript">
// тут будем писать скрипт
</script>
</head>

<body>



</body>
</html>

Использовать эту библиотеку очень просто, чтобы добавить обработчик для какого либо события нужно вызвать метод add(), а чтобы удалить событие - remove():

// к примеру добавим алерт на клик
var clickHandler = function() {
alert('ok!');
}

// вызываем метод add() объекта Event
// первый параметр - елемент к которому цепляем обработчик
// второй параметр - событие на которое будет срабатывать функция (без префикса "on"! не забывайте)
// третий параметр - функция-обработчик события
Event.add(element, 'click', clickHandler);

// то же самое для remove()
Event.remove(element, 'click', clickHandler);

Просто правда?

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

Проектирование

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

Ну-с продолжим...

Создаем объект

Для тех кто не знаком с ООП в яваскрипте рекомендую заглянуть в эту тему, там я описывал основные принципы объектно-ориентированного программирования в JavaScript и приводил ссылки на ресурсы где можно почитать об этом более подробно. Для тех кто уже дружит с объектами в JavaScript приступим к созданию нашего объекта (HTML-код я опускаю, больше он меняться не будет, далее будет идти только яваскрипт):

var MyMineSweeper = {
// наш главный метод, он будет инициализировать нашего сапёра
init: function(o) {
this.W = o ? o.W : 7;
this.H = o ? o.H : 7;
this.bombs = o ? o.bombs : Math.floor(this.W * this.H / 4);
this.placedBombs = this.bombs;

this.generateGUI();
}
}

Итак, что тут происходит:

Мы создаем главный метод - init(), который будет запускать нашего сапера в первый раз, а так же когда юзер захочет переиграть заново. В качестве параметра этот метод будет принимать объектный литерал (или объект в формате JSON - JavaScript Object Notation), в котором будут задаваться необходимые параметры для построения игрового поля (далее я буду назвать игровое поле просто доской, для краткости).

Вот какие данные мы описываем в методе init():

1) this.W - ширина доски (от слова width), по умолчанию будет равна 7 (формат ширины у нас будет задаваться не в каких-либо единицах измерения, а в клетках - 7 означает, что ширина доски будет 7 клеток)

2) this.H - высота доски (от слова height), по умолчанию тоже 7 клеток

3) this.bombs - количество бомб на доске, если не задано иное будет заполнена 1/4 доски (я не знаю сколько бомб на доске в настоящем сапёре, поэтому поставил так чтобы черверть доски была заполнена бомбами)

4) this.placedBombs - сколько бомб уже установлено (это число будет уменьшаться по мере того как программа будет расставлять мины на карте)

5) this.generateGUI() - этот метод будет генерить GUI (Graphical User Interface - по русски все что юзер будет видеть на экране, т.е. наш HTML)

Я не хочу создавать клон виндового сапёра, поэтому я решил, что не стоит ограничивать пользователя всего тремя досками (маленькая, большая и средняя), в нашем мега-сапёре юзер сам решит сколько на сколько будет у него доска и сколько на ней будет бомб, для сего мы предусмотрим три поля ввода. Метод init() будет устанавливать все данные по умолчанию, но если мы передадим ему как параметр объектный литерал (я его обозвал o - коротко и ясно, но вообще такие короткие названия переменным давать не стоит ибо потом запутаться где что можно), то наш сапёр примет те параметры которые пользователь ввел в поля ввода.

Сам GUI мы будем строить на таблицах. "Почему не на дивах?", - спросят некоторые сильно продвинутые верстальщики. А все потому, что нам пофиг на семантику и т.п., а таблицы гибче, требуют меньше усилий на верстку и не развалятся в нашем обожаемом ИЕ. Но если вы хотите, то стройте на дивах, может оно и красивше выйдет.

Итак наш GUI будет содержать:

1) поле ввода для задания ширины доски (в клетках)

2) поле ввода для задания высоты доски (в клетках)

3) поле ввода для задания количества бомб на доске

4) кнопку с надписью "Старт!", по нажатию на которую будет перезапускаться игра

5) область статистики (туда мы будем писать Game Over уже или еще нет)

6) собственно саму доску, на которой юзер будет искать мины

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

Критику, советы, предложения, вопросы, хвальбы, лесть и т.п. прошу писать в личку.

P.S. Вот что у нас получится в итоге: http://mine-sweeper.narod.ru/

Link to comment
Share on other sites

3 answers to this question

Recommended Posts

  • 0

Создаем GUI

Итак, давайте приступим к созданию нашего интерфеса. Все стили для него я уже привел, так что ничто не будет отвлекать нас от чистого яваскрипта, ура! Метод, который будет строить наш интерфейс мы назовем generateGUI (многие уже и так об этом догадалиь, внимательно посмотрев предыдущую часть урока).

Для создания элементов на странице можно воспользоваться тремя способами:

1) использовать метод write или writeln

2) использовать метод innerHTML

3) использовать связку createElement и appendChild

Первый метод пишет что-либо в документ в том месте в котором вызван, различие между write и writeln в том, что writeln добавляет в конце перенос строки (типа \r\n - возврат каретки и новая строка).

Второй метод записывает HTML в выбранную ноду.

Третий метод (он типа самый правильный) работает с DOM (объектной моделью документа) и использует ее методы для создания элемента (document.createElement('имя_тега')) и втравки созданного элемента на страницу в выбранную ноду (element.appendChild(newElement)).

Я создавал своего сапёра с таким расчетом, чтобы его можно было вставить в любое место на сайте (типа как виджет, чтобы пользователей развлекать). Поэтому я решил соединить первый и третий методы создания объектов на странице. А именно, мы сначала создадим контейнер при помощи document.writeln('html'), а потом в него понапихаем все задуманные элементы интерфейса.

Итак, приступим к написанию кода. Для создания основного контейнера я выделил специальный метод у нашего объекта MyMineSweeper и назвал его writeMainContainer:

var MyMineSweeper = {
// наш главный метод, он будет инициализировать нашего сапёра
init: function(o) {
this.W = o ? o.W : 7;
this.H = o ? o.H : 7;
this.bombs = o ? o.bombs : Math.floor(this.W * this.H / 4);
this.placedBombs = this.bombs;

this.generateGUI();
},

generateGUI: function() {
/*
* интерфейс будет создаваться только один раз,
* поэтому, чтобы не появилось дубликатов, проверяем
* не создан ли уже наш контейнер
*/
if (!this.game) { // если такой переменной еще нет
this.game = this.writeMainContainer(); // то создаем наш контейнер
}
},

// метод который пишет в документ код нашего контейнера
writeMainContainer: function() {
// это, собственно, html-текст, который будет вписан в документ
var html = '<div id="game" class="game"></div>';

// вписываем его в документ
document.writeln(html);

/*
* а потом, находим наш контейтнер по ID и возвращаем его,
* чтобы получить ссылку на ноду
* и работать уже с полученным объектом
*/
return document.getElementById('game');
}
}

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

<!-- допустим создаем сапёра в <body> -->
<body>

<script type="text/javascript">
MyMineSweeper.init();
</script>

</body>

Причем если вы вызовете метод init() два раза (или больше), нового контейнера создано не будет.

Теперь надо немного остановиться на одном из элементов DOM, который немного не похож на другие - я имею в виду таблицу (<table>). Все дело в том как в таблицах создаются и находятся строки и столбцы. Дело в том, что таблица сама по себе является двумерным массивом (т.е. массивом, каждый элемент которого тоже массив). Вы конечно можете создавать строки и ячейки в таблице при помощи createElement, но гораздо правильней использовать для этого специальные методы:

1) метод table.insertRow(index) - создает новую строку (<tr>) и возвращает ссылку на нее, где index - это номер создаваемой строки (начиная с нуля, как в обычном массиве).

Пример:

// создаем таблицу
var table = document.createElement('table');

// создаем строку
var row = table.insertRow(0);
alert(row);

// создаем еще одну строку,
// которая будет вставлена под первой
var row2 = table.insertRow(1);

// если строка будет с меньшим или с одинаковым индексом,
// то она будет вставлена над уже созданной строкой
var row3 = table.insertRow(0); // эта строка будет выше всех

2) метод row.insertCell(index) - создает новую ячейку в уже созданной строке и возвращает ссылку на нее, где index - это номер создаваемой ячейки (начиная с нуля)

Пример:

// создаем таблицу
var table = document.createElement('table');

// создаем строку
var row = table.insertRow(0);
alert(row);

// создаем ячейку
var cell = row.insertCell(0);
alert(cell);

// аналогично строкам, если задать меньший или одинаковый индекс
// ячейка будет вставлена впереди
var cell2 = row.insertCell(0); // эта ячейка будет первой

Находить строки и ячейки можно при помощи методов rows() и cells() объекта таблицы. Эти методы содержат массивы строк и ячеек соответственно. Таким образом, создавая таблицу, мы сразу имеем двумерный массив, с которым удобно работать, обходя его, напрмер, при помощи цикла for. Ну вот, вроде про таблицы рассказал.

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

var MyMineSweeper = {
// наш главный метод, он будет инициализировать нашего сапёра
init: function(o) {
this.W = o ? o.W : 7;
this.H = o ? o.H : 7;
this.bombs = o ? o.bombs : Math.floor(this.W * this.H / 4);
this.placedBombs = this.bombs;

this.generateGUI();
},

generateGUI: function() {
// интерфейс будет создаваться только один раз
if (!this.game) {
this.game = this.writeMainContainer();

// создаем основную таблицу, состоящую из двух строк (первая - статистика, вторая - игровое поле и инпуты)
this.gameCont = document.createElement('table');
this.gameCont.className = 'game-cont'; // присваиваем нужное имя css-класса

// эти строки нам как объекты не нужны, поэтому переменных не создаем
this.gameCont.insertRow(0); // создаем первую строку
this.gameCont.insertRow(1); // и вторую строку

// создаем ячейку для инпутов (игровое меню)
this.gameMenu = this.gameCont.rows[0].insertCell(0);
this.gameMenu.rowSpan = 2; // оно будет занимать 2 строки
this.gameMenu.className = 'game-menu'; // назначаем класс
this.gameMenu.appendChild(document.createTextNode('Ширина поля:')); // втавляем текстовую ноду
this.gameMenu.appendChild(document.createElement('br')); // и перенос строки (<br />)

// создаем инпут для ввода ширины доски
this.WInput = document.createElement('input');
this.WInput.type = 'text'; // назначаем тип инпута - текст

this.gameMenu.appendChild(this.WInput); // вставляем созданный инпут
this.gameMenu.appendChild(document.createTextNode('Высота поля:')); // вставляем текстовую ноду
this.gameMenu.appendChild(document.createElement('br')); // вставляем перенос

// все тоже самое для инпуда, предназначенного для ввода высоты доски
this.HInput = document.createElement('input');
this.HInput.type = 'text'; // назначаем тип инпута - текст

this.gameMenu.appendChild(this.HInput);
this.gameMenu.appendChild(document.createTextNode('Кол-во мин:'));
this.gameMenu.appendChild(document.createElement('br'));

// создаем инпут для ввода количества бомб
this.BInput = document.createElement('input');
this.BInput.type = 'text'; // назначаем тип инпута - текст

// создаем кнопку запуска игры
this.Init = document.createElement('input');
this.Init.type = 'button'; // назначаем тип инпута - кнопка
this.Init.value = 'Старт'; // надпись на кнопке
this.Init.className = 'game-start-button'; // назначаем класс

// вставляем инпут и кнопку
this.gameMenu.appendChild(this.BInput);
this.gameMenu.appendChild(this.Init);

// создаем ячейку статистики
this.gameStats = this.gameCont.rows[0].insertCell(1);

// создаем ячейку для игрового поля
this.gameField = this.gameCont.rows[1].insertCell(0);
this.gameField.className = 'game-field'; // назначаем класс

// наконец, вставляем наш основной контейнер внутрь основного
this.game.appendChild(this.gameCont);
}

// если играем второй раз, то старую доску надо заменить на новую
if (this.board) { // если доска уже существует
this.board.parentNode.removeChild(this.board); // удаляем ее из дерева нод
this.board = null; // и очищаем переменную
}

/*
* игровую доску будем генерировать кажный раз заново
* поэтому создаем ее вне условия
* метод generateField() будет генерить доску (расставлять бомбы, вписывать цифры вокруг бомб и т.п.),
* а потом вернет созданную таблицу
*/
this.board = this.generateField();
this.board.cellSpacing = 0; // убираем отступы между ячейками (нельзя задать в CSS кроссбраузерно)
this.board.className = 'game-board'; // назначаем класс

// вставляем доску в ячейку
this.gameField.appendChild(this.board);

// записываем в поле статистики размер созданной доски и количество бомб на поле
this.gameStats.innerHTML = 'Новая игра: поле ' + this.W + 'x' + this.H + ', ' + this.bombs + 'мин';

// в этом методе будут назначаться обработчики событий интерфейса
this.setupEvents();
},

writeMainContainer: function() {
var html = '<div id="game" class="game"></div>';
document.writeln(html);
return document.getElementById('game');
}
}

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

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

Критику, советы, прочее и прочее шлите в приват.

Link to comment
Share on other sites

  • 0

Назначаем обработчкики событий

Вообще этот метод у меня самый короткий наверное, т.к. в моем интерфейсе кнопка всего одна :)

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


var MyMineSweeper = {
init: function(o) {
this.W = o ? o.W : 7;
this.H = o ? o.H : 7;
this.bombs = o ? o.bombs : Math.floor(this.W * this.H / 4);
this.placedBombs = this.bombs;

this.generateGUI();
},

generateGUI: function() {
if (!this.game) {
this.game = this.writeMainContainer();

this.gameCont = document.createElement('table');
this.gameCont.className = 'game-cont';

this.gameCont.insertRow(0);
this.gameCont.insertRow(1);

this.gameMenu = this.gameCont.rows[0].insertCell(0);
this.gameMenu.rowSpan = 2;
this.gameMenu.className = 'game-menu';
this.gameMenu.appendChild(document.createTextNode('Ширина поля:'));
this.gameMenu.appendChild(document.createElement('br'));

this.WInput = document.createElement('input');
this.WInput.type = 'text';

this.gameMenu.appendChild(this.WInput);
this.gameMenu.appendChild(document.createTextNode('Высота поля:'));
this.gameMenu.appendChild(document.createElement('br'));

this.HInput = document.createElement('input');
this.HInput.type = 'text';

this.gameMenu.appendChild(this.HInput);
this.gameMenu.appendChild(document.createTextNode('Кол-во мин:'));
this.gameMenu.appendChild(document.createElement('br'));

this.BInput = document.createElement('input');
this.BInput.type = 'text';

this.Init = document.createElement('input');
this.Init.type = 'button';
this.Init.value = 'Старт';
this.Init.className = 'game-start-button';

this.gameMenu.appendChild(this.BInput);
this.gameMenu.appendChild(this.Init);

this.gameStats = this.gameCont.rows[0].insertCell(1);

this.gameField = this.gameCont.rows[1].insertCell(0);
this.gameField.className = 'game-field';

this.game.appendChild(this.gameCont);
}

if (this.board) {
this.board.parentNode.removeChild(this.board);
this.board = null;
}

this.board = this.generateField();
this.board.cellSpacing = 0;
this.board.className = 'game-board';

this.gameField.appendChild(this.board);

this.gameStats.innerHTML = 'Новая игра: поле ' + this.W + 'x' + this.H + ', ' + this.bombs + 'мин';

this.setupEvents();
},

setupEvents: function() {
var self = this;

var buttonClick = function() {
self.init({W: self.WInput.value, H: self.HInput.value, bombs: self.BInput.value});
}

Event.add(this.Init, 'click', buttonClick);
},

generateField: function() {
var self = this;
var table = document.createElement('table');

for (var i = 0; i < this.H; i++) {
var r = table.insertRow(i);

for (var j = 0; j < this.W; j++) {
var c = r.insertCell(j);
c.num = 0;
c.index = [i, j];

c.clickHandler = function() {
self.showInfo(this);
}

Event.add(c, 'click', c.clickHandler);
}
}

do {
var hNum = this.rand(0, this.H - 1);
var wNum = this.rand(0, this.W - 1);

if (!table.rows[hNum].cells[wNum].bomb) {
table.rows[hNum].cells[wNum].num = null;
table.rows[hNum].cells[wNum].bomb = true;
this.placedBombs--;
}
} while (this.placedBombs > 0);

for (var i = 0, len = table.rows.length; i < len; i++) {
for (var j = 0, len2 = table.rows[i].cells.length; j < len2; j++) {
if (table.rows[i].cells[j].bomb) {
this.placeNumbers(table, j, i);
}
}
}

return table;
},

placeNumbers: function(t, x, y) {
if (x > 0) {
t.rows[y].cells[x - 1].num++;
}

if (x < this.W - 1) {
t.rows[y].cells[x + 1].num++;
}

if (x > 0 && y > 0) {
t.rows[y - 1].cells[x - 1].num++;
}

if (y > 0) {
t.rows[y - 1].cells[x].num++;
}

if (y > 0 && x < this.W - 1) {
t.rows[y - 1].cells[x + 1].num++;
}

if (x > 0 && y < this.H - 1) {
t.rows[y + 1].cells[x - 1].num++;
}

if (y < this.H - 1) {
t.rows[y + 1].cells[x].num++;
}

if (x < this.W - 1 && y < this.H - 1) {
t.rows[y + 1].cells[x + 1].num++;
}
},

writeMainContainer: function() {
var html = '<div id="game" class="game"></div>';
document.writeln(html);
return document.getElementById('game');
},

rand: function(min, max) {
min = parseInt(min);
max = parseInt(max);

return Math.floor(Math.random() * (max - min + 1)) + min;
}
}

Пока все. Продолжение будет.

Все вопросы, предложения и прочее в приват. Не забываем про лесть :)

Link to comment
Share on other sites

  • 0

Обрабатываем клик по клетке

Итак, у нас уже почти все готово для полноценной игры. Осталось только обработать клик по клетке на доске. Давайте создадим метод который назовем showInfo, внутри данного метода мы будем проверять проиграл ли уже пользователь, что отобразить на клетке и т.п. Метод будет принимать в качестве параметра элемент на котором кликнули:


var MyMineSweeper = {
init: function(o) {
this.W = o ? o.W : 7;
this.H = o ? o.H : 7;
this.bombs = o ? o.bombs : Math.floor(this.W * this.H / 4);
this.placedBombs = this.bombs;

this.generateGUI();
},

generateGUI: function() {
if (!this.game) {
this.game = this.writeMainContainer();

this.gameCont = document.createElement('table');
this.gameCont.className = 'game-cont';

this.gameCont.insertRow(0);
this.gameCont.insertRow(1);

this.gameMenu = this.gameCont.rows[0].insertCell(0);
this.gameMenu.rowSpan = 2;
this.gameMenu.className = 'game-menu';
this.gameMenu.appendChild(document.createTextNode('Ширина поля:'));
this.gameMenu.appendChild(document.createElement('br'));

this.WInput = document.createElement('input');
this.WInput.type = 'text';

this.gameMenu.appendChild(this.WInput);
this.gameMenu.appendChild(document.createTextNode('Высота поля:'));
this.gameMenu.appendChild(document.createElement('br'));

this.HInput = document.createElement('input');
this.HInput.type = 'text';

this.gameMenu.appendChild(this.HInput);
this.gameMenu.appendChild(document.createTextNode('Кол-во мин:'));
this.gameMenu.appendChild(document.createElement('br'));

this.BInput = document.createElement('input');
this.BInput.type = 'text';

this.Init = document.createElement('input');
this.Init.type = 'button';
this.Init.value = 'Старт';
this.Init.className = 'game-start-button';

this.gameMenu.appendChild(this.BInput);
this.gameMenu.appendChild(this.Init);

this.gameStats = this.gameCont.rows[0].insertCell(1);

this.gameField = this.gameCont.rows[1].insertCell(0);
this.gameField.className = 'game-field';

this.game.appendChild(this.gameCont);
}

if (this.board) {
this.board.parentNode.removeChild(this.board);
this.board = null;
}

this.board = this.generateField();
this.board.cellSpacing = 0;
this.board.className = 'game-board';

this.gameField.appendChild(this.board);

this.gameStats.innerHTML = 'Новая игра: поле ' + this.W + 'x' + this.H + ', ' + this.bombs + 'мин';

this.setupEvents();
},

setupEvents: function() {
var self = this;

var buttonClick = function() {
self.init({W: self.WInput.value, H: self.HInput.value, bombs: self.BInput.value});
}

Event.add(this.Init, 'click', buttonClick);
},

generateField: function() {
var self = this;
var table = document.createElement('table');

for (var i = 0; i < this.H; i++) {
var r = table.insertRow(i);

for (var j = 0; j < this.W; j++) {
var c = r.insertCell(j);
c.num = 0;
c.index = [i, j];

c.clickHandler = function() {
self.showInfo(this);
}

Event.add(c, 'click', c.clickHandler);
}
}

do {
var hNum = this.rand(0, this.H - 1);
var wNum = this.rand(0, this.W - 1);

if (!table.rows[hNum].cells[wNum].bomb) {
table.rows[hNum].cells[wNum].num = null;
table.rows[hNum].cells[wNum].bomb = true;
this.placedBombs--;
}
} while (this.placedBombs > 0);

for (var i = 0, len = table.rows.length; i < len; i++) {
for (var j = 0, len2 = table.rows[i].cells.length; j < len2; j++) {
if (table.rows[i].cells[j].bomb) {
this.placeNumbers(table, j, i);
}
}
}

return table;
},

placeNumbers: function(t, x, y) {
if (x > 0) {
t.rows[y].cells[x - 1].num++;
}

if (x < this.W - 1) {
t.rows[y].cells[x + 1].num++;
}

if (x > 0 && y > 0) {
t.rows[y - 1].cells[x - 1].num++;
}

if (y > 0) {
t.rows[y - 1].cells[x].num++;
}

if (y > 0 && x < this.W - 1) {
t.rows[y - 1].cells[x + 1].num++;
}

if (x > 0 && y < this.H - 1) {
t.rows[y + 1].cells[x - 1].num++;
}

if (y < this.H - 1) {
t.rows[y + 1].cells[x].num++;
}

if (x < this.W - 1 && y < this.H - 1) {
t.rows[y + 1].cells[x + 1].num++;
}
},

showInfo: function(elem) {
if (!elem.bomb) {
this.openCell(elem);
} else {
this.openCell(elem);
this.gameOver();
}
},

openCell: function(elem) {
if (!elem.bomb) {
if (elem.num > 0) {
elem.innerHTML = '<b>' + elem.num + '</b>';
} else {
elem.innerHTML = ' ';
}

switch (elem.num) {
case 1:
elem.style.color = 'blue';
break;
case 2:
elem.style.color = 'green';
break;
case 3:
elem.style.color = 'red';
break;
case 4:
elem.style.color = 'dakrblue';
break;
case 5:
elem.style.color = 'pink';
break;
case 6:
elem.style.color = 'navy';
break;
case 7:
elem.style.color = 'brown';
break;
case 8:
elem.style.color = 'grey';
break;
default:
elem.style.color = 'black';
}
} else {
elem.innerHTML = '<b>M</b>';
}

elem.style.background = '#d8e0ec';
},

writeMainContainer: function() {
var html = '<div id="game" class="game"></div>';
document.writeln(html);
return document.getElementById('game');
},

rand: function(min, max) {
min = parseInt(min);
max = parseInt(max);

return Math.floor(Math.random() * (max - min + 1)) + min;
}
}

Обрабатываем Game Over

Теперь подумаем, что должно происходить когда юзер нарвался на мину:

1) нам нужно показать все мины на доске,

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

3) нам нужно уведомить пользователя о том что он проиграл.

Реализуем все в коде:

gameOver: function() {
// пробегаемся циклом по доске
for (var i = 0, len1 = this.board.rows.length; i < len1; i++) { // цикл по вертикали
for (var j = 0, len2 = this.board.rows[i].cells.length; j < len2; j++) { // цикл по горизонтали
if (this.board.rows[i].cells[j].bomb) { // если на клетке стоит мина
// показываем мину
this.openCell(this.board.rows[i].cells[j]);
}

// снимаем обработчик события "click" с каждой клетки
Event.remove(this.board.rows[i].cells[j], 'click', this.board.rows[i].cells[j].clickHandler);
}
}

// красим ячейку статистики в красный цвет
this.gameStats.style.background = 'red';
// и говорим юзеру, что он подорвался
this.gameStats.innerHTML = '<b>БАБАХ!!! GAME OVER!</b>';
}

У нас все готово для полноценной игры. Ура!

Однако радоваться рано. Если посмотреть на настоящего сапёра, то можно увидет одну особенность - если мы кликнем на клетку рядом с которой нет мин (т.е. номер на ней 0), то откроются все соседние пустые клетки, т.е. все клетки, граничащие с открытой, рядом к которыми тоже нет мин. То есть происходит так называемый каскад. Пожалуй это будет самая сложная функция в нашем сапёре.

Итак...

Реализуем каскад

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

И вот, наконец я допер, что в нелегком деле каскадирования нам поможет рекурсия!

Что такое рекурсия?

Это когда функция вызывает сама себя.

Что обязательно должно быть в рекурсивной функции?

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

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

// функция принимает в качестве параметра начальное число и конечное число
function myRecursion(num, maxNum) {
/*
* первым делом продумываем
* выход из бесконечной рекурсии
*/

if (num > maxNum) { // если начальное число больше конечного
// выходим из функции
return;
}

// пишем в документ текущую цифру
document.write(num + ', ');
// увеличиваем число на 1
num++;

// вызываем саму себя (рекурсивный вызов)
myRecursion(num, maxNum);
}

myRecursion(0, 5); // выведет 0, 1, 2, 3, 4, 5

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

roll: function(x, y) {
/*
* первым делом продумываем
* выход из бесконечной рекурсии
*/

// если x или y выходят за границы доски
if (x < 0 || y < 0 || x >= this.W || y >= this.H) {
// выходим
return;
}

// открываем клетку
this.openCell(this.board.rows[y].cells[x]);

// если рядом с клеткой есть бомба (т.е. номер в ней больше 0)
if (this.board.rows[y].cells[x].num > 0) {
// вставляем в клетку цифру
this.board.rows[y].cells[x].innerHTML = '<b>' + this.board.rows[y].cells[x].num + '</b>';
// выходим
return;
}

// если мы уже эту клетку проверяли
if (this.board.rows[y].cells[x].check) {
// выходим
return;
}

// отмечаем, что эту ячейку мы уже проверяли
this.board.rows[y].cells[x].check = true;

// далее пошла рекурсия
this.roll(x + 1, y); // делаем шаг вправо
this.roll(x - 1, y); // делаем шаг влево
this.roll(x, y + 1); // делаем шаг вниз
this.roll(x, y - 1); // делаем шаг вверх
this.roll(x - 1, y - 1); // делаем шаг вверх влево
this.roll(x + 1, y - 1); // делаем шаг вверх вправо
this.roll(x - 1, y + 1); // делаем шаг вниз влево
this.roll(x + 1, y + 1); // делаем шаг вниз вправо
}

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

showInfo: function(elem) {
if (!elem.bomb) { // если в клетке нет мины
if (elem.num > 0) { // если рядом есть мины (номер больше 0)
// открываем клетку
this.openCell(elem);
} else { // если рядом нет ни одной мины
/*
* запускаем каскад
* тут нам и пригодилось дополнительное
* свойство хранимое в клетке (index),
* в котором храняться в виде массива
* координаты клетки на доске
*/
this.roll(elem.index[1], elem.index[0]);
}
} else { // если в клетке мина
// открываем клетку
this.openCell(elem);
// завершаем игру
this.gameOver();
}
}

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

Ах да, вот полный код нашего сапера:

var MyMineSweeper = {
init: function(o) {
this.W = o ? o.W : 7;
this.H = o ? o.H : 7;
this.bombs = o ? o.bombs : Math.floor(this.W * this.H / 4);
this.placedBombs = this.bombs;

this.generateGUI();
},

generateGUI: function() {
if (!this.game) {
this.game = this.writeMainContainer();

this.gameCont = document.createElement('table');
this.gameCont.className = 'game-cont';

this.gameCont.insertRow(0);
this.gameCont.insertRow(1);

this.gameMenu = this.gameCont.rows[0].insertCell(0);
this.gameMenu.rowSpan = 2;
this.gameMenu.className = 'game-menu';
this.gameMenu.appendChild(document.createTextNode('Ширина поля:'));
this.gameMenu.appendChild(document.createElement('br'));

this.WInput = document.createElement('input');
this.WInput.type = 'text';

this.gameMenu.appendChild(this.WInput);
this.gameMenu.appendChild(document.createTextNode('Высота поля:'));
this.gameMenu.appendChild(document.createElement('br'));

this.HInput = document.createElement('input');
this.HInput.type = 'text';

this.gameMenu.appendChild(this.HInput);
this.gameMenu.appendChild(document.createTextNode('Кол-во мин:'));
this.gameMenu.appendChild(document.createElement('br'));

this.BInput = document.createElement('input');
this.BInput.type = 'text';

this.Init = document.createElement('input');
this.Init.type = 'button';
this.Init.value = 'Старт';
this.Init.className = 'game-start-button';

this.gameMenu.appendChild(this.BInput);
this.gameMenu.appendChild(this.Init);

this.gameStats = this.gameCont.rows[0].insertCell(1);

this.gameField = this.gameCont.rows[1].insertCell(0);
this.gameField.className = 'game-field';

this.game.appendChild(this.gameCont);
}

if (this.board) {
this.board.parentNode.removeChild(this.board);
this.board = null;
}

this.board = this.generateField();
this.board.cellSpacing = 0;
this.board.className = 'game-board';

this.gameField.appendChild(this.board);

this.gameStats.innerHTML = 'Новая игра: поле ' + this.W + 'x' + this.H + ', ' + this.bombs + 'мин';
this.WInput.value = this.W;
this.HInput.value = this.H;
this.BInput.value = this.bombs;
this.gameStats.style.background = '#bfd3ed';

this.setupEvents();
},

setupEvents: function() {
var self = this;

var buttonClick = function() {
self.init({W: self.WInput.value, H: self.HInput.value, bombs: self.BInput.value});
}

Event.add(this.Init, 'click', buttonClick);
},

generateField: function() {
var self = this;
var table = document.createElement('table');

for (var i = 0; i < this.H; i++) {
var r = table.insertRow(i);

for (var j = 0; j < this.W; j++) {
var c = r.insertCell(j);
c.num = 0;
c.index = [i, j];

c.clickHandler = function() {
self.showInfo(this);
}

Event.add(c, 'click', c.clickHandler);
}
}

do {
var hNum = this.rand(0, this.H - 1);
var wNum = this.rand(0, this.W - 1);

if (!table.rows[hNum].cells[wNum].bomb) {
table.rows[hNum].cells[wNum].num = null;
table.rows[hNum].cells[wNum].bomb = true;
this.placedBombs--;
}
} while (this.placedBombs > 0);

for (var i = 0, len = table.rows.length; i < len; i++) {
for (var j = 0, len2 = table.rows[i].cells.length; j < len2; j++) {
if (table.rows[i].cells[j].bomb) {
this.placeNumbers(table, j, i);
}
}
}

return table;
},

placeNumbers: function(t, x, y) {
if (x > 0) {
t.rows[y].cells[x - 1].num++;
}

if (x < this.W - 1) {
t.rows[y].cells[x + 1].num++;
}

if (x > 0 && y > 0) {
t.rows[y - 1].cells[x - 1].num++;
}

if (y > 0) {
t.rows[y - 1].cells[x].num++;
}

if (y > 0 && x < this.W - 1) {
t.rows[y - 1].cells[x + 1].num++;
}

if (x > 0 && y < this.H - 1) {
t.rows[y + 1].cells[x - 1].num++;
}

if (y < this.H - 1) {
t.rows[y + 1].cells[x].num++;
}

if (x < this.W - 1 && y < this.H - 1) {
t.rows[y + 1].cells[x + 1].num++;
}
},

showInfo: function(elem) {
if (!elem.bomb) {
if (elem.num > 0) {
this.openCell(elem);
} else {
this.roll(elem.index[1], elem.index[0]);
}
} else {
this.openCell(elem);
this.gameOver();
}
},

openCell: function(elem) {
if (!elem.bomb) {
if (elem.num > 0) {
elem.innerHTML = '<b>' + elem.num + '</b>';
} else {
elem.innerHTML = ' ';
}

switch (elem.num) {
case 1:
elem.style.color = 'blue';
break;
case 2:
elem.style.color = 'green';
break;
case 3:
elem.style.color = 'red';
break;
case 4:
elem.style.color = 'dakrblue';
break;
case 5:
elem.style.color = 'pink';
break;
case 6:
elem.style.color = 'navy';
break;
case 7:
elem.style.color = 'brown';
break;
case 8:
elem.style.color = 'grey';
break;
default:
elem.style.color = 'black';
}
} else {
elem.innerHTML = '<b>M</b>';
}

elem.style.background = '#d8e0ec';
},

gameOver: function() {
for (var i = 0, len1 = this.board.rows.length; i < len1; i++) {
for (var j = 0, len2 = this.board.rows[i].cells.length; j < len2; j++) {
if (this.board.rows[i].cells[j].bomb) {
this.openCell(this.board.rows[i].cells[j]);
}

Event.remove(this.board.rows[i].cells[j], 'click', this.board.rows[i].cells[j].clickHandler);
}
}

this.gameStats.style.background = 'red';
this.gameStats.innerHTML = '<b>БАБАХ!!! GAME OVER!</b>';
},

roll: function(x, y) {
if (x < 0 || y < 0 || x >= this.W || y >= this.H) {
return;
}

this.openCell(this.board.rows[y].cells[x]);

if (this.board.rows[y].cells[x].num > 0) {
this.board.rows[y].cells[x].innerHTML = '<b>' + this.board.rows[y].cells[x].num + '</b>';
return;
}

if (this.board.rows[y].cells[x].check) {
return;
}

this.board.rows[y].cells[x].check = true;

this.roll(x + 1, y);
this.roll(x - 1, y);
this.roll(x, y + 1);
this.roll(x, y - 1);
this.roll(x - 1, y - 1);
this.roll(x + 1, y - 1);
this.roll(x - 1, y + 1);
this.roll(x + 1, y + 1);
},

writeMainContainer: function() {
var html = '<div id="game" class="game"></div>';
document.writeln(html);
return document.getElementById('game');
},

rand: function(min, max) {
min = parseInt(min);
max = parseInt(max);

return Math.floor(Math.random() * (max - min + 1)) + min;
}
}

Всем спасибо за внимание, надеюсь урок был вам полезен.

Предложения, замечания и т.п. пишите в личку.

  • Like 4
Link to comment
Share on other sites

Guest
This topic is now closed to further replies.
 Share

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue. See more about our Guidelines and Privacy Policy