Lines

HTML5 CANVAS: игра Lines

Lines

Lines

Lines (Color Lines, в народе Шарики) — логич. компьютерная игра, изобретённая в 1992 году.

В игре на экране показано поле, в случайные клетки на котором программа выставляет шарики разных цветов. За один ход игрок может передвинуть один шарик, выделив его и указав его новое местоположение. Для совершения хода необходимо, чтобы между начальной и конечной клетками существовал путь из свободных клеток. Цель игры состоит в удалении максимального количества шариков, которые исчезают при выстраивании шариков одного цвета. При исчезновении ряда шариков новые шарики не выставляются. В остальных случая каждый ход выставляются новые шарики.

Ради интереса решил сделать что-то подобное, используя элемент Canvas в HTML5.

Игра будет состоять из 2-х файлов: сама страница html и файл логики игры.

Содержимое html файла выглядит приблизительно так:

<html>
     <head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 		<script src="lines5.js"></script>
		<title>Lines</title>
	</head>
	
<body>
	<canvas id="Lines5">HTML5 нид, мэн</canvas>
        <script>init(5,6,3);</script>
</body>
</html>

В общем-то большая часть работы ведётся во втором файле. Он состоит из метода инициализации игры:

//инициализация
function init(wCount, hCount, rCount) 
{
	var cellSize = 60;
    var canvas = document.getElementById("Lines5");
	
        canvas.width = cellSize*wCount+150; 	// ширина
        canvas.height = cellSize*hCount;	// высота
		
    var context = canvas.getContext("2d");
	var field = new l5(); // создаём объект 
		field.setParams(wCount, hCount, rCount);

		
        context.fillStyle = "#969696"; // цвет "заливки"
        context.fillRect(0, 0, canvas.width, canvas.height); // закраска   
		field.draw(context, cellSize);
		
		
				
		// функция производит необходимые действие при клике	
		function event(x, y) { field.move(x, y); }
	
		// обрабатка кликов мыши
    	canvas.onclick = function(e) 
		{ 
        	var x = (e.pageX - canvas.offsetLeft) / cellSize | 0;
       	 	var y = (e.pageY - canvas.offsetTop)  / cellSize | 0;
        	event(y, x); 
    	};
   
}

Ну и, собственно, класс, отвечающий за логику игры:

