«Пятнашки» в виде расширения, или игростроительство на js

в 20:10, , рубрики: chrome, chrome extensions, css, html, javascript, jquery, webkit, пятнашки, метки: , , , , ,

Данная статья может быть интересной для начинающих web-разработчиков, или же обычных программистов, которые вёрстку учили только на первом курсе, и сейчас не до конца понимают как с html вообще можно играть.

image

Предисловие

В последнее время я начал замечать всё больше браузерных игр, которые написаны на связке html+ccs+js. Именно в такой связке, без флеша и других технологий. Не знаю, может их и раньше было много, но замечать всё больше с каждым днём я начал только сейчас. Одна 2048 сколько шуму наделала!
Вообщем, я был вдохновлён, и решил создать что-то простенькое, за пару вечеров, зато своё. За идею взял старые хорошие «пятнашки».
Единственным условием было написание всего исключительно на javascript (используя, правда, JQuery), не используя canvas, WebGL, и другие причуды технологии.

Концепт

image
Изначально даная затея задумывалась как чисто локальная вещь, без залива куда-либо, но чуть позже я заметил некий интерес к данной разработке в офисе, и решил что надо выкладывать. Первая идея, конечно же — обычный сайт. И вконце-концов это было сделано, и на этом бы всё и закончилось, если бы через пару дней коллега не спросил у меня: «ты писал когда-нибудь расширения для хрома?». Но обо всём попорядку.

Реализация самой игры

Внешний вид

Первоначальной идеей была реализация на html-таблице 4х4. Тут и готовые ячейки, и всё такое. На первый взгляд. При более детальной обдумке оказалось что плюсов у данной задумки никаких нет, и от идеи пришлось отказаться. В результате, я пришёл к одному блоку, внутри которого ещё 15, и у всех выставлено свойство

float:left;

Получилось так:
image

Игровое поле, и блоки (сss)

.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». Именно так и было реализовано начальное расположение в конце-концов.

Код перемешивания (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": "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"
}

}

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

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>

css

.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;
}

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('<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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js