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(); }; }
Код конечно грязноват, да и логика не вся продумана. Пока уничтожение только при совпадении по горизонтали и вертикали, диагональные элементы не проверяются. Плюс, если говорить об оригинале, то не показывается, где на следующем ходу появятся шары.
Если кто хочет оценить игру, смотрим.