Классический сапёр на html5 и LibCanvas

  • Tutorial


В этой статье я пошагово расскажу, как писать самый обычный, классический сапёр при помощи Html5 Canvas, AtomJS, и тайлового движка LibCanvas.

А также смотрите продолжение — "Изометрический сапёр на LibCanvas (html5)"


Воспользуемся стандартным шаблоном для «старта» нашего приложения. Важно не забывать подключать js-файлы после создания соответствующих классов.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>LibCanvas :: Mines</title>
        <link href="/files/styles.css" rel="stylesheet" />
        <script src="/files/js/atom.js"></script>
        <script src="/files/js/libcanvas.js"></script>
    </head>
    <body>
        <p><a href="/">Return to index</a></p>
        <script>
new function () {
    LibCanvas.extract();

    atom.dom(function () {
        new Mines.Controller();
    });
};
        </script>
        <script src="js/controller.js"></script>
    </body>
</html>


Я нарисовал две картинки — мины и флага. Всё остальное мы будем делать «вручную» прям в приложении. Объединил их в один спрайт для уменьшения количества запросов и предзагружу перед тем, как стартовать приложение. В коде так же можно увидеть нарезку при помощи atom.ImagePreloader:

/** @class Mines.Controller */
atom.declare( 'Mines.Controller', {
	initialize: function () {
		atom.ImagePreloader.run({
			flag: 'flag-mine.png [48:48]{0:0}',
			mine: 'flag-mine.png [48:48]{1:0}'
		}, this.start.bind(this) );
	},

	start: function (images) {
		this.images = images;
	}
});


Отрисовка


Я люблю визуально видеть то, что присходит, потому предпочитаю начинать с программирования отрисовки, а только потом переходить к логике. Для того, чтобы наш код заработал мы воспользуемся LibCanvas.Engines.Tile. Добавим класс View, в котором и создадим наш движок. Также нам надо создать простое приложение и привязать движок к приложению при помощи TileEngine.Element.app. Значение по умолчанию у нас будет равно закрытой ячейке. Не забудем создать этот View, в нашем контроллере.

/** @class Mines.View */
atom.declare( 'Mines.View', {
	initialize: function (controller, fieldSize) {
		this.images = controller.images;

		this.engine = new TileEngine({
			size: fieldSize,
			cellSize: new Size(24, 24),
			cellMargin: new Size(0, 0),
			defaultValue: 'closed'
		})
		.setMethod( this.createMethods() );

		this.app = new App({
			size  : this.engine.countSize(),
			simple: true
		});

		this.element = TileEngine.Element.app( this.app, this.engine );
	},


/** @class Mines.Controller */

// ...

	start: function (images) {
		this.images = images;
		this.view = new Mines.View( this, new Size(15,8) );
	}



Не торопитесь запускать этот код, у нас ещё не определён метод createMethods класса View. Давайте вообще определимся с тем, какие у нас могут быть состояния ячейки.

Во время игры мы можем видеть такое:

1. Числа от 1 до 8.
2. Закрытая ячейка
3. Открытая, но пустая ячейка
4. Флажок

После её окончания — следующее:

1. Все мины
2. Если подорвались на одной из них — она выделена
3. Если где-то неверно поставили флаг

Итого, 8 + 3 + 3 = 14 разных состояний. Опишем их все:

/** @class Mines.View */

// ...

	createMethods: function () {
		return {
			1: this.number.bind(this, 1),
			2: this.number.bind(this, 2),
			3: this.number.bind(this, 3),
			4: this.number.bind(this, 4),
			5: this.number.bind(this, 5),
			6: this.number.bind(this, 6),
			7: this.number.bind(this, 7),
			8: this.number.bind(this, 8),
			explode : this.explode.bind(this),
			closed  : this.closed .bind(this),
			mine    : this.mine   .bind(this),
			flag    : this.flag   .bind(this),
			empty   : this.empty  .bind(this),
			wrong   : this.wrong  .bind(this)
		};
	},


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

/** @class Mines.Controller */
// ...
	start: function (images) {
		// ...

		// todo: remove after debug
		'1 2 3 4 5 6 7 8 empty mine flag explode wrong closed'
			.split(' ')
			.forEach(function (name, i) {
				this.view.engine
					.getCellByIndex(new Point(i, 3))
					.value = name;
			}.bind(this));


Мы просто взяли все индексы и присвоили их по очереди разным клеткам поля. Теперь отрисовка. В первую очередь нам необходимо создать общий метод, который будет «раскрашивать» ячейку — заливать и обводить необходимым цветом. Если линия шириной в 1 пиксель будет отрисовываться в целые координаты — она будет блуриться (см htmlbook.ru/html5/canvas, ответ на вопрос «В. Почему мы начинаем x и y c 0.5, а не с 0?»), потому воспользуемся экспериментальным методом прямоугольника snapToPixel

/** @class Mines.View */
// ...
	color: function (ctx, cell, fillStyle, strokeStyle) {
		var strokeRect = cell.rectangle.clone().snapToPixel();

		return ctx
			.fill( cell.rectangle, fillStyle)
			.stroke( strokeRect, strokeStyle );
	},


Теперь по очереди добавляем методы отрисовки. Пустая клетка — просто красим:

/** @class Mines.View */
// ...
	empty: function (ctx, cell) {
		return this.color(ctx, cell, '#999', '#aaa');
	},


Мина и флаг — это просто картинки на пустой клетке:

/** @class Mines.View */
// ...
	mine: function (ctx, cell) {
		return this
			.empty(ctx, cell)
			.drawImage( this.images.get('mine'), cell.rectangle );
	},

	flag: function (ctx, cell) {
		return this
			.empty(ctx, cell)
			.drawImage( this.images.get('flag'), cell.rectangle );
	},


Мина, на которой мы подорвались отрисовывается с красным фоном:

/** @class Mines.View */
// ...
	explode: function (ctx, cell) {
		return this
			.color(ctx, cell, '#c00', '#aaa')
			.drawImage( this.images.get('mine'), cell.rectangle );
	},


Неправильно установленный флаг — красный крест. Отрисовать его достаточно просто. Сначала — ограничиваем отрисовку в пределах нашего прямоугольника при помощи clip.
Заливаем его фоном, а потом рисуем две красных линии — с верхнего-левого в нижний-правый и с нижнего-левого угла в верхний-правый.

/** @class Mines.View */
// ...
	wrong: function (ctx, cell) {
		var r = cell.rectangle;

		return this.empty(ctx, cell)
			.save()
			.clip( r )
			.set({ lineWidth: Math.round(cell.rectangle.width / 8) })
			.stroke( new Line( r.from      , r.to       ), '#900' )
			.stroke( new Line( r.bottomLeft, r.topRight ), '#900' )
			.restore();
	},


Закрытая ячейка отрисовывается тоже достаточно просто — градиент от тёмного к светлому, с верхнего-левого угла в нижний-правый.

/** @class Mines.View */
// ...
	closed: function (ctx, cell) {
		return ctx.fill( cell.rectangle,
			ctx.createGradient(cell.rectangle, {
				0: '#eee', 1: '#aaa'
			})
		);
	},


И, собственно, цифры. Сначала в прототип добавим список цветов для каждой цифры. Нуля нету, потому ставим нул.
Обратите внимание, что первым аргументом функции у нас number. Именно его мы биндили в методе createMethods.
После этого рисуем клетку, как пустую, а сверху, текстом, пишем цифру.

/** @class Mines.View */
// ...
	numberColors: [null, '#009', '#060', '#550', '#808', '#900', '#555', '#055', '#000' ],

	number: function (number, ctx, cell) {
		var size = Math.round(cell.rectangle.height * 0.8);

		return this.empty(ctx, cell)
			.text({
				text  : number,
				color : this.numberColors[number],
				size  : size,
				lineHeight: size,
				weight: 'bold',
				align : 'center',
				to    : cell.rectangle
			});
	}


Наша реализация позволяет нам менять размер ячеек и они будут в любом случае отлично выглядеть:



Генератор мин


Как видим, отрисовка полностью готова. Теперь нам достаточно сделать простое действие и клетка поменяет свой внешний вид.

Удалим наш дебаг-код и создадим инстанс генератора:
/** @class Mines.Controller */
// ..
	start: function (images) {
		this.images = images;
		
		this.size  = new Size(15, 8);
		this.mines = 20;

		this.view = new Mines.View( this, this.size );
		this.generator = new Mines.Generator( this.size, this.mines );
	}


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



Алгоритм генерации мин у нас будет очень простой — создаём список валидных точек (все, кроме той, на которую кликнули) — метод snapshot, после этого «выдёргиваем» из них необходимое количество случайных — метод createMines:
/** @class Mines.Generator */
atom.declare( 'Mines.Generator', {

	mines: null,

	initialize: function (fieldSize, minesCount) {
		this.fieldSize  = fieldSize;
		this.minesCount = minesCount;
	},

	/** @private */
	snapshot: function (ignore) {
		var x, y, point,
			result = [],
			size = this.fieldSize;

		for (y = size.height; y--;) for (x = size.width; x--;) {
			point = new Point(x, y);

			if (!point.equals(ignore)) {
				result.push(point);
			}
		}

		return result;
	},

	/** @private */
	createMines: function (count, ignore) {
		var snapshot = this.snapshot( ignore );

		return atom.array.create(count, function () {
			return snapshot.popRandom();
		});
	}

});


Следующий шаг — это добавить api-метод, который будет вызываться для генерации этих мин и заносить их в индекс для быстрого доступа. Создадим двумерный хеш со значениями 1, где мина есть и 0, где мины нету. Нам важно использовать именно Integer, причину мы увидим ниже. Теперь у нас есть быстрый метод isMine для определения, есть ли мина по координате. Метод isReady будет использоваться, чтобы узнать внешним классам, сгенерировано ли уже минное поле.

/** @class Mines.Generator */
// ..
	
	isReady: function () {
		return this.mines != null;
	},

	isMine: function (point) {
		return this.mines[point.y][point.x];
	},

	generate: function (ignore) {
		var mines, minesIndex,
			size = this.fieldSize;

		mines = this.createMines(this.minesCount, ignore);

		minesIndex = atom.array.fillMatrix(size.width, size.height, 0);

		mines.forEach(function (point) {
			minesIndex[point.y][point.x] = 1;
		});

		this.mines = minesIndex;
	},


Следующий шаг — сделать получение значения клетки, если там мины нет. Алгоритм очень прост — берём всех соседей, которые не выходят за рамки поля, считаем суму их значений. Именно в этом месте то, что мина есть Integer нам и пригодилось.

/** @class Mines.Generator */
// ..
	initialize: function (fieldSize, minesCount) {
		// эти два метода мы передаём как колбеки, потому привяжем их к контексту
		this.bindMethods([ 'isValidPoint', 'isMine' ]);


// ..
	getValue: function (point) {
		// получаем всех соседей
		return this.getNeighbours(point)
			// превращаем их в список мин (1 и 0)
			.map(this.isMine)
			// получаем количество мин в соседних клетках
			.sum();
	},

	// Проверяем, чтобы точка не вышла за пределы поля
	isValidPoint: function (point) {
		return point.x >= 0
			&& point.y >= 0
			&& point.x < this.fieldSize.width
			&& point.y < this.fieldSize.height;
	},

	// Список соседей - это все соседи, кроме тех, что выходят за границы
	getNeighbours: function (point) {
		return point.neighbours.filter( this.isValidPoint );
	},


Взаимодействие с пользователем


У нас есть движок игры, теперь необходимо всё это сделать игрой, а не только логикой. Создаём класс Action, который будет отвечать за все действия пользователя. Первое, что мы сделаем — это реакцию на клик пользователя. При помощи TileEngine.Mouse мы будем слушать события мыши, связанные с полем. Вешаем Mouse.prevent на событие 'contextmenu', чтобы не выскакивало надоедливое меню. При клике проверяем кнопку. Левая кнопка мыши равна 0, средняя равна 1, правая равна 2. Напомним, что в оригинальной игре клик левой означал открытие клетки, крик средней — открытие всех окружающих, а клик правой — постановка мины.

/** @class Mines.Controller */
// ..
	start: function (images) {
		// ..
		this.action = new Mines.Action(this);
	}


/** @class Mines.Action */
atom.declare( 'Mines.Action', {
	actions: [ 'open', 'all', 'close' ],

	initialize: function (controller) {
		this.controller = controller;
		this.bindMouse();
	},

	bindMouse: function () {
		var view, mouse;

		view = this.controller.view;
		mouse = new Mouse(view.app.container.bounds);

		new App.MouseHandler({ mouse: mouse, app: view.app })
			.subscribe( view.element );

		mouse.events.add( 'contextmenu', Mouse.prevent );

		new TileEngine.Mouse( view.element, mouse ).events
			.add( 'click', function (cell, e) {
				this.activate(cell, e.button);
			}.bind(this));
	},

	activate: function (cell, actionCode) {
		console.log( cell.point.dump(), actionCode );
	}
});


Добавим первую интерактивность. Мы будем получать по индексу название метода, который необходимо вызвать и, заодно напишем самый простой метод — close. Если клетка закрыта, то устанавливаем на неё флаг, если на клетке уже стоит флаг, то отмечаем её закрытой. Теперь можно увидеть первое взаимодействие — по правой кнопке мыши появляется флаг на клетке.

/** @class Mines.Action */

// ...

	activate: function (cell, actionCode) {
		if (typeof actionCode == 'number') {
			actionCode = this.actions[actionCode];
		}

		this[actionCode](cell);
	},

	close: function (cell) {
		if (cell.value == 'closed') {
			cell.value = 'flag';
		} else if (cell.value == 'flag') {
			cell.value = 'closed';
		}
	},

	open: function (cell) {

	},

	all: function (cell) {

	}


Теперь опишем открытие клетки. Для начала, открываем только те клетки, которые закрыты. Нечего взаимодействовать с всякими статичными цифрами и флагами. Во-вторых, проверяем, готовы ли наши мины и, если нет — запускаем генератор.

Если открыта мина, то вызываем метод lose, где помечаем клетку, как взорвавшуюся.
Если в клетке есть цифра, то просто пишем её, никаких других действий с этой клеткой не сделать.
Если клетка пуста, то нам необходимо рекурсивно открывать все клетки вокруг, потому пока создаём метод и помечаем клетку как пустую.

/** @class Mines.Action */

// ...

	open: function (cell) {
		if (cell.value != 'closed') return;
		
		var value, gen = this.controller.generator;

		if (!gen.isReady()) {
			gen.generate(cell.point);
		}

		if (gen.isMine(cell.point)) {
			this.lose(cell);
		} else {
			value = gen.getValue(cell.point);

			if (value) {
				cell.value = value;
			} else {
				this.openEmpty(cell);
			}
		}
	},

	lose: function () {
		cell.value = 'explode';
	},

	openEmpty: function (cell) {
		cell.value = 'empty';
	},


Для открытия всех клеток вокруг пустой просто получаем соседей и передаём в метод open. Этим мы воспользуемся для рекурсивного открытия пустых клеток и для быстрого открытия по средней кнопке мыши.

/** @class Mines.Action */

// ...
	openNeighbours: function (cell) {
		this.controller.generator
			.getNeighbours(cell.point)
			.forEach(function (point) {
				this.open( this.getCell(point) );
			}.bind(this));
	},

	openEmpty: function (cell) {
		cell.value = 'empty';

		this.openNeighbours(cell);
	},

	getCell: function (point) {
		return this.controller.view.engine.getCellByIndex(point);
	},

	all: function (cell) {
		if (parseInt(cell.value)) {
			this.openNeighbours(cell);
		}
	},


Проигрышь отображаем так — проходим все клетки, где у нас было закрыто и на самом деле была мина — отрисовываем мину. Где у нас стоял флаг, а на самом деле мины нету — отображаем ошибку. Так же блокируем методы open и close после проигрыша.

/** @class Mines.Action */

// ...
	lost: false,
	
	lose: function (cell) {
		this.lost = true;
		
		cell.value = 'explode';

		this.controller.view.engine.cells
			.forEach(this.checkCell.bind(this));
	},

	checkCell: function (cell) {
		if (cell.value == 'closed' || cell.value == 'flag') {
			var isMine = this.controller.generator.isMine(cell.point);
			
			if (isMine && cell.value == 'closed') {
				cell.value = 'mine';
			}
			if (!isMine && cell.value == 'flag') {
				cell.value = 'wrong';
			}
		}
	},


// ...
	close: function (cell) {
		if (this.lost) return;
// ...
	open: function (cell) {
		if (this.lost) return;
// ...




Победа!


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

/** @class Mines.Action */

// ...
	initialize: function (controller) {

		// ...

		this.startTime = null;

		this.minesLeft = controller.mines;
		this.minesTrace = atom.trace(0);
		this.changeMines(0);

		this.emptyCells = controller.size.width * controller.size.height - this.minesLeft;
	},

	changeMines: function (delta) {
		this.minesLeft += delta;
		this.minesTrace.value = "Mines: " + this.minesLeft;
	},

	// ...

	open: function (cell) {
		// ...

		if (!gen.isReady()) {
			// ...
			this.startTime = Date.now();
		}

		if (gen.isMine(cell.point)) {
			// ...
		} else {
			// ...

			if (--this.emptyCells == 0) {
				this.win();
			}
		}
	},

	// ...
	win: function () {
		var time = Math.round( (Date.now()-this.startTime) / 1000 );
		alert.delay(100, window, ['Congratulations! Mines has been neutralized in '+ time +' sec!']);
	},

	// ...
	close: function (cell) {
		// ...

		if (cell.value == 'closed') {
			// ...
			this.changeMines(-1);
		} else if (cell.value == 'flag') {
			// ...
			this.changeMines(+1);
		}
	},


Играть в сапёр

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Нравится ли вам формат топика «Краткое описание + Кусок кода»?

Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 58

  • НЛО прилетело и опубликовало эту надпись здесь
      +1
      Нажатия обеих кнопок мыши на открытой цифре очень не хватает

      Колёсико мышки
        0
        Это не то. Л + П кнопок реально не хватает. Моторная память.
          –2
          Как альтернатива — просто ЛКМ на цифре. Очень удобно.
      +2
      Делал лет 5 назад на таблице с картинками.
      Миноискатель лучше сделать по левому клик на открытой ячейке.
        0
        Миноискатель лучше сделать по левому клик на открытой ячейке.

        Согласен, добавил.
        0
        Однако в оригинальном сапере было ещё и состояние — «может мина-может нет» :) вопросик там. и соответственно, при клике двумя кнопками мыши — открывались/проверялись все остальные по кругу, без учета вопроса. Т.е. если стоит вопрос — он не взорвется, однако и не откроется.

        зы: надеюсь вы знаете о возможности клика двумя кнопками мыши?
          –2
          зы: надеюсь вы знаете о возможности клика двумя кнопками мыши?

          Колёсико мышки
          –1
          У меня знакомый подобного сапера реализовал года 2 назад со всеми тыкалками с двух кнопок и выставления флажков.
          Кому интересно, пруф могу дать)
            +1
            Кошмар детства вернулся…
            Скрытый текст
            image
              0
              В чем заключается кошмар?
                0
                так и не нашел решение :(
                +8
                Дык это же просто, у вас ещё куча ходов)



                В одной из двух жёлтых клеток ОБЯЗАТЕЛЬНО стоит мина (например, четвёрке не хватает одной), следовательно в зелёной мины нету согласно соседней цифре, потому что она есть в жёлтой.
                  0
                  okay :( я всегда был недостаточно умен для этой игры :(
                    +2
                    Я всегда подозревал, что чего-то еще я в этой игре не знаю. Спасибо :)
                  0
                  Супер, спасибо за статью.
                  Лицензия? Можно игру к себе утащить?
                    +1
                    Естественно. Сама статья — под CC BY 3.0.
                    AtomJS, LibCanvas и все примеры с ресурсами на LibCanvas.GitHub.Com — под лицензией LGPL/MIT
                    +2
                    В опросе ответил «Да», только рекомендую любой код в котором больше 10 строк оборачивать в тег «Скрыть/показать»
                      0
                      Что-то на клики мышкой очень плохо реагирует. Если быстро кликать, половину кликов пропускает. Может, конечно, моего старенького CoreDuo не хватает для такой игры :(
                        0
                        Может, конечно, моего старенького CoreDuo не хватает для такой игры :(

                        Это уже чисто рефлекс такой — для каждого приложения на html5 кричать о загрузке проца? Вы бы посмотрели перед подобными предположениями в диспетчер задач и увидели там цифры от 0 до 3% на вкладку с игрой.

                        Не повторяется у окружающих, расскажите подробнее о вашей проблеме, пожалуйста. Какой браузер? Как нестандартно кликаете?
                          0
                          Браузер Firefox 18.0.1. Просто откройте поле и кликайте от всей души в рэндомном порядке левой кнопкой, пока не взорвётесь. Сколько раз кликнули по разным клеткам, столько и должно открыться. У меня открывается существенно меньше.

                          Про загрузку проца я не говорил, вы сами додумали. Если же игра ест 3% CPU, почему она клики не все обрабатывает? И почему не повторяется? Вон ниже IDVsbruck пишет о том же самом.
                            –1
                            Сейчас, впрочем, лучше выходит. Исправили чего-то уже?
                              +1
                              Да, перевесил событие на mousedown, а не на click. Раньше, если нажать на одной клетке, а поднять на другой, то ни одна не открывалась. Есть предположение, что при очень быстрой игре именно так и происходило.

                              Про загрузку проца я не говорил, вы сами додумали

                              Тогда я не понял к чему это:

                              Может, конечно, моего старенького CoreDuo не хватает для такой игры
                                –1
                                А вы откройте классического сапёра и посмотрите. Там события висят и на mousedown, и на mouseup, и на mousemove. На mousedown текущая клетка прижимается, на mousemove она сдвигается, а на mouseup открывается та, на которой мышка стоит в конце. С двойным прижатием то же самое: оно срабатывает на отпускание. Только флажки ставятся на mousedown, так как установка флажка — безопасное действие.

                                Тогда я не понял к чему это:

                                Табличку «Сарказм» нарисовать? :-)
                                  0
                                  У меня нету классического сапёра))
                                    –1
                                    Что ж вы пишете «классического сапёра», даже не посмотрев, как на самом деле работает классический сапёр? :-)
                                      0
                                      Считайте это стилизацией под классический сапёр.
                                      Когда я выложу следующую тему, вы поймёте, почему я назвал текущую именно так.
                                        +1
                                        Гляньте топик "Изометрический сапёр на LibCanvas". Я надеюсь, вы поймёте, почему текущая реализация была названа «классической»,
                                          0
                                          Забавный.
                                      0
                                      Нарисуйте себе на лбу фломастером. Этот «сарказм» уже во всех комментариях к каждой статье надоело конкретно. Про проц вы первый написали и тов. Shock ничего недодумывал и если вы не имели ввиду загрузку процессора тогда объясните пожалуйста что вы имели ввиду. То что в оригинальном сапёре есть куча других фич это понятно, но у автора этого поста очевидно не было цели написать сапёра в который бы все по утрам заходили играть, слово tutorial же должно о чём-то говорить читающему о статье, а вы цепляетесь к кликам и травите неуместные шутки про свой процессор.
                                        0
                                        Не кипятитесь так сильно :-) Я имел в виду ровно то что написал — программа пропускала клики. Если вы считаете, что для динамичной игры совершенно неважно, реагирует программа на клики мышки или нет, то хорошо, будем считать, что я «цепляюсь». А так если человек написал глючную программу, он должен быть готов к незлым насмешкам. Тем более, если это tutorial, то есть он учит других. Вы никогда не смеялись над глупыми ошибками в учебниках?
                                          –1
                                          Вместо того, чтобы смеятся необходимо постараться сделать лучше. Полноценный фидбек по кликам был бы намного результативнее насмешек. Тем более, у меня обычно, вроде, неплохое качество ;) Я на вас не обижаюсь, но объективно считаю, что можно было поступить более правильно.
                                            +1
                                            Не кипячусь )) Я всегда с утра слегка не в настроении, поймите правильно )) Просто у нас с вами разное представление о целях автора этой статьи. Вы воспринимаете пост как руководство по написанию сапёра, а я как руководство по взаимодействию с libcanvas )) Поэтому вы видите недостатки, а я нет ))
                              –1
                              Я люблю визуально видеть то, что присходит

                              Стесняюсь спросить, а как еще можно видеть?
                                +1
                                Ну можно видеть в каком-то текстовом виде, или как результаты тестов, или верить, что оно происходит.
                                  –1
                                  Я имею в виду, что «визуально» = «зрительно наблюдая», то есть глазами. Можно визуально программировать, увеличивать, уменьшать, работать,… но видеть — это получается «масло масляное».
                                    +1
                                    Возможно, больше подошло бы что-то вроде «видеть визуализированно»?
                                      –1
                                      Кажется, это то же самое. Можно «я люблю сразу видеть, что получается» или «мне нравится иметь визуальное представление (или визуализацию) того, что происходит» или «я люблю визуально наблюдать, что происходит».
                                        +1
                                        Такое ощущение что не на хабре, а на лингвистическом форуме сижу. Я вот сразу понял что автор хотел сказать, вы нет? Кстати при написании сапёра вообще можно отрисовку слепить ближе к концу написания игры при большом желании, и получится так что ковыряя код на js вы вообще визуально не будете видеть как выглядит игра на текущем моменте.
                                          0
                                          «Хабр — для грамотных людей» (цитата). Я лишь хотел помочь автору стать лучше. Понятность — не аргумент. «Новичёк», «хочет углублятся», «построеного», «объеденил», «расскрашивать» — Вы же сразу поняли, что автор хотел сказать?
                                            0
                                            Я сразу понял. Ну раз вы грамотный человек, тогда для поправок и используйте личку, а комментарии они для того и комментарии что должны быть к смысловому содержанию статьи относиться.
                                              0
                                              Я может быть сегодня не выспался, но крайне раздражают люди которые нудят про запятые, тся/ться, две Н и прочие ништяки русского языка в общей массе комментариев.
                                                0
                                                Я всякую мелочь как раз и отправил автору в личку, а про «визуально видеть» не смог придумать однозначной замены. Думал, может кто подскажет, как лучше (и не предполагал дискуссии).
                                                  0
                                                  Подтверждаю, было дело, ошибки исправлены по фидбеку)
                                  +2
                                  Расцветка — жуть. Отзывчивать никакая! — Когда «профессионально» играешь в минер, скорость кликов очень высокая, а предложенная реализация иногда «забывает» открыть клетку.
                                  Растяжки цветов не нужны тут, максимально «метровкий» дизайн, чтобы не отвлекало, хотя и изящности циферок не помешало бы добавить. Идеальным считаю расцветку классического — к нему все привыкли, и основной психологический барьер связан с невосприятием гаммы.
                                    +1
                                    Я кое-что исправил, скажите — улучшилась ли «отзывчивость»? Теперь он не «забывает» открыть клетку?
                                    0
                                    Не помешала бы еще кнопка «начать новую игру».
                                      0
                                      Ф5 ведь) Я и так очень нагрузил деталями — старался максимально резать функциональность для топика.
                                        0
                                        А как показывает практика, лучше два кода. Один для статьи, другой для странички с кнопкой Рестарт и прочими сладостями, народ так больше любит. Большинство по ссылке поиграть переходит, а не код смотреть)
                                          +1
                                          Согласен, но для двух кодов надо два времени(
                                            0
                                            Да. Время ресурс дефицитный.
                                      0
                                      Флажок, думаю, на закрытой ячейке должен рисоваться
                                        +1
                                        Во всех примерах на LC, я все-таки так и не увидел хорошего решения проблемы игрового контекста. Поясню: хотелось бы не протягивать указатель на экземпляр движка и игрового контроллера во все классы бизнес логики, а просто иметь стандартный игровой контекст в том месте, где он становится нужен по ходу дела. Если для приложения, типа сапёра, показанный в статье прием еще как-то терпеть можно, то для чуть более сложного приложения такое, «протягивание» становится утомительным и чреватым ошибками. Плохо, но решил бы задачу синглтон движка, но я уже признал, что это плохое решение, а лучшего пока что-то мозгов не хватает придумать.
                                          +2
                                          Вспомнил за что я так любил сапёра.

                                          image
                                            +1
                                            Даааа, безумно обидно)) Но в этом случае хоть сам виноват. Вон в топике есть пример, когда у тебя просто выбора нету, кроме как угадывать:
                                            image
                                            0
                                            Где картинки?

                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                          Самое читаемое