Canvas: пятнадцать минут на пятнашки

  • Tutorial
CANVAS шаг за шагом:
  1. Основы
  2. Изображения
  3. Понг
  4. Пятнашки

В детстве у меня были пятнашки, я думаю все знают эту головоломку. Двигать пятнашки в пластиковой коробочке до получения заветного порядка цифр было очень интересным занятием. Вот и совсем недавно, в порядке спортивного интереса, я написал для себя пятнашки в которые бы можно было играть не только из окна браузера, но и с смартфона под управлением ОС Андроид или iOS.
Наша игра будет состоять из двух файлов лежащих в одной папочке, первый обзовём index.htm, а второй puzzle15.js
Содержимое html файла в течении всего поста меняться не будет, и собственно он должен выглядеть приблизительно так:
<html>
	<head>
		<meta charset="utf-8">
		<title>Пятнашки</title>
		<script src="puzzle15.js"></script>
	</head>
	<body>
		<canvas id="puzzle15">Пора уже в этой жизни что-то менять</canvas>
		<script>init();</script>
	</body>
</html>

Написанием второго файла мы и будем заняты. Итак изначально мы напишем функцию init, которая изменит размер нашего холста и закрасит его однородым цветом, т.е. содержимое файла puzzle15.js должно быть таким:
function init() {
	var canvas = document.getElementById("puzzle15");
	    canvas.width = 320; // задаём размеры холста
	    canvas.height = 320;
	var context = canvas.getContext("2d");
	    context.fillStyle = "#222"; // цвет "заливки"
	    context.fillRect(0, 0, canvas.width, canvas.height); // закрашиваем холст        
}

Если открыть браузером файл index.htm, то можно увидеть тёмно-серый прямоугольник. Для пятнашек я заготовил один класс в который вынесена вся игровая логика, этот класс собственно надо поместить в файл:
function game15() {
	var cellView = null;
	var numView = null;
	var arr = [[1,2,3,4], [5,6,7,8], [9,10,11,12], [13,14,15,0]];
	var clicks = 0;
	function getNull() { // функция возвращает координату пустой клетки
		for (var i = 0; i < 4; i++) {
			for (var j = 0; j < 4; j++) {
				if (arr[j][i] === 0) {
					return{"x":i,"y":j};
				}
			}
		}
	};
	// функция возвращает произвольное логическое значение
	function getRandomBool() {
		if (Math.floor(Math.random() * 2) === 0) {
			return true;
		}
	}
	// метод возвращает число касаний
	this.getClicks = function() {
		return clicks;
	};
	// метод перемещает "пятнашку" в пустую клутку 
	this.move = function(x, y) {
		var nullX = getNull().x;
		var nullY = getNull().y;
		if (((x - 1 == nullX || x + 1 == nullX) && y == nullY) || ((y - 1 == nullY || y + 1 == nullY) && x == nullX)) {
			arr[nullY][nullX] = arr[y][x];
			arr[y][x] = 0;
			clicks++;
		}
	};
	// проверка условия победы
	this.victory = function() {
		var e = [[1,2,3,4], [5,6,7,8], [9,10,11,12], [13,14,15,0]];
		var res = true;
		for (var i = 0; i < 4; i++) {
			for (var j = 0; j < 4; j++) {
				if (e[i][j] != arr[i][j]) {
					res = false;
				}
			}
		}
		return res;
	};
	// метод "перемешивает" пятнашки
	this.mix = function(stepCount) {
		var x,y;
		for (var i = 0; i < stepCount; i++) {
			var nullX = getNull().x;
			var nullY = getNull().y;
			var hMove = getRandomBool();
			var upLeft = getRandomBool();
			if (!hMove && !upLeft) { y = nullY; x = nullX - 1;}
			if (hMove && !upLeft)  { x = nullX; y = nullY + 1;}
			if (!hMove && upLeft)  { y = nullY; x = nullX + 1;}
			if (hMove && upLeft)   { x = nullX; y = nullY - 1;}
			if (0 <= x && x <= 3 && 0 <= y && y <= 3) {
				this.move(x, y);
			}
		}
		clicks = 0;
	};
	// внешний вид пятнашки
	this.setCellView = function(func) {
		cellView = func;
	};
	// параметры шрифта цифр
	this.setNumView = function(func) {
		numView = func;
	};
	// Метод рисующий наши пятнашки на холсте
	this.draw = function(context, size) {
		for (var i = 0; i < 4; i++) {
			for (var j = 0; j < 4; j++) {
				if (arr[i][j] > 0) {
					if (cellView !== null) {
						cellView(j * size, i * size);
					}
					if (numView !== null) {
						numView();
						context.fillText(arr[i][j], j * size + size / 2, i * size + size / 2);
					}
				}
			}
		}
	};
}

