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

от автора

Данная статья может быть интересной для начинающих 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 сейчас на модерации

ссылка на оригинал статьи http://habrahabr.ru/post/227547/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *