Данная статья может быть интересной для начинающих web-разработчиков, или же обычных программистов, которые вёрстку учили только на первом курсе, и сейчас не до конца понимают как с html вообще можно играть.
Предисловие
В последнее время я начал замечать всё больше браузерных игр, которые написаны на связке html+ccs+js. Именно в такой связке, без флеша и других технологий. Не знаю, может их и раньше было много, но замечать всё больше с каждым днём я начал только сейчас. Одна 2048 сколько шуму наделала!
Вообщем, я был вдохновлён, и решил создать что-то простенькое, за пару вечеров, зато своё. За идею взял старые хорошие «пятнашки».
Единственным условием было написание всего исключительно на javascript (используя, правда, JQuery), не используя canvas, WebGL, и другие причуды технологии.
Концепт
Изначально даная затея задумывалась как чисто локальная вещь, без залива куда-либо, но чуть позже я заметил некий интерес к данной разработке в офисе, и решил что надо выкладывать. Первая идея, конечно же — обычный сайт. И вконце-концов это было сделано, и на этом бы всё и закончилось, если бы через пару дней коллега не спросил у меня: «ты писал когда-нибудь расширения для хрома?». Но обо всём попорядку.
Реализация самой игры
Внешний вид
Первоначальной идеей была реализация на html-таблице 4х4. Тут и готовые ячейки, и всё такое. На первый взгляд. При более детальной обдумке оказалось что плюсов у данной задумки никаких нет, и от идеи пришлось отказаться. В результате, я пришёл к одному блоку, внутри которого ещё 15, и у всех выставлено свойство
float:left;
Получилось так:
.game_field{
position: absolute;
left: 50%;
top: 50%;
margin-top: -128px;
margin-left: -128px;
width: 256px;
height: 256px;
border-radius: 4px;
}
.block{
-ms-user-select:none;
-moz-user-select:none;
-khtml-user-select:none;
-webkit-user-select:none;
-user-select:none;
float: left;
width: 60px;
height: 60px;
margin: 2px 2px 2px 2px;
background-color: #F3EDD6;
border-radius: 4px;
text-align: center;
font-size: 250%;
font-weight: bold;
cursor: pointer;
}
Логика
Движение
Самое сложное для меня это была логика перемещения блоков. Возможно, для опытного разработчика всё прояснилось бы мгновенно, но у меня на это ушёл целый вечер. Первой идеей была реализация столкновений: если пусто, то блок продвинется, а если нет — то останется на месте. Уже спустя некоторое время, и десятки форумов, я понял что двигаюсь в каком-то не том направлении. В результате я пришёл к такой реализации: массив на 16 ячеек с булевым значением. 0 — занято, 1 — пусто. Ну а далее всё понятно — даём блокам id с текущей позицией, сравниваем с реальным номером, для движения влево отнимаем 1, вправо — добавляем 1, вверх — минус 4. Если ячейка массива с номером в виде полученой позиции — true, значит двигаем, заменяем текущую на true, а новую на false.
Начальное расположение
Понятное дело, что начальное положение всех блоков должно быть неправильным, чтобы было, собственно говоря, во что играть. В теории: ставим все блоки на 15 позиций, потом рандомно присваиваем им номера, радуемся жизни. На практике: получаем критичный баг. Дело в том, что как оказалось, половина случайных расположений в пятнашках — принципиально непроходимые. Тоесть в результате в половине случаев мы имеем игру в которую выиграть просто невозможно. Я уверен что большинство это и так знает, но лично я — не знал. Решение пришло ввиде совета от второго коллеги: «расставляй правильно, а потом рандомно перемешивай, итераций на 400». Именно так и было реализовано начальное расположение в конце-концов.
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_version": 2,
"name": "Fifteen puzzle",
"version": "1.0",
"description": "A famous fifteen puzzle now in you browser!",
"icons": {
"32": "32x32.png",
"48": "48x48.png",
"64": "64x64.png",
"128": "128x128.png"
},
"browser_action": {
"default_title": "Game of 15",
"default_icon": "48x48.png",
"default_popup": "popup.html"
}
}
Так как кода сравнительно немного, то смысла заливать его на гитхаб я не вижу, и выкладываю прямо здесь:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="css/style.css">
<meta name="description" content="game v 0.001 alpha">
<meta name="author" content="vlreshet">
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/timer.jquery.minified.js"></script>
<script type="text/javascript" src="js/game.min.js"></script>
</head>
<body style="width:386px; height:266px; background-color:#CFF2DE;">
<div class="game_field_backdiv"></div>
<div class="game_field" border="10" onselectstart="return false">
<div class="block" id="1">1 </div>
<div class="block" id="2">2 </div>
<div class="block" id="3">3 </div>
<div class="block" id="4">4 </div>
<div class="block" id="5">5 </div>
<div class="block" id="6">6 </div>
<div class="block" id="7">7 </div>
<div class="block" id="8">8 </div>
<div class="block" id="9">9 </div>
<div class="block" id="10">10</div>
<div class="block" id="11">11</div>
<div class="block" id="12">12</div>
<div class="block" id="13">13</div>
<div class="block" id="14">14</div>
<div class="block" id="15">15</div>
</div>
<div class = "start_button_field">
<div class="button start" id="button">START</div>
<div class="win" style="display:none;">YOU WIN!</div>
<div class='timer_place timer' id="win_time"></div>
</div>
<div class="driver_button_background"></div>
<div class = "driver_button_field">
<div class="button" id="pause">PAUSE</div>
<div class="button" id="reset">RESET</div>
<div class="timer" id="timer"></div>
</div>
</body>
</html>
.html{
-ms-user-select:none;
-moz-user-select:none;
-khtml-user-select:none;
-webkit-user-select:none;
-user-select:none;
}
.game_field{
position: absolute;
left: 50%;
top: 50%;
margin-top: -128px;
margin-left: -128px;
width: 256px;
height: 256px;
border-radius: 4px;
}
.game_field_backdiv{
position: absolute;
left: 50%;
top: 50%;
margin-top: -133px;
margin-left: -133px;
width: 266px;
height: 266px;
border-radius: 10px;
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: 60px;
height: 60px;
margin: 2px 2px 2px 2px;
background-color: #F3EDD6;
border-radius: 4px;
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: -132px;
background-color: #E4DDE4;
opacity: .9;
position: absolute;
left: 50%;
margin-left: -132px;
width: 264px;
height: 264px;
border-radius: 4px;
}
.button {
-ms-user-select:none;
-moz-user-select:none;
-khtml-user-select:none;
-webkit-user-select:none;
-user-select:none;
-moz-box-shadow:inset 0px 0px 9px 0px #c1ed9c;
-webkit-box-shadow:inset 0px 0px 9px 0px #c1ed9c;
box-shadow:inset 0px 0px 9px 0px #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: 8px;
border-bottom-left-radius:8px;
text-indent:0px;
display:inline-block;
color:#ffffff;
font-family:Arial;
font-size:20px;
font-weight:bold;
font-style:normal;
height:35px;
line-height:35px;
width:96px;
text-decoration:none;
text-align:center;
text-shadow:1px 1px 0px #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: -48px;
top:40%;
margin-top: 9px;
}
#win_time{
display: none;
}
.timer_place{
color:#19FE0B;
font-family:Arial;
font-size:20px;
font-weight:bold;
font-style:normal;
height:35px;
line-height:35px;
width:96px;
position: relative;
text-decoration:none;
text-align:center;
left: 50%;
top:40%;
margin-left: -48px;
margin-top: -10px;
}
.win{
display: none;
color:#19FE0B;
font-family:Arial;
font-size:20px;
font-weight:bold;
font-style:normal;
height:35px;
line-height:35px;
width:96px;
position: relative;
text-decoration:none;
text-align:center;
left: 50%;
top:40%;
margin-left: -48px;
margin-top: -25px;
}
.driver_button_field{
display: none;
position: absolute;
border-radius: 4px;
width: 105px;
height: 110px;
border-radius: 8px;
left: 60%;
top: 50%;
margin-top: -133px;
}
.driver_button_background{
display: none;
position: absolute;
border-radius: 4px;
background-color: #20C0D9;
opacity: 0.7;
width: 115px;
height: 130px;
border-radius: 8px;
left: 60%;
top: 50%;
margin-top: -133px;
}
#pause{
margin-left: 10px;
margin-top: 5px;
}
#reset{
margin-left: 10px;
margin-top: 5px;
}
#timer{
box-shadow: 0px 0px 14px 0px #5D6FE5 inset;
-ms-user-select:none;
-moz-user-select:none;
-khtml-user-select:none;
-webkit-user-select:none;
-user-select:none;
border: 1px #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: 8px;
text-indent:0px;
display:inline-block;
color:#ffffff;
font-family:Arial;
font-size:20px;
font-weight:bold;
font-style:normal;
height:35px;
line-height:35px;
width:96px;
text-decoration:none;
text-align:center;
text-shadow:1px 1px 0px #689324;
margin-left: 10px;
margin-top: 5px;
}
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('<div class="block" id="1">1 </div><div class="block" id="2">2 </div><div class="block" id="3">3 </div><div class="block" id="4">4 </div><div class="block" id="5">5 </div><div class="block" id="6">6 </div><div class="block" id="7">7 </div><div class="block" id="8">8 </div><div class="block" id="9">9 </div><div class="block" id="10">10</div><div class="block" id="11">11</div><div class="block" id="12">12</div><div class="block" id="13">13</div><div class="block" id="14">14</div><div class="block" id="15">15</div>');
for(var i = 1; i <= 15; i++){
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 сейчас на модерации
Автор: vlreshet