Теперь нашу игру можно смело перевоплотить из статичного прямоугольника, в практически рабочие пятнашки
function init() {
	var canvas = document.getElementById("puzzle15");
	    canvas.width  = 320;
	    canvas.height = 320;
	var cellSize = canvas.width / 4;
	var context = canvas.getContext("2d");
	var field = new game15(); // создаём объект пятнашек
	    field.mix(350); // тщательно перемешиваем содердимое коробки
	    field.setCellView(function(x, y) { // задаём внешний вид пятнашек
	    	context.fillStyle = "#FFB93B";
	    	context.fillRect(x+1, y+1, cellSize-2, cellSize-2);
	    });
	    field.setNumView(function() { // параметры шрифта для цифр
	    	context.font = "bold "+(cellSize/2)+"px Sans";
	    	context.textAlign = "center";
	    	context.textBaseline = "middle";
	    	context.fillStyle = "#222";
	    });
	context.fillStyle = "#222";
	context.fillRect(0, 0, canvas.width, canvas.height);
	field.draw(context, cellSize);
}

Ну вот осталось добавить лишь обработку событий мыши и касаний, и у нас получиться вполне рабочая версия пятнашек, для этого в функцию init после последней строки field.draw(context, cellSize) добавим немного кода:
	function event(x, y) { // функция производит необходимые действие при клике(касанию)
		field.move(x, y);
		context.fillStyle = "#222";
		context.fillRect(0, 0, canvas.width, canvas.height);
		field.draw(context, cellSize);
		if (field.victory()) { // если головоломка сложена, то пятнашки заново перемешиваются
			alert("Собрано за "+field.getClicks()+" касание!"); // вывод сообщения о выигрыше!!
			field.mix(300);
			context.fillStyle = "#222";
			context.fillRect(0, 0, canvas.width, canvas.height);
			field.draw(context, cellSize);
		}
	}
	canvas.onclick = function(e) { // обрабатываем клики мышью
		var x = (e.pageX - canvas.offsetLeft) / cellSize | 0;
		var y = (e.pageY - canvas.offsetTop)  / cellSize | 0;
		event(x, y); // выхов функции действия
	};
	canvas.ontouchend = function(e) { // обрабатываем касания пальцем
		var x = (e.touches[0].pageX - canvas.offsetLeft) / cellSize | 0;
		var y = (e.touches[0].pageY - canvas.offsetTop)  / cellSize | 0;
		event(x, y);
	};