function l5() 
{
	var loser = false;
	var rCount = 0;			//  количество рэндомно генерируемых шаров 
    var wCount = 0;			//  в ширину сколько
    var hCount = 0;  		//  в высоту сколько
    var mas = null;			//	поле
    //var clicks = 0;
	var context = null;		//	контекст
	var curHeight =  0;		//	текущая высота прыжка шарика
	var curAsc = true;		//	подпрыгивает или падает после прыжка
	var curI = -1;			//	строка выбранного
	var curJ = -1;			//	столбец выбранного
	var timer = null;
	var self = this;
	var points = 0;			//	очки
	
	var masRandom = Array();
	var randomCurrent = 0;
	
	//таймер для прыганья ^_^
	this.runMultiple = function ()
	{
		i = curI;
		j = curJ;

		if(curAsc) curHeight--;
		else curHeight++;
		
		if(curHeight==-5) curAsc = false;
		if(curHeight==0) curAsc = true;

		
		this.clearCell(i,j);
		this.circleView(i,j,curHeight, this.getColor(mas[i][j]));
	};
	
	
	this.setParams = function(weightC, heightC, rC)
	{
		rCount=rC;			//  количество рэндомно генерируемых шаров 
    	wCount = weightC;	//  в ширину сколько
    	hCount = heightC;  	//  в высоту 
		
	};
	
	
	this.clearTimer = function (i,j)
	{
		curHeight =  0;
		curAsc = true;
		curI = -1;
		curJ = -1;
		this.clearCell(i,j);
		this.circleView(i,j,0, this.getColor(mas[i][j]));
		clearInterval(timer);
	}
	
	
	//проверка на проходимость
	this.checkPassability = function (iCheck, jCheck)
	{
		var afterCount = 0;
		var beforeCount = 0;
		var curColor = mas[curI][curJ];
		mas[curI][curJ] = -1;
		while(true)
		{
			afterCount = 0;
			beforeCount = 0;
			for(var i = 0; i < hCount; ++i)
				for(var j = 0; j < wCount; ++j)
					if(mas[i][j] == -1) ++beforeCount;	
					
			for(var i = 0; i < hCount; ++i)
				for(var j = 0; j < wCount; ++j)
					if(mas[i][j] == -1)
					{	
						if((i-1 >= 0) &amp;amp;amp;&amp;amp;amp; (mas[i-1][j] == 0))	
							mas[i-1][j] = -1;
						if((i+1 < hCount) &amp;amp;amp;&amp;amp;amp; (mas[i+1][j] == 0))	
							mas[i+1][j] = -1;
						if((j-1 >= 0) &amp;amp;amp;&amp;amp;amp; (mas[i][j-1] == 0))	
							mas[i][j-1] = -1;
						if((j+1 < wCount) &amp;amp;amp;&amp;amp;amp; (mas[i][j+1] == 0))	
							mas[i][j+1] = -1;
					}
				
			for(var i = 0; i < hCount; ++i)
				for(var j = 0; j < wCount; ++j)
					if(mas[i][j] == -1) ++afterCount;	
			if(afterCount == beforeCount) break;
			
		}
		
		var cool = false;
		if(mas[iCheck][jCheck] == -1)	
			cool = true;
		
		mas[curI][curJ] = curColor;	
		for(var i = 0; i < hCount; ++i)
			for(var j = 0; j < wCount; ++j)
				if(mas[i][j] == -1) mas[i][j] = 0;
					
		return cool;
	}
	
	this.eraseCircle = function (i,j)
	{	
		mas[i][j] = 0;
		this.clearCell(i, j);
	}
	
	this.getPoints = function(sameColorCount)
	{
		
		return Math.pow(2,sameColorCount);
	}
	
	this.checkOfLosing = function ()
	{
		if(loser) return loser;
		var emptyCellsCount = 0;
		for(var i = 0; i < hCount; ++i)
			for(var j = 0; j < wCount; ++j)
					if(mas[i][j] == 0) ++emptyCellsCount;
		
		loser = (emptyCellsCount==0);
		
		
		
		return loser;
	}
	
	this.showLosingMsg = function()
	{
		context.beginPath();
		context.fillStyle = "#7e7e7e";
		context.strokeStyle = "#000";
		context.moveTo(size*wCount+2, 0);
    	context.fillRect(10, size*hCount/2-20, size*wCount-10, size*hCount/2+20);
		context.stroke();
		
		
		context.fillStyle = "#000"; 
		context.font = "bold 20px Sans";
		context.fillText('Losing with Points: '+points,20,size*hCount/2);
	}
	
	//проверка горизонтали
	this.checkHorizontalLine = function (iCheck, jCheck)
	{
		var erase = false;
		var sameColorCount = 0;
		var jMasForErase = new Array();
		
		for(var j=jCheck+1; j < wCount; j++)
		{
			if(mas[iCheck][j] == mas[iCheck][jCheck])
			{
				jMasForErase[sameColorCount] = j;
				++sameColorCount;
			}
			else break;
		}
		
		for(var j=jCheck; j >=0; j--)
		{
			if(mas[iCheck][j] == mas[iCheck][jCheck])
			{
				jMasForErase[sameColorCount] = j;
				++sameColorCount;
			}
			else break;
		}
		
		
		if(sameColorCount>=4)
		{
			erase = true;
			points += this.getPoints(sameColorCount);
			for(var j=0; j <sameColorCount; ++j)
				this.eraseCircle(iCheck,jMasForErase[j]);
			
			
			
		}
		
		return erase;
		
	}
	
	
	
	//проверка вертикали
	this.checkVerticalLine = function (iCheck, jCheck)
	{
		var erase = false;
		var sameColorCount = 0;
		var iMasForErase = new Array();
		
		for(var i=iCheck+1; i < hCount; i++)
		{
			if(mas[i][jCheck] == mas[iCheck][jCheck])
			{
				iMasForErase[sameColorCount] = i;
				++sameColorCount;
			}
			else break;
		}
		
		for(var i=iCheck; i >=0; i--)
		{
			if(mas[i][jCheck] == mas[iCheck][jCheck])
			{
				iMasForErase[sameColorCount] = i;
				++sameColorCount;
			}
			else break;
		}
		
		
		if(sameColorCount>=4)
		{
			erase = true;
			points += this.getPoints(sameColorCount);
			for(var i=0; i <sameColorCount; ++i)
				this.eraseCircle(iMasForErase[i],jCheck);
			
			
			
		}
		
		return erase;
	}
	
	//проверка на уничтожение линий
	this.checkLines = function (i,j)
	{
		var erase = false;
		var curColorN = mas[i][j];
		if(this.checkHorizontalLine(i,j))
		{
			erase = true;		
			this.circleView(i,j,0,this.getColor(curColorN));	
			mas[i][j] = curColorN;
		}
		if(this.checkVerticalLine(i,j) || erase)
		{	
			this.eraseCircle(i,j);
			erase = true;	
		}
			

			
			
		return erase;
	}
	
	//при клике, действия с шариком
	this.move = function (i, j)
	{
		if(loser) return;
		
		//если повторно на тот же шарик нажали
		if(curI == i &amp;amp;amp;&amp;amp;amp; curJ == j)
			this.clearTimer(i,j);
		else
		{
			
			//alert(curI+"=="+curJ);
			if(mas[i][j] !== 0)
			{
				//если был нажат ранее шарик, но сейчас жмём не на него
				if(curI !== -1 &amp;amp;amp;&amp;amp;amp; curJ !== -1)
					this.clearTimer(curI,curJ);
					
				curI = i;
				curJ = j;
				timer = setInterval(function() {self.runMultiple();}, 50);
			}
			else
			{
				//если может пройти
				if(this.checkPassability(i,j))
				{
					//ползёт
					this.circleGo(i,j);
					
					//если ничего не исчезло
					if(!this.checkLines(i,j))
						//новые шары
						this.drawRandomCircles();
						
					else 
						this.drawPoints();
					
				}
				else 
				{
				}
			}
		}
		
		
	}
	
	
	//двигаем шарик
	this.circleGo = function(i,j) 
	{
		var iToClear = curI;
		var jToClear =  curJ;
		
		mas[i][j] = mas[curI][curJ];
		this.clearTimer(curI, curJ);
		mas[iToClear][jToClear] = 0;
		this.clearCell(iToClear, jToClear);
		
		this.circleView(i,j,0,this.getColor(mas[i][j]));
	}
	
    // рисуем ячейку
    this.cellView = function(x1,y1,x2,y2) 
	{
		context.beginPath();   
		context.lineWidth = 2;
		context.moveTo(x1, y1);
		context.lineTo(x2, y1);
		context.lineTo(x2,y2);
		context.lineTo(x1, y2);
		context.lineTo(x1, y1);
		context.stroke();
		
    };
  
  	//чистка ячейки
  	this.clearCell = function(y,x) 
	{
		//сначала заливка фоном
		context.beginPath();
		context.fillStyle = "#969696";
		context.strokeStyle = "#000";
		context.moveTo(size*x, size*y);
    	context.fillRect(size*x, size*y, size, size);
		context.stroke();
		
		//затем перерисовка
		this.cellView(size*x, size*y, size*(x+1),size*(y+1));
		
    };
	
	//цвет для рэндомного шарика
	this.getColor = function(n)		 
	{
		rand = Math.random(); 
		switch(n)
		{
			case 1: return '#d71f42';
			case 2: return '#281fd7';
			case 3: return '#1fd74f';
			case 4: return '#e2ee59';
			default: return '#9721d4';
		}
		
	};
  
  
  	this.randomN = function(n)		 
	{
		rand=Math.random(); 
		if(rand<0.2) 
			return 1;
		if(rand<0.4) 
			return 2;
		if(rand<0.6) 
			return 3;
		if(rand<0.8) 
			return 4;
		
		return 5;
		
	};
	
	//рисуем шарик 
   	this.circleView = function(y,x, h, color) 
	{
		var radius = size/4;
		context.strokeStyle = color;
	
		context.beginPath();
		context.moveTo((x+0.5)*this.size+radius ,(y+0.5)*size);
		context.arc((x+0.5)*size, (y+0.5)*size+h, radius , 0, 2*Math.PI);
		context.fillStyle = color; 
		context.fill();
		context.stroke();
		//context.restore();
    };
	
	
  	this.drawRandomCircle = function()		 
	{
		
		if(this.checkOfLosing())  return;
		var randj=Math.round((Math.random())*(wCount-1)); 
		var randi=Math.round((Math.random())*(hCount-1)); 
		var fl=0;
		while(fl==0)
		{
			if(mas[randi][randj]==0)
			{
				mas[randi][randj] = this.randomN();
				this.circleView(randi,randj,0, this.getColor(mas[randi][randj])); 
				masRandom[randomCurrent][0] = randi;
				masRandom[randomCurrent][1] = randj;
				randomCurrent++;
				fl=1;
				//alert('g');
			}
			else
			{
				randj=Math.round((Math.random())*(wCount-1)); 
				randi=Math.round((Math.random())*(hCount-1)); 
			}
		}
		if(this.checkOfLosing())  return;
	};
	
	this.drawPoints = function() 
	{
		
		context.beginPath();
		context.fillStyle = "#969696";
		context.strokeStyle = "#000";
		context.moveTo(size*wCount+2, 0);
    	context.fillRect(size*wCount+2, 0, size*wCount+150, size*hCount);
		context.stroke();
		
		
		context.fillStyle = "#000"; 
		context.font = "bold 20px Sans";
		context.fillText(points,size*wCount+70,100);
		
		context.font = "bold 30px Sans";
		context.fillText('Points:',size*wCount+50,70);
		
		
	}
	
	this.drawRandomCircles = function()	
	{
		for (var i = 0; i < rCount; i++) this.drawRandomCircle();	
		
		//проверям, может рэндомные шары норм в линию на уничтожение встали
		for (var i = 0; i < rCount; i++) 
			if(this.checkLines(masRandom[i][0],masRandom[i][1]))
				this.drawPoints();
					
		randomCurrent = 0;
		if(loser) this.showLosingMsg();
	}
	
    // рисуем поле
    this.draw = function(cont, s) 
	{
		context = cont;
		size = s;
		//поле пусто - забиваем нулями
		mas = new Array(hCount);
		for(var i = 0; i < rCount; i++)
			masRandom[i] = new Array(2);
		for(var i = 0; i < hCount; i++)
		{
			mas[i] = new Array(wCount);
			for(var j = 0; j < wCount; j++)
				mas[i][j] = 0;
		}
		context.strokeStyle = "#000";
		
		//рисуем ячейки
        for (var i = 0; i < hCount; i++) 
		{
			
            for (var j = 0; j < wCount; j++) 
				this.cellView(size*j, size*i, size*(j+1),size*(i+1));

        }
		
		//начальные рэндомные шары
		this.drawRandomCircles();
		this.drawPoints();
    };
	
}

Код конечно грязноват, да и логика не вся продумана. Пока уничтожение только при совпадении по горизонтали и вертикали, диагональные элементы не проверяются. Плюс, если говорить об оригинале, то не показывается, где на следующем ходу появятся шары.

Если кто хочет оценить игру, смотрим.