[Из песочницы] «Пятнашки» в виде расширения, или игростроительство на js
Данная статья может быть интересной для начинающих web-разработчиков, или же обычных программистов, которые вёрстку учили только на первом курсе, и сейчас не до конца понимают как с html вообще можно играть.
ПредисловиеВ последнее время я начал замечать всё больше браузерных игр, которые написаны на связке html+ccs+js. Именно в такой связке, без флеша и других технологий. Не знаю, может их и раньше было много, но замечать всё больше с каждым днём я начал только сейчас. Одна 2048 сколько шуму наделала! Вообщем, я был вдохновлён, и решил создать что-то простенькое, за пару вечеров, зато своё. За идею взял старые хорошие «пятнашки».Единственным условием было написание всего исключительно на javascript (используя, правда, JQuery), не используя canvas, WebGL, и другие причуды технологии.Концепт Изначально даная затея задумывалась как чисто локальная вещь, без залива куда-либо, но чуть позже я заметил некий интерес к данной разработке в офисе, и решил что надо выкладывать. Первая идея, конечно же — обычный сайт. И вконце-концов это было сделано, и на этом бы всё и закончилось, если бы через пару дней коллега не спросил у меня: «ты писал когда-нибудь расширения для хрома?». Но обо всём попорядку.Реализация самой игры Внешний вид Первоначальной идеей была реализация на html-таблице 4×4. Тут и готовые ячейки, и всё такое. На первый взгляд. При более детальной обдумке оказалось что плюсов у данной задумки никаких нет, и от идеи пришлось отказаться. В результате, я пришёл к одному блоку, внутри которого ещё 15, и у всех выставлено свойство float: left; Получилось так: Игровое поле, и блоки (сss) .game_field{ position: absolute; left: 50%; top: 50%; margin-top: -128 px; margin-left: -128 px; width: 256 px; height: 256 px; border-radius: 4 px; } .block{ -ms-user-select: none; -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; -user-select: none; float: left; width: 60 px; height: 60 px; margin: 2 px 2 px 2 px 2 px; background-color: #F3EDD6; border-radius: 4 px; text-align: center; font-size: 250%; font-weight: bold; cursor: pointer; } Логика Движение Самое сложное для меня это была логика перемещения блоков. Возможно, для опытного разработчика всё прояснилось бы мгновенно, но у меня на это ушёл целый вечер. Первой идеей была реализация столкновений: если пусто, то блок продвинется, а если нет — то останется на месте. Уже спустя некоторое время, и десятки форумов, я понял что двигаюсь в каком-то не том направлении. В результате я пришёл к такой реализации: массив на 16 ячеек с булевым значением. 0 — занято, 1 — пусто. Ну, а далее всё понятно — даём блокам id с текущей позицией, сравниваем с реальным номером, для движения влево отнимаем 1, вправо — добавляем 1, вверх — минус 4. Если ячейка массива с номером в виде полученой позиции — true, значит двигаем, заменяем текущую на true, а новую на false.Начальное расположение Понятное дело, что начальное положение всех блоков должно быть неправильным, чтобы было, собственно говоря, во что играть. В теории: ставим все блоки на 15 позиций, потом рандомно присваиваем им номера, радуемся жизни. На практике: получаем критичный баг. Дело в том, что как оказалось, половина случайных расположений в пятнашках — принципиально непроходимые. Тоесть в результате в половине случаев мы имеем игру в которую выиграть просто невозможно. Я уверен что большинство это и так знает, но лично я — не знал. Решение пришло ввиде совета от второго коллеги: «расставляй правильно, а потом рандомно перемешивай, итераций на 400». Именно так и было реализовано начальное расположение в конце-концов.Код перемешивания (js) mix: function (){
core.check_win = false;
for (var i = 0; i < 600; i++){
var num = Math.floor (Math.random () * (4 — 1 + 1)) + 1; var free_pos = 0;
for (var j = 1; j<=16;j++){ if(core.table_of_emptify[j] == true){ free_pos = j; break; } }
switch (num) { case 1: $('#'+(free_pos-4)).trigger ('click'); break; case 2: $('#'+(free_pos+4)).trigger ('click'); break; case 3: $('#'+(free_pos-1)).trigger ('click'); break; case 4: $('#'+(free_pos+1)).trigger ('click'); break; default: break; }
}
core.check_win = true;
} Расширения для Chrome После изучения нескольких статей на хабре, оказалось что для создания несложного расширения требуется всего 2 вещи — создание файла manifest.json, и… 5$. Я до сих пор не понимаю до конца логики, но для размещения своего расширения на web магазине хрома, надо единоразово уплатить 5$.Код manifest.json { «manifest_version»: 2, «name»: «Fifteen puzzle», «version»:»1.0», «description»: «A famous fifteen puzzle now in you browser!», «icons»: { »32»:»32×32.png»,»48»:»48×48.png»,»64»:»64×64.png»,»128»:»128×128.png» }, «browser_action»: { «default_title»: «Game of 15», «default_icon»:»48×48.png», «default_popup»: «popup.html» } }
Так как кода сравнительно немного, то смысла заливать его на гитхаб я не вижу, и выкладываю прямо здесь:
HTML файл
.game_field{ position: absolute; left: 50%; top: 50%; margin-top: -128 px; margin-left: -128 px; width: 256 px; height: 256 px; border-radius: 4 px; }
.game_field_backdiv{ position: absolute; left: 50%; top: 50%; margin-top: -133 px; margin-left: -133 px; width: 266 px; height: 266 px; border-radius: 10 px; background-color: #20C0D9; opacity: 0.7; }
.block{ -ms-user-select: none; -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; -user-select: none; float: left; width: 60 px; height: 60 px; margin: 2 px 2 px 2 px 2 px; background-color: #F3EDD6; border-radius: 4 px; text-align: center; font-size: 250%; font-weight: bold; cursor: pointer; }
table{ text-align: center; }
.false{ background-color: #F7A603; }
.true{ background-image: -webkit-gradient ( linear, left top, left bottom, color-stop (0, #39F046), color-stop (1, #76ED7E), color-stop (1, #9AFFA4) ); background-image: -o-linear-gradient (bottom, #39F046 0%, #76ED7E 100%, #9AFFA4 100%); background-image: -moz-linear-gradient (bottom, #39F046 0%, #76ED7E 100%, #9AFFA4 100%); background-image: -webkit-linear-gradient (bottom, #39F046 0%, #76ED7E 100%, #9AFFA4 100%); background-image: -ms-linear-gradient (bottom, #39F046 0%, #76ED7E 100%, #9AFFA4 100%); background-image: linear-gradient (to bottom, #39F046 0%, #76ED7E 100%, #9AFFA4 100%); }
.start_button_field{ top: 50%; margin-top: -132 px; background-color: #E4DDE4; opacity: .9; position: absolute; left: 50%; margin-left: -132 px; width: 264 px; height: 264 px; border-radius: 4 px; }
.button { -ms-user-select: none; -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; -user-select: none; -moz-box-shadow: inset 0 px 0 px 9 px 0 px #c1ed9c; -webkit-box-shadow: inset 0 px 0 px 9 px 0 px #c1ed9c; box-shadow: inset 0 px 0 px 9 px 0 px #c1ed9c; background:-webkit-gradient (linear, left top, left bottom, color-stop (0.05, #9dce2c), color-stop (1, #8cb82b)); background:-moz-linear-gradient (center top, #9dce2c 5%, #8cb82b 100%); filter: progid: DXImageTransform.Microsoft.gradient (startColorstr='#9dce2c', endColorstr='#8cb82b'); background-color:#9dce2c; border-radius: 8 px; border-bottom-left-radius:8 px; text-indent:0 px; display: inline-block; color:#ffffff; font-family: Arial; font-size:20 px; font-weight: bold; font-style: normal; height:35 px; line-height:35 px; width:96 px; text-decoration: none; text-align: center; text-shadow:1 px 1 px 0 px #689324; }
.button: hover { background:-webkit-gradient (linear, left top, left bottom, color-stop (0.05, #8cb82b), color-stop (1, #9dce2c)); background:-moz-linear-gradient (center top, #8cb82b 5%, #9dce2c 100%); filter: progid: DXImageTransform.Microsoft.gradient (startColorstr='#8cb82b', endColorstr='#9dce2c'); background-color:#8cb82b; cursor: pointer; }
.start{ position: relative; left: 50%; margin-left: -48 px; top:40%; margin-top: 9 px; }
#win_time{ display: none; }
.timer_place{ color:#19FE0B; font-family: Arial; font-size:20 px; font-weight: bold; font-style: normal; height:35 px; line-height:35 px; width:96 px; position: relative; text-decoration: none; text-align: center; left: 50%; top:40%; margin-left: -48 px; margin-top: -10 px; }
.win{ display: none; color:#19FE0B; font-family: Arial; font-size:20 px; font-weight: bold; font-style: normal; height:35 px; line-height:35 px; width:96 px; position: relative; text-decoration: none; text-align: center; left: 50%; top:40%; margin-left: -48 px; margin-top: -25 px; }
.driver_button_field{ display: none; position: absolute; border-radius: 4 px; width: 105 px; height: 110 px; border-radius: 8 px; left: 60%; top: 50%; margin-top: -133 px; }
.driver_button_background{ display: none; position: absolute; border-radius: 4 px; background-color: #20C0D9; opacity: 0.7; width: 115 px; height: 130 px; border-radius: 8 px; left: 60%; top: 50%; margin-top: -133 px; }
#pause{ margin-left: 10 px; margin-top: 5 px; }
#reset{ margin-left: 10 px; margin-top: 5 px; }
#timer{ box-shadow: 0 px 0 px 14 px 0 px #5D6FE5 inset; -ms-user-select: none; -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; -user-select: none; border: 1 px #22C3DD solid; background:-webkit-gradient (linear, left top, left bottom, color-stop (0.05, #20C0D9), color-stop (1, #22C3DD)); background:-moz-linear-gradient (center top, #20C0D9 5%, #22C3DD 100%); filter: progid: DXImageTransform.Microsoft.gradient (startColorstr='#20C0D9', endColorstr='#22C3DD'); background-color:#20C0D9; border-radius: 8 px; text-indent:0 px; display: inline-block; color:#ffffff; font-family: Arial; font-size:20 px; font-weight: bold; font-style: normal; height:35 px; line-height:35 px; width:96 px; text-decoration: none; text-align: center; text-shadow:1 px 1 px 0 px #689324; margin-left: 10 px; margin-top: 5 px; } JavaScript var handlersSetter = {
setHandlers: function (){ $('#button').on ('click', function (){ $('.start_button_field').hide ('fast'); $('.driver_button_background').show ('fast'); $('.driver_button_field').show ('fast'); });
$('.start').on ('click', function (){ $('.timer').timer ('start'); $(this).addClass ('hidden'); $('.start_button').hide (); handlersSetter.paused = false; });
$('#reset').on ('click', function (){ core.set_default (); $('.timer').timer ('start'); $('.timer').timer ('reset'); $('.start_button_field').hide ('fast'); $('.start').show (); $('.win').hide (); $('#win_time').hide (); });
$('#pause').on ('click', function (){ if (! handlersSetter.paused){ $('.timer').timer ('pause'); handlersSetter.paused = true; $('.start_button_field').show (); } });
$(«body»).on («click»,».block», function (e){
var position = parseFloat (e.currentTarget['id']);
if (core.check_top (position)){ core.replace (position, -64, 0, -4); }
if (core.check_bottom (position)){ core.replace (position, 64, 0, 4); }
if (core.check_right (position)){ core.replace (position, 0, 64, 1); }
if (core.check_left (position)){ core.replace (position, 0, -64, -1); }
});
}
}
var core = {
check_win: false,
replace: function (position, func_top, func_left, func_position){
var obj = $('#'+position); var left = obj.offset ().left; var top = obj.offset ().top;
new_position = new Object (); new_position.top = top + func_top; new_position.left = left + func_left; core.table_of_emptify[position] = true; core.table_of_emptify[position+func_position] = false; obj.attr («id»,(position+func_position)); new_position.left = left + func_left; obj.offset (new_position); core.check_pos (position+func_position);
},
setEmptifyTable: function (func){ core.table_of_emptify = [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true]; func (); },
mix: function (){
core.check_win = false;
for (var i = 0; i < 600; i++){
var num = Math.floor (Math.random () * (4 — 1 + 1)) + 1; var free_pos = 0;
for (var j = 1; j<=16;j++){ if(core.table_of_emptify[j] == true){ free_pos = j; break; } }
switch (num) { case 1: $('#'+(free_pos-4)).trigger ('click'); break; case 2: $('#'+(free_pos+4)).trigger ('click'); break; case 3: $('#'+(free_pos-1)).trigger ('click'); break; case 4: $('#'+(free_pos+1)).trigger ('click'); break; default: break; }
}
core.check_win = true;
},
check_top: function (position){
target_position = parseFloat (position) — 4;
if (target_position > 0){ return (core.table_of_emptify[target_position]); }else{ return false; }
},
check_bottom: function (position){
target_position = parseFloat (position) + 4;
if (target_position <= 16){ return(core.table_of_emptify[target_position]); }else{ return false; }
},
check_left: function (position){
target_position = parseFloat (position) — 1;
if ((target_position!= 0)&&(target_position!= 4)&&(target_position!= 8)&&(target_position!= 12)){ return (core.table_of_emptify[target_position]); }else{ return false; }
},
check_right: function (position){
target_position = parseFloat (position) + 1;
if ((target_position!= 1)&&(target_position!= 5)&&(target_position!= 9)&&(target_position!= 13)){ return (core.table_of_emptify[target_position]); }else{ return false; }
},
check_pos: function (pos){
var obj = $('#'+pos); if (obj.html () == pos){ obj.attr ('class','block true'); }else{ obj.attr ('class','block false'); }
if (! core.check_win){ return; }
if ($('#15').html () == '15'){
var flag = true;
for (var i = 1; i <= 15; i++){
if ($('#'+i).html () != i){ flag = false; break; }
}
if (flag){
$('.start').hide (); $('.start_button_field').show (); $('.win').show (); $('#win_time').show (); $('.timer').timer ('pause');
}
}
},
set_default: function (){
$('.game_field').html ('
if ($('#'+i).html () == i){ $('#'+i).attr ('class','block true'); }else{ $('#'+i).attr ('class','block false'); }
} core.setEmptifyTable (core.mix);
},
init: function (){
handlersSetter.setHandlers (); core.setEmptifyTable (core.mix);
}
} Ссылка на расширениеСсылка на чесно заюзаный js-плагин для реализации таймераСсылка на первоначальный сайт
P.S. можно собрать данное расширение вручную, вставить в Opera 20+, и оно отлично будет работать (chromium же). Расширение в Opera Store сейчас на модерации