Кто хочет сразу поиграть для того ссылка.
Ничего сложного в коде нет, и всё пишется очень быстро. Спасибо пользователю hobbeat за идею и фотографию пятнашек на айПоде.
Support the author
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 51

    0
    А зачем вы используйте

    var context = canvas.getContext(«2d»);
    в двух функциях anim и init?
      0
      Правда ваша. В anim контекст излишен) Исправил
        0
        Если следовать посту, то в anim контекст необходим
          0
          Вообще убил в тексте anim. Когда торопишься на некоторые вещи иногда не обращаешь внимания, и как показала практика совершенно напрасно )
      +1
      Зачем перерисовывать поле каждые 20 (в вашем случае) мс? Не лучше ли было бы перерисовывать при непосредственно перемещении плитки? Зачем функции, которая передается в setDraw, параметры-координаты? Или я неверно понял логику?
        0
        Поле вообще не надо перерисовывать) Надо зарисовать старую пятнашку и нарисовать новую ;)
          0
          Это-то в идеале) Код вообще, что называется, оставил ряд вопросов после себя.
            0
            Ряд вопросов? )) Задавайте. Пытался писать как можно проще. Поле неоправданно перерисовываю каждый раз признаю, но это скорей издержка того что задумывались пятнашки более продвинутыми и в них перерисовка была просто необходима. Но в итоге было решено всё упростить и оставить так как описано в посте.
        0
        Спасибо! Вспомнил детство :)
        0
        Собрал махом.
        А с пятнашками из комплекта Portal 2 несколько дней мучаюсь ;)
          +2
          А я их успел собрать, пока игра устанавливалась :)
          +10
          Спасибо, вспомнил цитату с башорга:
          Удивительное это дело — прогресс. Чтобы набрать и распечатать одну
          страничку красиво оформленного текста мне уже не хватает мощности
          компьютера, который с легкостью может управлять двумя тысячами советских
          боевых спутников одновременно.
          Я к тому, что страница загружает одно ядро на 100%. И это всего лишь пятнашки.
            0
            Ну, код для обучения сгодиться. То же самое при помощи LibCanvas.Engines.Tiles можно сделать на Canvas с меньшим кодом и с близкой к нулевой загрузкой проца)
            +6
            А зачем вообще канву использовать? Даёшь пятнашки на дивах!
              0
              А добавьте таблицу рекордов вида:
              md5(начальная позиция), время*клики ник.
                0
                Поздно уже добавлять, но учту ваше пожелание на будущее.
                Кстати в блоге к Пятнашкам комменты все в рекордах.
                +1
                Не хватает возможности перетаскивать несколько пятнашек на одной линии, приходится порой делать целых три клика вместо одного :)
                  0
                  Я изначально так и делал. Но без анимации как-то это убого выглядело и потому убрал. Зря наверное, но уже сделано )
                  0
                  Какие NP-полные задачи решают эти пятнашки? Они грузят двух-ядерный процессор на 50%.
                    +1
                    Я думаю неважно на сколько грузится процессор, так как идея у автора была совсем другая.
                    Первое: вспомнить детство и реализовать старую добрую игру на компьютере.
                    Второе: проверить свои силы в JavaScript + Canvas, может узнать чего-то новое.
                      +1
                      Видимо, также, третье: рассказать основы рисования на Canvas)
                        0
                        Чистая правда. И первое, и второе, и третье ))
                    0
                    На фотке с айФоном лампу надо было ставить под сильным углом, тогда тени стали бы такими, что цифры можно было бы прочитать)
                      0
                      А вообще произведение не моё. Но идея мне нравится )) тов.hobbeat'у респект за фото
                      +2
                      Делать было нечего, сделал простелькие пятнашки на дивах :)
                        0
                        Ох, завидую я вам, вам делать было нечего…
                          +5
                          Вижу, ищете стиль и стараетесь делать по уму. Впринципе, неплохо, но кое-где перепроектировали, а кое-где — недопроектировали ;)

                          regisrty => registry.

                          var div = $("<div dir='{blockId:"+index+"}'/>");
                          // Лучше через второе свойство добавлять аттрибуты
                          var div = $("<div />", { dir:  dir='{blockId:'+index+'}'});
                          


                          $.each(regisrty.getOption('block').css, function(property, value) {
                              div.css(property, value);
                          }.bind(this));
                          
                          //  =>
                          div.css(regisrty.getOption('block').css);
                          


                          div.css('float', 'left');
                          div.css('border', '1px solid #000');
                          div.css('width', regisrty.blockSize - 2);
                          div.css('height', regisrty.blockSize - 2);
                          
                          // =>
                          div.css({
                              float : 'left',
                              border: '1px solid #000',
                              width : registry.blockSize - 2,
                              height: registry.blockSize - 2
                          })
                          


                          Хотя, такие вещи лучше вынести в CSS.

                          if (0 == index) {
                          // =>
                          if (!index)
                          


                          $(div).click(function() {
                              this.processEvent(div);
                          }.bind(this));
                          // =>
                          $(div).click(this.processEvent.bind(this, div));
                          


                          this.processEvent так и проситься рефакторинга. Проглядывается копипаст шаблон:
                          if (typeof stack[column][value] != 'undefined' && stack[column][value] == 0) {
                              stack[column][value] = stack[column][row];
                              stack[row] = 0;
                          }
                          


                          Вместо typeof stack[column][row+1] != 'undefined' лучше пишите stack[column][row+1] != null. В вашем случае суть та же, а захламление кода меньше. Тем более реальная проверка на 'undefined' практически никогда не нужна.

                          this.init = function() {
                              $.each(this.options.css, function(property, value) {
                                  $('#' + this.options.id).css(property, value);
                              }.bind(this));
                              
                              this.paint();
                          }
                          // => 
                          this.init = function() {
                              $('#' + this.options.id).css(this.options.css);
                              this.paint();
                          }
                          


                          Идея с флагом — не очень, проверяете одно и то же:
                          var finish = false;
                          while (!finish) {
                              r = Math.round(Math.random() * 15);
                              
                              if ( !localStack[r] && 0 != localStack[r] ) {
                                  localStack[r] = r;
                                  finish = true;
                              }
                          }
                          // =>
                          while (true) {
                              r = Math.round(Math.random() * 15);
                              
                              if ( !localStack[r] ) {
                                  localStack[r] = r;
                                  break;
                              }
                          }
                          


                          getDiv() — плохое название. А если пятнашка станет spanом? getCell/getItem/getElement, что-то типа такого.

                            +1
                            Ого, спасибо:)
                            Сначала старался сделать правильно, потом уже просто лишь бы работало.
                              +3
                              Обращайтесь) Рад помочь всем, кто серьёзно изучает JavaScript)
                                +2
                                ps. Можно в джаббер
                                0
                                Мне понравилось) Прикольно вышло
                              0
                              И да, bind — штука прикольная, но лучше добавить обратную совместимость, ибо даже в третьем фоксе пятнашки уже не работают.
                                0
                                Кстати, такой вопрос: есть ли библиотека/модуль с подобной поддержкой обратной совместимости для всех или большинства нововведений? Может, даже поделитесь своей :)
                                  0
                                  AtomJS ;) В частности, везде одинаково работают аксессоры, есть JavaScript 1.8.5 Compatibility и еще кое-что)
                                0
                                Ваша версия не решается в 50% случаев — habrahabr.ru/blogs/html5/118356/#comment_3859537

                                Мне вот сходу достался нерешаемый вариант (
                                  0
                                  Неправильно генерится поле… не всегда можно собрать.
                                  +1
                                  Тоже было нечего делать, а с canvas никогда не работал — решил попробовать игру сделать какую-нибудь…
                                  В итоге решил сделать «Swell Foop» — так она в убунте называлась, вроде бы. Посидел вечерок, сделал.
                                  Код написан неоптимально, допиливать лень, да и нет времени. Проект на git: https://github.com/Shut/swellfoo.
                                  Хостинга нет, так что выложить посмотреть негде, к сожалению.
                                    +1
                                    Выложите на Github pages. Если не требуется серверная часть, то довольно удобно и относительо вечно
                                      0
                                      Спасибо, не знал (первый проект на git, никогда не пользовался до этого, позор мне :) ).
                                      Доступно теперь по http://shut.github.com/swellfoop/.
                                      Таблицы рейтингов отключить пришлось.
                                      Да и тестровал только в FF, Chrome и Opera — IE в топку.
                                        +2
                                        Очень классно. Порадовала высокая производительность и внешний вид.
                                        Код грязноват.

                                        Т.к. в IE не работает и врядли заработает могу посоветовать убрать функцию addEvent — у всех целевых браузеров есть addEventListener. Аналогично с XHR и ActiveXObject.

                                        Одна из серьезный проблем — у вас используются то табы, то пробелы. Рекомендую выбрать что-то одно.

                                        Вы, практически, переписали половину AtomJS. Если не знаете, это миниатюрная js-либа, смесь JQuery и MooTools, который не тянет совместимость со старыми браузерами. У вас написано пол этой либы)))

                                        function randomNumber(m, n) {
                                          return Math.floor( Math.random() * (n - m + 1) ) + m;
                                        }
                                        randomNumber(min, max)
                                        // =>
                                        Number.random(min, max)
                                        

                                        document.getElementById('newGame')
                                        // =>
                                        atom.dom("#newGame")
                                        

                                        Game.prototype._getNickname = function() {
                                        	var cookie = ' ' + document.cookie;
                                        	var search = ' nickname=';
                                        	var nickname = null;
                                        	var offset = 0;
                                        	var end = 0;
                                        	if (cookie.length > 0) {
                                        		offset = cookie.indexOf(search);
                                        		if (offset != -1) {
                                        			offset += search.length;
                                        			end = cookie.indexOf(";", offset)
                                        			if (end == -1) {
                                        				end = cookie.length;
                                        			}
                                        			nickname = cookie.substring(offset, end);
                                        		}
                                        	}
                                        	return (nickname);
                                        }
                                        // => 
                                        Game.prototype._getNickname = function() {
                                            return atom.cookie.get('nickname');
                                        }
                                        
                                        


                                        А также atom.ajax, atom.Class, atom.Class.Options.

                                        Ну и, как я говорил выше, такая штука очень круто делается на LibCanvas.Engines.Tiles
                                          0
                                          1) Да, насчет кода знаю — можно выкинуть/переписать, но руки никак не дохоодят… Хотел даже топик написать, но куда там, если на гитхаб уже месяц не мог выложить — а все лень.
                                          2)Некоторые части (запись в куки, навешивание событий) — просто скопипастены из других проектов, а там поддерживается ИЕ (т.к. тот же jQuery не используется на работе, куча уже ставших классическими функций, вроде навешивания обработчика событий, ajax).
                                          3) Про atom.js читал (вроде Ваше творение, да?), но вот использовать не приходилось, к сожалению… Будет время — поразбираюсь, прикручу :)
                                          4) Да, про табы в курсе. Просто начал делать на работе (а там для отступов принято использовать пробелы), дома же у меня табы (да и везде в своих проектах их использую). А ширина отступа везде 4 пробела, то не заметил даже.
                                            0
                                            3. Да, моё и развитие не останавливается)
                                          –1
                                          я набрал 3220 очков: о)
                                        0
                                        Разобрал пример с пятнашек и решил повторить ваш подвиг :)
                                        У меня немного меньше кода вышло stfalcon.github.com/swell-foop/, но не реализован подсчет очков и подсветка секторов.
                                      • UFO just landed and posted this here
                                          0
                                          Ошиблись дважды. Не Smart Cover и не iPhone! Пятнашки на iPod'е ))
                                          0
                                          к сожалению, в Вашей игре(прототипе) есть маленький недостаток, делающий игру неиграбельной — отслеживается событие onmouseclick (а не omousedown). Поэтому при отпускании кнопки мыши (если она передвинулась хоть на пиксель) — события не происходит…

                                          Only users with full accounts can post comments. Log in, please.