«П'ятнашки» у вигляді розширення, або ігростроітельство на 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 зараз на модерації

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.