<!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>
/** @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
. Add a class View
, in which we create our engine. We also need to create a simple application and bind the engine to the application using TileEngine.Element.app
. The default value will be equal to the closed cell. Do not forget to create this View
, in our controller. /** @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
method of the View
class. Let's generally define what we can have cell states. /** @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
methods, bringing them to the current context. In order to see what we have, we need to add the corresponding cells on the field. /** @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));
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
. That is what we bind in the createMethods
method. /** @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
method, then “pull” the necessary number of random ones out of them - the createMines
method: /** @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(); }); } });
isMine
method to determine if there is a mine by coordinate. The isReady
method will be used to tell external classes whether a minefield has already been generated. /** @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; },
/** @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
class that will be responsible for all user actions. The first thing we do is react to a user’s click. With TileEngine.Mouse
we will listen to mouse events associated with the field. We hang Mouse.prevent
on the 'contextmenu'
event so that the annoying menu does not pop up. When clicking we check the button. The left mouse button is 0, the middle one is 1, the right one is 2. Recall that in the original game the left click meant opening the cage, the middle shouting revealed all others, and the right click showed mines. /** @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
. If the cell is closed, then set the flag on it, if the flag is already on the cell, then mark it closed. Now you can see the first interaction - the flag on the cell appears by the right mouse button. /** @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
method, where we mark the cell as exploded. /** @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
method. We will use this for recursive opening of empty cells and for quick opening by the middle mouse button. /** @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
and close
methods after losing. /** @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
. Get the number of minutes. Calculate the number of empty cells - this is the number of cells just minus the number of minutes. Each time a cell is opened, we will decrease the value of empty cells by one. When they reach zero - the game is won. Let us draw the canvas and with a slight delay display the alert to the user. /** @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); } },
Source: https://habr.com/ru/post/168435/
All Articles