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; (mas[i-1][j] == 0))
mas[i-1][j] = -1;
if((i+1 < hCount) &amp;amp;&amp;amp; (mas[i+1][j] == 0))
mas[i+1][j] = -1;
if((j-1 >= 0) &amp;amp;&amp;amp; (mas[i][j-1] == 0))
mas[i][j-1] = -1;
if((j+1 < wCount) &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; curJ == j)
this.clearTimer(i,j);
else
{
//alert(curI+"=="+curJ);
if(mas[i][j] !== 0)
{
//если был нажат ранее шарик, но сейчас жмём не на него
if(curI !== -1 &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();
};
}
Код конечно грязноват, да и логика не вся продумана. Пока уничтожение только при совпадении по горизонтали и вертикали, диагональные элементы не проверяются. Плюс, если говорить об оригинале, то не показывается, где на следующем ходу появятся шары.
Если кто хочет оценить игру, смотрим.
