Initial Commit (first version of Minesweeper.js)
This commit is contained in:
159
animations/click.js
Normal file
159
animations/click.js
Normal file
@@ -0,0 +1,159 @@
|
||||
window.requestAnimFrame = (function (callback) {
|
||||
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || function (callback) {
|
||||
window.setTimeout(callback, 1000 / 60);
|
||||
}
|
||||
})();
|
||||
|
||||
const overlayCanvas = document.getElementById('minesweeper-overlay');
|
||||
const overlayCtx = overlayCanvas.getContext('2d');
|
||||
|
||||
const particlesPerExplosion = 50;
|
||||
const particlesMinSpeed = 3;
|
||||
const particlesMaxSpeed = 6;
|
||||
const particlesMinSize = 3;
|
||||
const particlesMaxSize = 6;
|
||||
const explosions = [];
|
||||
|
||||
let fps = 60;
|
||||
const interval = 1000 / fps;
|
||||
|
||||
let now, delta;
|
||||
let then = Date.now();
|
||||
|
||||
// Optimization for mobile devices
|
||||
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
|
||||
fps = 29;
|
||||
}
|
||||
|
||||
// Draw
|
||||
function draw() {
|
||||
// Loop
|
||||
requestAnimationFrame(draw);
|
||||
|
||||
// Set NOW and DELTA
|
||||
now = Date.now();
|
||||
delta = now - then;
|
||||
|
||||
overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
|
||||
|
||||
// New frame
|
||||
if (delta > interval) {
|
||||
|
||||
// Update THEN
|
||||
then = now - (delta % interval);
|
||||
|
||||
// Our animation
|
||||
drawExplosion();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Draw explosion(s)
|
||||
function drawExplosion() {
|
||||
|
||||
if (explosions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < explosions.length; i++) {
|
||||
|
||||
const explosion = explosions[i];
|
||||
const particles = explosion.particles;
|
||||
|
||||
if (particles.length === 0) {
|
||||
explosions.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
const particlesAfterRemoval = particles.slice();
|
||||
for (let ii = 0; ii < particles.length; ii++) {
|
||||
|
||||
const particle = particles[ii];
|
||||
|
||||
// Check particle size
|
||||
// If 0, remove
|
||||
if (particle.size <= 0) {
|
||||
particlesAfterRemoval.splice(ii, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
overlayCtx.beginPath();
|
||||
overlayCtx.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false);
|
||||
overlayCtx.closePath();
|
||||
overlayCtx.fillStyle = 'rgb(' + particle.r + ',' + particle.g + ',' + particle.b + ')';
|
||||
overlayCtx.fill();
|
||||
|
||||
// Update
|
||||
particle.x += particle.xv;
|
||||
particle.y += particle.yv;
|
||||
particle.size -= .1;
|
||||
}
|
||||
|
||||
explosion.particles = particlesAfterRemoval;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Clicked
|
||||
function clicked(e) {
|
||||
|
||||
let xPos, yPos;
|
||||
|
||||
if (e.offsetX) {
|
||||
xPos = e.offsetX;
|
||||
yPos = e.offsetY;
|
||||
} else if (e.layerX) {
|
||||
xPos = e.layerX;
|
||||
yPos = e.layerY;
|
||||
}
|
||||
|
||||
explosions.push(
|
||||
new explosion(xPos, yPos)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// Explosion
|
||||
function explosion(x, y) {
|
||||
|
||||
this.particles = [];
|
||||
|
||||
for (let i = 0; i < particlesPerExplosion; i++) {
|
||||
this.particles.push(
|
||||
new particle(x, y)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Particle
|
||||
function particle(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.xv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
|
||||
this.yv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
|
||||
this.size = randInt(particlesMinSize, particlesMaxSize, true);
|
||||
this.r = randInt(113, 222);
|
||||
this.g = '00';
|
||||
this.b = randInt(105, 255);
|
||||
}
|
||||
|
||||
// Returns an random integer, positive or negative
|
||||
// between the given value
|
||||
function randInt(min, max, positive) {
|
||||
|
||||
let num;
|
||||
if (positive === false) {
|
||||
num = Math.floor(Math.random() * max) - min;
|
||||
num *= Math.floor(Math.random() * 2) === 1 ? 1 : -1;
|
||||
} else {
|
||||
num = Math.floor(Math.random() * max) + min;
|
||||
}
|
||||
|
||||
return num;
|
||||
|
||||
}
|
||||
|
||||
draw();
|
66
animations/victory.js
Normal file
66
animations/victory.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const overlay2Canvas = document.getElementById('minesweeper-overlay2');
|
||||
const overlay2Ctx = overlay2Canvas.getContext('2d');
|
||||
|
||||
let W = window.innerWidth,
|
||||
H = window.innerHeight,
|
||||
circles = [];
|
||||
|
||||
overlay2Canvas.width = W;
|
||||
overlay2Canvas.height = H;
|
||||
|
||||
//Random Circles creator
|
||||
function create() {
|
||||
|
||||
//Place the circles at the center
|
||||
|
||||
this.x = W/2;
|
||||
this.y = H/2;
|
||||
|
||||
|
||||
//Random radius between 2 and 6
|
||||
this.radius = 2 + Math.random()*3;
|
||||
|
||||
//Random velocities
|
||||
this.vx = -5 + Math.random()*10;
|
||||
this.vy = -5 + Math.random()*10;
|
||||
|
||||
//Random colors
|
||||
this.r = Math.round(Math.random())*255;
|
||||
this.g = Math.round(Math.random())*255;
|
||||
this.b = Math.round(Math.random())*255;
|
||||
}
|
||||
|
||||
for (var i = 0; i < 500; i++) {
|
||||
circles.push(new create());
|
||||
}
|
||||
|
||||
function drawVictory() {
|
||||
|
||||
//Fill overlay2Canvas with black color
|
||||
overlay2Ctx.globalCompositeOperation = "source-over";
|
||||
overlay2Ctx.fillStyle = "rgba(0,0,0,0.15)";
|
||||
overlay2Ctx.fillRect(0, 0, W, H);
|
||||
|
||||
//Fill the overlay2Canvas with circles
|
||||
for(var j = 0; j < circles.length; j++){
|
||||
var c = circles[j];
|
||||
|
||||
//Create the circles
|
||||
overlay2Ctx.beginPath();
|
||||
overlay2Ctx.arc(c.x, c.y, c.radius, 0, Math.PI*2, false);
|
||||
overlay2Ctx.fillStyle = "rgba("+c.r+", "+c.g+", "+c.b+", 0.5)";
|
||||
overlay2Ctx.fill();
|
||||
|
||||
c.x += c.vx;
|
||||
c.y += c.vy;
|
||||
c.radius -= .02;
|
||||
|
||||
if(c.radius < 0)
|
||||
circles[j] = new create();
|
||||
}
|
||||
}
|
||||
|
||||
function animate() {
|
||||
requestAnimFrame(animate);
|
||||
drawVictory();
|
||||
}
|
7
icons/flag.svg
Normal file
7
icons/flag.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||
<svg>
|
||||
<polygon points="105.62 69 22 69 22 8 105.62 8 87.519 38.5" fill="#ED1D1D"/>
|
||||
<polygon points="105.62 8 92.617 8 74.519 38.5 92.617 69 105.62 69 87.519 38.5" fill="#BF1111"/>
|
||||
<path d="M91.007,38.5L110.886,5H19v114c0,1.657,1.343,3,3,3s3-1.343,3-3V72h85.886L91.007,38.5z M25,11h75.349 L84.03,38.5L100.349,66H25V11z" fill="#231F20"/>
|
||||
</svg>
|
After Width: | Height: | Size: 484 B |
27
index.html
Normal file
27
index.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
<style>
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
#minesweeper-overlay, #minesweeper-overlay2 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0">
|
||||
|
||||
<canvas id="minesweeper-game" width="100" height="100"></canvas>
|
||||
<canvas id="minesweeper-overlay" width="100" height="100"></canvas>
|
||||
<canvas id="minesweeper-overlay2" width="100" height="100"></canvas>
|
||||
|
||||
<script type="text/javascript" src="animations/click.js"></script>
|
||||
<script type="text/javascript" src="animations/victory.js"></script>
|
||||
<script type="text/javascript" src="minesweeper.js"></script>
|
||||
</body>
|
||||
</html>
|
1
lib/canvg.min.js
vendored
Normal file
1
lib/canvg.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
333
minesweeper.js
Normal file
333
minesweeper.js
Normal file
@@ -0,0 +1,333 @@
|
||||
const canvas = document.getElementById('minesweeper-game');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const fieldSize = {x: 21, y: 13};
|
||||
let tileSize;
|
||||
const bombCount = 30;
|
||||
const field = [];
|
||||
let gameOver = false;
|
||||
let victory = false;
|
||||
const scaleFactor = .5;
|
||||
let isFirstClick = true;
|
||||
|
||||
ctx.scale(canvas.width / fieldSize.x * scaleFactor, canvas.height / fieldSize.y * scaleFactor);
|
||||
|
||||
/**
|
||||
* Initializes game by creating the game field and setting bombs
|
||||
*/
|
||||
function initGame() {
|
||||
for(let x = 0; x < fieldSize.x; x++) {
|
||||
field.push([]);
|
||||
for(let y = 0; y < fieldSize.y; y++) {
|
||||
field[x].push({tileValue: 0, clicked: false, flagged: false});
|
||||
}
|
||||
}
|
||||
|
||||
scaleCanvas();
|
||||
}
|
||||
|
||||
function initBombs(startX, startY) {
|
||||
for(let i = 0; i < bombCount; i++) {
|
||||
const ranX = Math.floor(Math.random() * fieldSize.x);
|
||||
const ranY = Math.floor(Math.random() * fieldSize.y);
|
||||
|
||||
if (ranX === startX || ranX === startX - 1 || ranX === startX + 1 || ranY === startY || ranY === startY - 1 || ranY === startY + 1 || field[ranX][ranY].tileValue === true) {
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
field[ranX][ranY].tileValue = true;
|
||||
}
|
||||
|
||||
for(let x = 0; x < fieldSize.x; x++) {
|
||||
for (let y = 0; y < fieldSize.y; y++) {
|
||||
if (field[x][y].tileValue !== true) {
|
||||
field[x][y].tileValue = countBombs(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawGrid() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
for(let x = 0; x < fieldSize.x; x++) {
|
||||
for (let y = 0; y < fieldSize.y; y++) {
|
||||
ctx.strokeRect(x * tileSize.x, y * tileSize.y, tileSize.x, tileSize.y);
|
||||
if(field[x][y].clicked)
|
||||
drawText(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSurroundingTiles(x, y) {
|
||||
const tiles = {};
|
||||
if(x > 0) {
|
||||
tiles["left"] = { tileValue: field[x - 1][y], x: x - 1, y: y };
|
||||
if(y > 0) {
|
||||
tiles["left-top"] = { tileValue: field[x - 1][y - 1], x: x - 1, y: y - 1 };
|
||||
}
|
||||
if(y < fieldSize.y - 1) {
|
||||
tiles["left-bottom"] = { tileValue: field[x - 1][y + 1], x: x - 1, y: y + 1 };
|
||||
}
|
||||
}
|
||||
if(x < fieldSize.x - 1) {
|
||||
tiles["right"] = { tileValue: field[x + 1][y], x: x + 1, y: y};
|
||||
if(y > 0)
|
||||
tiles["right-top"] = { tileValue: field[x + 1][y - 1], x: x + 1, y: y - 1 };
|
||||
if(y < fieldSize.y - 1)
|
||||
tiles["right-bottom"] = { tileValue: field[x + 1][y + 1], x: x + 1, y: y + 1 };
|
||||
}
|
||||
if(y > 0)
|
||||
tiles["top"] = { tileValue: field[x][y - 1], x: x, y: y - 1 };
|
||||
if(y < fieldSize.y - 1)
|
||||
tiles["bottom"] = { tileValue: field[x][y + 1], x: x, y: y + 1 };
|
||||
return tiles;
|
||||
}
|
||||
|
||||
function countBombs(x, y) {
|
||||
const tiles = getSurroundingTiles(x, y);
|
||||
return tiles.count(true);
|
||||
}
|
||||
|
||||
function countFlaggedBombs(x, y) {
|
||||
const tiles = getSurroundingTiles(x, y);
|
||||
return tiles.countFlagged(true);
|
||||
}
|
||||
|
||||
Object.prototype.count = function (val) {
|
||||
let counter = 0;
|
||||
for(let el in this) {
|
||||
if(this.hasOwnProperty(el)) {
|
||||
if (val === this[el].tileValue.tileValue) {
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return counter;
|
||||
};
|
||||
|
||||
Object.prototype.countFlagged = function (val) {
|
||||
let counter = 0;
|
||||
for(let el in this) {
|
||||
if(this.hasOwnProperty(el)) {
|
||||
if(this[el].tileValue.flagged === val) {
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return counter;
|
||||
};
|
||||
|
||||
function tileClickEvent(x, y) {
|
||||
if(gameOver)
|
||||
return;
|
||||
uncoverTile(x, y);
|
||||
if(!field[x][y].flagged && field[x][y].tileValue === true) {
|
||||
gameOver = true;
|
||||
gameOverEvent();
|
||||
}
|
||||
}
|
||||
|
||||
function tileDoubleClick(x, y) {
|
||||
if(gameOver)
|
||||
return;
|
||||
if(field[x][y].clicked && !field[x][y].flagged && countFlaggedBombs(x, y) === field[x][y].tileValue) {
|
||||
uncoverSurroundings(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
function tileFlag(x, y) {
|
||||
if(gameOver)
|
||||
return;
|
||||
if(field[x][y].clicked && !field[x][y].flagged)
|
||||
return;
|
||||
field[x][y].flagged = !field[x][y].flagged;
|
||||
field[x][y].clicked = field[x][y].flagged;
|
||||
ctx.clearRect(x * tileSize.x + (1 / scaleFactor), y * tileSize.y + (1 / scaleFactor), tileSize.x - (2 / scaleFactor), tileSize.y - (2 / scaleFactor));
|
||||
drawText(x, y);
|
||||
}
|
||||
|
||||
|
||||
overlay2Canvas.addEventListener("click", (e) => {
|
||||
const pos = getPositon(e);
|
||||
|
||||
if(isFirstClick) {
|
||||
initBombs(pos.x, pos.y);
|
||||
isFirstClick = false;
|
||||
}
|
||||
|
||||
tileClickEvent(pos.x, pos.y);
|
||||
|
||||
victoryCheck();
|
||||
|
||||
clicked(e);
|
||||
});
|
||||
|
||||
overlay2Canvas.addEventListener("dblclick", (e) => {
|
||||
const pos = getPositon(e);
|
||||
|
||||
tileDoubleClick(pos.x, pos.y);
|
||||
|
||||
victoryCheck();
|
||||
});
|
||||
|
||||
overlay2Canvas.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
console.log(e);
|
||||
|
||||
const pos = getPositon(e);
|
||||
|
||||
tileFlag(pos.x, pos.y);
|
||||
});
|
||||
|
||||
function getPositon(e) {
|
||||
const x = e.x - canvas.offsetLeft;
|
||||
const y = e.y - canvas.offsetTop;
|
||||
const fieldX = Math.floor(x / tileSize.x);
|
||||
const fieldY = Math.floor(y / tileSize.y);
|
||||
|
||||
return {x: fieldX, y: fieldY};
|
||||
}
|
||||
|
||||
function scaleCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
overlayCanvas.width = window.innerWidth;
|
||||
overlayCanvas.height = window.innerHeight;
|
||||
|
||||
W = window.innerWidth;
|
||||
H = window.innerHeight;
|
||||
|
||||
// tileSize = {x: canvas.width / fieldSize.x, y: canvas.height / fieldSize.y};
|
||||
tileSize = {x: 100, y: 100};
|
||||
drawGrid();
|
||||
if(gameOver) {
|
||||
gameOverEvent();
|
||||
}
|
||||
}
|
||||
|
||||
function uncoverTile(x, y) {
|
||||
if(field[x][y].clicked || field[x][y].flagged) {
|
||||
return;
|
||||
}
|
||||
field[x][y].clicked = true;
|
||||
drawText(x, y);
|
||||
if(field[x][y].tileValue === true) {
|
||||
gameOverEvent();
|
||||
}
|
||||
if(field[x][y].tileValue === 0) {
|
||||
uncoverSurroundings(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
function uncoverSurroundings(x, y) {
|
||||
const surrounding = getSurroundingTiles(x, y);
|
||||
for(let tile in surrounding) {
|
||||
if(surrounding.hasOwnProperty(tile)) {
|
||||
uncoverTile(surrounding[tile].x, surrounding[tile].y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const colors = {
|
||||
1: "blue",
|
||||
2: "green",
|
||||
3: "red",
|
||||
4: "purple",
|
||||
5: "yellow",
|
||||
6: "pink"
|
||||
};
|
||||
|
||||
function drawText(x, y) {
|
||||
ctx.font = "bold 50px Roboto";
|
||||
ctx.textAlign = "center";
|
||||
if(!field[x][y].flagged && field[x][y].clicked) {
|
||||
ctx.fillStyle = "#ddd";
|
||||
ctx.fillRect(x * tileSize.x + 1, y * tileSize.y + 1, tileSize.x - 2, tileSize.y - 2);
|
||||
if (field[x][y].tileValue !== 0) {
|
||||
ctx.fillStyle = colors[field[x][y].tileValue];
|
||||
ctx.fillText(field[x][y].tileValue, (x + .5) * tileSize.x, (y + .5) * tileSize.y + 15);
|
||||
}
|
||||
} else if(field[x][y].flagged) {
|
||||
ctx.fillStyle = "red";
|
||||
ctx.fillRect(x * tileSize.x + 5 / scaleFactor, y * tileSize.y + 5 / scaleFactor, tileSize.x - 10 / scaleFactor, tileSize.y - 10 / scaleFactor);
|
||||
|
||||
ctx.font = "bold 50px FontAwesome";
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillText("", (x + .5) * tileSize.x, (y + .5) * tileSize.y + 15);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
scaleCanvas();
|
||||
});
|
||||
|
||||
function gameOverEvent() {
|
||||
console.log("Game Over");
|
||||
ctx.fillStyle = "orange";
|
||||
ctx.fillText("Game Over", canvas.width / 2, canvas.height / 2);
|
||||
animateBackground({r: 0, g: 0, b: 0, a: 0}, 0, 0, canvas.width, canvas.height, .75, new Date().getTime(), 2);
|
||||
animateText("Game Over", canvas.width / 2, canvas.height / 2, 0, 100, new Date().getTime(), 200);
|
||||
}
|
||||
|
||||
function animateText(text, x, y, curFontSize, finalFontSize, startTime, speed) {
|
||||
const time = (new Date()).getTime() - startTime;
|
||||
|
||||
const newFontSize = speed * time / 1000;
|
||||
|
||||
if(newFontSize < finalFontSize) {
|
||||
curFontSize = newFontSize;
|
||||
} else {
|
||||
curFontSize = finalFontSize;
|
||||
}
|
||||
|
||||
// drawGrid();
|
||||
ctx.fillStyle = "orange";
|
||||
ctx.font = "bold " + curFontSize + "px Roboto";
|
||||
ctx.fillText(text, x, y);
|
||||
|
||||
requestAnimFrame(function () {
|
||||
animateText(text, x, y, curFontSize, finalFontSize, startTime, speed);
|
||||
})
|
||||
}
|
||||
|
||||
function animateBackground(color, x, y, width, height, maxOpacity, startTime, speed) {
|
||||
const time = (new Date()).getTime() - startTime;
|
||||
|
||||
const newOpacity = speed * time / 1000;
|
||||
|
||||
if(newOpacity <= maxOpacity) color.a = newOpacity;
|
||||
|
||||
drawGrid();
|
||||
ctx.fillStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + color.a + ")";
|
||||
ctx.fillRect(x, y, width, height);
|
||||
|
||||
requestAnimFrame(function () {
|
||||
animateBackground(color, x, y, width, height, maxOpacity, startTime, speed);
|
||||
});
|
||||
}
|
||||
|
||||
function countClickedTiles() {
|
||||
let count = 0;
|
||||
for(let x = 0; x < fieldSize.x; x++) {
|
||||
for(let y = 0; y < fieldSize.y; y++) {
|
||||
if(field[x][y].clicked && !field[x][y].flagged)
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function victoryCheck() {
|
||||
if(!victory && countClickedTiles() === fieldSize.x * fieldSize.y - bombCount) {
|
||||
victory = true;
|
||||
victoryEvent();
|
||||
}
|
||||
}
|
||||
|
||||
function victoryEvent() {
|
||||
console.log("Win!");
|
||||
animate();
|
||||
}
|
||||
|
||||
initGame();
|
Reference in New Issue
Block a user