Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/node_modules
/assets
.idea
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
# js-minesweeper
# js-minesweeper

## Основные правила:

* Для начала игры необходимо указать размер игрового поля и количество мин и нажать "Start";
* Открытие ячейки осуществляется кликом левой кнопкой мыши;
* Установка/снятие флажка - правая кнопка мыши;
* Игра заканчивает в случае, если:
* Все ячейки с минами помечены фалжками, а все остальные ячейки открыты - победа;
* Открыта любая ячейка с миной - поражение.
* В случае победы результат (количество потраченных на игру секунд) фиксируется локалльно (localStorage), выводится таблица с результатами.
Текущий результат выделен серым цветом.

7 changes: 7 additions & 0 deletions frontend/js/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

let Game = require('./game');

let game = new Game({
element: document.querySelector('[data-component="game"]')
});
39 changes: 39 additions & 0 deletions frontend/js/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict';

exports.CellType = {
EMPTY: 0,
MINE: -1
};

exports.GameState = {
START: 0,
WIN: 1,
LOST: 2
};

exports.Default = {
COLUMNS: 9,
ROWS: 9,
MINES: 10,
MIN_ROWS: 9,
MAX_ROWS: 20,
MIN_COLUMNS: 9,
MAX_COLUMNS: 20,
MIN_MINES: 2,
MAX_MINES: 40
};

exports.Selectors = {
ROWS_INPUT: '[name="rows"]',
COLUMNS_INPUT: '[name="columns"]',
MINES_INPUT: '[name="mines"]',
MINES_RANGE_TEXT: '[data-range="mines"]'
};

exports.Classes = {
EMPTY: 'cell-empty',
MINE: 'cell-mine',
NUMBER: 'cell-number',
MARKED: 'cell-marked',
HIGHLIGHT: 'cell-highlight'
};
184 changes: 184 additions & 0 deletions frontend/js/controls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
'use strict';

let Dispatcher = require('./dispatcher');
let Default = require('./constants').Default;
let Selectors = require('./constants').Selectors;
let controlsTemplate = require('../templates/controls-template.hbs');

/**
* Class representing the control panel
*/
class Controls extends Dispatcher {
/**
* Create controls
* @param options
*/
constructor(options) {
super(options);

this._render();
this._timer = options.timer;
this._timerEl = this._el.querySelector('[data-content="timer"]');
this._addListeners();
}

/**
* Render controls from template
* @private
*/
_render() {
let defaultOptions = {
minRows: Default.MIN_ROWS,
maxRows: Default.MAX_ROWS,
minCols: Default.MIN_COLUMNS,
maxCols: Default.MAX_COLUMNS,
minMines: Default.MIN_MINES,
maxMines: Default.MAX_MINES,
rows: Default.ROWS,
cols: Default.COLUMNS,
mines: Default.MINES
};

this._el.innerHTML = controlsTemplate({
options: defaultOptions
});

this._rowsInput = this._el.querySelector(Selectors.ROWS_INPUT);
this._columnsInput = this._el.querySelector(Selectors.COLUMNS_INPUT);
this._minesInput = this._el.querySelector(Selectors.MINES_INPUT);
this._minesRangeText = this._el.querySelector(Selectors.MINES_RANGE_TEXT);
}

/**
* Add main listeners for click (start game), timer update, and options input change
* @private
*/
_addListeners() {
this._el.addEventListener('click', this._onClick.bind(this));
this._el.addEventListener('change', this._onChange.bind(this));
this._timer.addEventListener('timeUpdate', this._onTimerUpdate.bind(this));
}

/**
* 'Click' event handler
* @param {MouseEvent} event - click event
* @private
*/
_onClick(event) {
var startBtn = event.target.closest('[data-action="start"]');

if (!startBtn) {
return
}
this._triggerStart();
}

/**
* Implement start - dispatch 'start' event with current options
* @private
*/
_triggerStart() {
let options = this._getOptions();

this.dispatchEvent('start', options);
}

/**
* 'Change' event hadler
* @param {Event} event - change event
* @private
*/
_onChange(event) {
let optionsForm = event.target.closest('[name="options-form"]');

if (!optionsForm) {
return;
}

this._validateOptions();
}

/**
* Validate current options
* @private
*/
_validateOptions() {
let options = this._getOptions();
//Check if input is not valid number
if (isNaN(options.columns)) {
options.columns = Default.MIN_COLUMNS;
}
if (isNaN(options.rows)) {
options.rows = Default.MIN_ROWS;
}
if (isNaN(options.mines)) {
options.rows = Default.MIN_MINES;
}

options.rows = Math.max(Default.MIN_ROWS, Math.min(options.rows, Default.MAX_ROWS));
options.columns = Math.max(Default.MIN_COLUMNS, Math.min(options.columns, Default.MAX_COLUMNS));

//Calculate mines max range
let maxMines = this._getMinesMaxRange(options.rows, options.columns);
options.mines = Math.max(Default.MIN_MINES, Math.min(options.mines, maxMines));
//Set mines range
this._minesRangeText.textContent = `(${Default.MIN_MINES}-${maxMines})`;
this._minesInput.max = Default.MIN_MINES;
this._minesInput.max = maxMines;

this._setOptions(options);
}

/**
* Get options from DOM
* @returns {{rows: Number, columns: Number, mines: Number}}
* @private
*/
_getOptions() {
return {
rows: parseInt(this._rowsInput.value),
columns: parseInt(this._columnsInput.value),
mines: parseInt(this._minesInput.value)
}
}

/**
* Set options to DOM
* @param options
* @private
*/
_setOptions(options) {
this._rowsInput.value = options.rows;
this._columnsInput.value = options.columns;
this._minesInput.value = options.mines;
}

_getMinesMaxRange(rows, columns) {
return Math.round(rows * columns / 2);
}

/**
* Render new timer value
* @param {CustomEvent} event
* @private
*/
_onTimerUpdate(event) {
let time = event.detail;
let min = Math.floor(time / 60);
let sec = time % 60;

this._timerEl.textContent = `${this._twoDigits(min)}:${this._twoDigits(sec)}`;
}

/**
* Convert n to two-digit string
* @param {Number} n
* @private
*/
_twoDigits(n) {
return ('0' + n).slice(-2);
}
}


module.exports = Controls;
43 changes: 43 additions & 0 deletions frontend/js/dispatcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';
/**
* Class representing the base event dispathcer
*/
class Dispatcher {
/**
* Crrate dispatcher
* @param options
*/
constructor(options) {
options = options || {};
this._el = options.element || document.createElement('div');
}

/**
* Add event handler
* @param {string} type
* @param {function} handler
* @param {boolean} capture - use capture
*/
addEventListener(type, handler, capture) {
this._el.addEventListener(type, handler, capture);
}

/**
* Dispatch custom event
* @param {string} type
* @param detail - event data
* @param {object} options
*/
dispatchEvent(type, detail, options) {
options = options || {};

if (detail != undefined) {
options.detail = detail;
}

let event = new CustomEvent(type, options);
this._el.dispatchEvent(event);
}
}

module.exports = Dispatcher;
Loading