@@ -4,9 +4,6 @@ window.requestAnimFrame = (function (callback) {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const overlayCanvas = document.getElementById('minesweeper-overlay');
|
|
||||||
const overlayCtx = overlayCanvas.getContext('2d');
|
|
||||||
|
|
||||||
const particlesPerExplosion = 10;
|
const particlesPerExplosion = 10;
|
||||||
const particlesMinSpeed = 3;
|
const particlesMinSpeed = 3;
|
||||||
const particlesMaxSpeed = 5;
|
const particlesMaxSpeed = 5;
|
||||||
@@ -40,7 +37,7 @@ function drawClickAnimation() {
|
|||||||
now = Date.now();
|
now = Date.now();
|
||||||
delta = now - then;
|
delta = now - then;
|
||||||
|
|
||||||
overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
|
game.layer1.clearRect(0, 0, game.width, game.height);
|
||||||
|
|
||||||
// New frame
|
// New frame
|
||||||
if (delta > interval) {
|
if (delta > interval) {
|
||||||
@@ -81,11 +78,11 @@ function drawExplosion() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
overlayCtx.beginPath();
|
game.layer1.beginPath();
|
||||||
overlayCtx.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false);
|
game.layer1.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false);
|
||||||
overlayCtx.closePath();
|
game.layer1.closePath();
|
||||||
overlayCtx.fillStyle = 'rgb(' + particle.r + ',' + particle.g + ',' + particle.b + ')';
|
game.layer1.fillStyle = 'rgb(' + particle.r + ',' + particle.g + ',' + particle.b + ')';
|
||||||
overlayCtx.fill();
|
game.layer1.fill();
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
particle.x += particle.xv;
|
particle.x += particle.xv;
|
||||||
@@ -158,5 +155,3 @@ function randInt(min, max, positive) {
|
|||||||
return num;
|
return num;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
drawClickAnimation();
|
|
||||||
|
@@ -1,18 +1,10 @@
|
|||||||
const overlay2Canvas = document.getElementById('minesweeper-overlay2');
|
let circles = [];
|
||||||
const overlay2Ctx = overlay2Canvas.getContext('2d');
|
|
||||||
|
|
||||||
let W = window.innerWidth,
|
|
||||||
H = window.innerHeight,
|
|
||||||
circles = [];
|
|
||||||
|
|
||||||
overlay2Canvas.width = W;
|
|
||||||
overlay2Canvas.height = H;
|
|
||||||
|
|
||||||
//Random Circles creator
|
//Random Circles creator
|
||||||
function Create() {
|
function create() {
|
||||||
//Place the circles at the center
|
//Place the circles at the center
|
||||||
this.x = W/2;
|
this.x = game.width / 2;
|
||||||
this.y = H/2;
|
this.y = game.height / 2;
|
||||||
|
|
||||||
//Random radius between 2 and 6
|
//Random radius between 2 and 6
|
||||||
this.radius = 2 + Math.random()*3;
|
this.radius = 2 + Math.random()*3;
|
||||||
@@ -30,32 +22,32 @@ function Create() {
|
|||||||
function initBalls() {
|
function initBalls() {
|
||||||
circles = [];
|
circles = [];
|
||||||
for (let i = 0; i < 500; i++) {
|
for (let i = 0; i < 500; i++) {
|
||||||
circles.push(new Create());
|
circles.push(new create());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawVictory() {
|
function drawVictory() {
|
||||||
//Fill overlay2Canvas with black color
|
//Fill overlay2Canvas with black color
|
||||||
overlayCtx.globalCompositeOperation = "source-over";
|
game.layer1.globalCompositeOperation = "source-over";
|
||||||
overlayCtx.fillStyle = "rgba(0,0,0,0.15)";
|
game.layer1.fillStyle = "rgba(0,0,0,0.15)";
|
||||||
overlayCtx.fillRect(0, 0, W, H);
|
game.layer1.fillRect(0, 0, game.width, game.height);
|
||||||
|
|
||||||
//Fill the overlay2Canvas with circles
|
//Fill the overlay2Canvas with circles
|
||||||
for(let j = 0; j < circles.length; j++){
|
for(let j = 0; j < circles.length; j++){
|
||||||
const c = circles[j];
|
const c = circles[j];
|
||||||
|
|
||||||
//Create the circles
|
//create the circles
|
||||||
overlayCtx.beginPath();
|
game.layer1.beginPath();
|
||||||
overlayCtx.arc(c.x, c.y, c.radius, 0, Math.PI*2, false);
|
game.layer1.arc(c.x, c.y, c.radius, 0, Math.PI*2, false);
|
||||||
overlayCtx.fillStyle = "rgba("+c.r+", "+c.g+", "+c.b+", 0.5)";
|
game.layer1.fillStyle = "rgba("+c.r+", "+c.g+", "+c.b+", 0.5)";
|
||||||
overlayCtx.fill();
|
game.layer1.fill();
|
||||||
|
|
||||||
c.x += c.vx;
|
c.x += c.vx;
|
||||||
c.y += c.vy;
|
c.y += c.vy;
|
||||||
c.radius -= .02;
|
c.radius -= .02;
|
||||||
|
|
||||||
if(c.radius < 0)
|
if(c.radius < 0)
|
||||||
circles[j] = new Create();
|
circles[j] = new create();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
136
css/style.css
Normal file
136
css/style.css
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
html {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-background {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #333;
|
||||||
|
-webkit-transition: background-color .5s;
|
||||||
|
-moz-transition: background-color .5s;
|
||||||
|
-o-transition: background-color .5s;
|
||||||
|
transition: background-color .5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-background.transparent {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
-webkit-transform: translate(-50%, -50%);
|
||||||
|
-moz-transform: translate(-50%, -50%);
|
||||||
|
-ms-transform: translate(-50%, -50%);
|
||||||
|
-o-transform: translate(-50%, -50%);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-container.slideDown {
|
||||||
|
animation: slideDown .5s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
0% {
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
top: 150%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-container h3 {
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-container button {
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: #2272ff;
|
||||||
|
color: #fff;
|
||||||
|
padding: 20px 40px;
|
||||||
|
font-size: 28px;
|
||||||
|
-webkit-transition: all .2s;
|
||||||
|
-moz-transition: all .2s;
|
||||||
|
-o-transition: all .2s;
|
||||||
|
transition: all .2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-container button:hover {
|
||||||
|
box-shadow: 0 10px 20px rgba(0,0,0,.18);
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-container button:active {
|
||||||
|
background-color: #1e64cd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2.5%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-stats {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(2.5vh / 2);
|
||||||
|
left: 50%;
|
||||||
|
height: 5vh;
|
||||||
|
width: 1592px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-container {
|
||||||
|
float: left;
|
||||||
|
background-color: black;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: red;
|
||||||
|
font-size: 4vh;
|
||||||
|
font-family: "SF Digital Readout", Roboto, Arial, sans-serif;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-container.right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#time, #bombs {
|
||||||
|
margin: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
animation: slideIn .5s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
top: -100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
541
game.js
Normal file
541
game.js
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
class Game {
|
||||||
|
constructor() {
|
||||||
|
const elements = `
|
||||||
|
<div class="main-container">
|
||||||
|
<div id="game-stats" class="game-stats">
|
||||||
|
<div class="stat-container">
|
||||||
|
<span id="bombs">
|
||||||
|
000
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-container right">
|
||||||
|
<span id="time">
|
||||||
|
00:00
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="game-container">
|
||||||
|
<canvas id="minesweeper-game" width="100" height="100"></canvas>
|
||||||
|
<canvas class="overlay" id="minesweeper-overlay" width="100" height="100"></canvas>
|
||||||
|
<canvas class="overlay" id="minesweeper-overlay2" width="100" height="100"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>`.toDOM();
|
||||||
|
document.body.appendChild(elements);
|
||||||
|
|
||||||
|
this.canvas = document.getElementById('minesweeper-game');
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
|
||||||
|
this.layer1Canvas = document.getElementById('minesweeper-overlay');
|
||||||
|
this.layer1 = this.layer1Canvas.getContext('2d');
|
||||||
|
|
||||||
|
this.layer2Canvas = document.getElementById('minesweeper-overlay2');
|
||||||
|
this.layer2 = this.layer2Canvas.getContext('2d');
|
||||||
|
|
||||||
|
this.statsContainer = document.getElementById('game-stats');
|
||||||
|
this.timeEl = document.getElementById('time');
|
||||||
|
this.bombsEl = document.getElementById('bombs');
|
||||||
|
|
||||||
|
this.fieldSize = {x: 16, y: 12};
|
||||||
|
this.bombCount = 25;
|
||||||
|
this.field = [];
|
||||||
|
this.gameOver = false;
|
||||||
|
this.scaleFactor = .5;
|
||||||
|
this.isFirstClick = true;
|
||||||
|
|
||||||
|
this.windowX = 0;
|
||||||
|
this.windowY = 0;
|
||||||
|
this.zoomFactor = 1;
|
||||||
|
|
||||||
|
this.startTime = 0;
|
||||||
|
|
||||||
|
this.startClientX = 0;
|
||||||
|
this.startClientY = 0;
|
||||||
|
this.startWindowX = 0;
|
||||||
|
this.startWindowY = 0;
|
||||||
|
|
||||||
|
this.hasClicked = false;
|
||||||
|
this.isDragging = false;
|
||||||
|
|
||||||
|
this.ctx.scale(this.canvas.width / this.fieldSize.x * this.scaleFactor, this.canvas.height / this.fieldSize.y * this.scaleFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyScaling() {
|
||||||
|
this.renderingConfig = this.calcScaling();
|
||||||
|
|
||||||
|
this.drawGrid(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
calcScaling() {
|
||||||
|
const width = Math.ceil(this.fieldSize.x * this.zoomFactor) + 1;
|
||||||
|
const height = Math.ceil(this.fieldSize.y * this.zoomFactor) + 1;
|
||||||
|
|
||||||
|
const offsetX = Math.floor(this.windowX * this.fieldSize.x);
|
||||||
|
const offsetY = Math.floor(this.windowY * this.fieldSize.y);
|
||||||
|
|
||||||
|
const tiltX = this.windowX * this.fieldSize.x - offsetX;
|
||||||
|
const tiltY = this.windowY * this.fieldSize.y - offsetY;
|
||||||
|
|
||||||
|
const sizeX = this.tileSize.x / this.zoomFactor;
|
||||||
|
const sizeY = this.tileSize.y / this.zoomFactor;
|
||||||
|
|
||||||
|
return {
|
||||||
|
width, height, offsetX, offsetY, tiltX, tiltY, sizeX, sizeY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
countBombs(x, y) {
|
||||||
|
const tiles = this.getSurroundingTiles(x, y);
|
||||||
|
return tiles.count(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
countFlaggedBombs(x, y) {
|
||||||
|
const tiles = this.getSurroundingTiles(x, y);
|
||||||
|
return tiles.countFlagged(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
countClickedTiles() {
|
||||||
|
let count = 0;
|
||||||
|
for (let x = 0; x < this.fieldSize.x; x++) {
|
||||||
|
for (let y = 0; y < this.fieldSize.y; y++) {
|
||||||
|
if (this.field[x][y].clicked && !this.field[x][y].flagged)
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
countTotalFlags() {
|
||||||
|
let count = 0;
|
||||||
|
for (let x = 0; x < this.fieldSize.x; x++) {
|
||||||
|
for (let y = 0; y < this.fieldSize.y; y++) {
|
||||||
|
if (this.field[x][y].flagged)
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawGrid(animations = true) {
|
||||||
|
this.ctx.clearRect(0, 0, this.width, this.height);
|
||||||
|
|
||||||
|
for (let x = 0; x < this.renderingConfig.width; x++) {
|
||||||
|
for (let y = 0; y < this.renderingConfig.height; y++) {
|
||||||
|
this.drawTile(x, y, animations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawTile(x, y, animations = true) {
|
||||||
|
const virtualX = this.renderingConfig.offsetX + x;
|
||||||
|
const virtualY = this.renderingConfig.offsetY + y;
|
||||||
|
|
||||||
|
if (virtualX >= this.fieldSize.x || virtualY >= this.fieldSize.y)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const content = this.field[virtualX][virtualY];
|
||||||
|
|
||||||
|
const width = .8 * this.renderingConfig.sizeX;
|
||||||
|
const height = .8 * this.renderingConfig.sizeY;
|
||||||
|
const drawX = (x + .1 - this.renderingConfig.tiltX) * this.renderingConfig.sizeX;
|
||||||
|
const drawY = (y + .1 - this.renderingConfig.tiltY) * this.renderingConfig.sizeY;
|
||||||
|
const radius = this.renderingConfig.sizeX * .1;
|
||||||
|
|
||||||
|
let color = this.getColor(virtualX, virtualY);
|
||||||
|
const fontSize = this.renderingConfig.sizeY * .5;
|
||||||
|
let fontFamily = "Roboto";
|
||||||
|
let textColor = "white";
|
||||||
|
let text = "";
|
||||||
|
|
||||||
|
this.ctx.textAlign = "center";
|
||||||
|
let duration = 0;
|
||||||
|
if (animations)
|
||||||
|
duration = 150;
|
||||||
|
|
||||||
|
if (!content.flagged && content.clicked) {
|
||||||
|
color = "#ddd";
|
||||||
|
if (content.tileValue === true) {
|
||||||
|
text = "";
|
||||||
|
fontFamily = "FontAwesome";
|
||||||
|
textColor = "#aa2211";
|
||||||
|
color = "#333";
|
||||||
|
} else if (content.tileValue !== 0) {
|
||||||
|
text = content.tileValue;
|
||||||
|
textColor = colors[content.tileValue];
|
||||||
|
}
|
||||||
|
} else if (content.flagged) {
|
||||||
|
color = "#ff0000";
|
||||||
|
fontFamily = "FontAwesome";
|
||||||
|
text = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
animateTile(this.ctx, drawX, drawY, 0, 0, width, height, 0, radius, new Date().getTime(), duration, color);
|
||||||
|
if (text !== "") {
|
||||||
|
animateText(this.ctx, this.renderingConfig, text, x, y, 0, fontSize, new Date().getTime(), duration, textColor, fontFamily);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gameOverEvent() {
|
||||||
|
play = false;
|
||||||
|
animateBackground(0, 0, this.width, this.height, 0, .75, new Date().getTime(), 200, {
|
||||||
|
r: 0,
|
||||||
|
g: 0,
|
||||||
|
b: 0,
|
||||||
|
a: 0
|
||||||
|
});
|
||||||
|
animateText(this.layer2, this.renderingConfig, "Game Over", this.fieldSize.x / 2 - .5, this.fieldSize.y / 2 - .5, 0, this.tileSize.y * 1.33, new Date().getTime(), 200, "orange", "Roboto");
|
||||||
|
}
|
||||||
|
|
||||||
|
getColor(x, y) {
|
||||||
|
x++;
|
||||||
|
y++;
|
||||||
|
const pos = x * y;
|
||||||
|
const limit = this.fieldSize.x * this.fieldSize.y;
|
||||||
|
|
||||||
|
let percentage = pos / limit * 360;
|
||||||
|
|
||||||
|
return `hsl(${percentage},100%,50%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPosition(e) {
|
||||||
|
const x = (e.x - (window.innerWidth - this.width) / 2) / (this.width * this.zoomFactor) + this.windowX;
|
||||||
|
const y = (e.y - (window.innerHeight - this.height) * .75) / (this.height * this.zoomFactor) + this.windowY;
|
||||||
|
const fieldX = Math.floor(x * this.fieldSize.x);
|
||||||
|
const fieldY = Math.floor(y * this.fieldSize.y);
|
||||||
|
|
||||||
|
return {x: fieldX, y: fieldY};
|
||||||
|
}
|
||||||
|
|
||||||
|
getSurroundingTiles(x, y) {
|
||||||
|
const tiles = {};
|
||||||
|
if (x > 0) {
|
||||||
|
tiles["left"] = {tileValue: this.field[x - 1][y], x: x - 1, y: y};
|
||||||
|
if (y > 0) {
|
||||||
|
tiles["left-top"] = {tileValue: this.field[x - 1][y - 1], x: x - 1, y: y - 1};
|
||||||
|
}
|
||||||
|
if (y < this.fieldSize.y - 1) {
|
||||||
|
tiles["left-bottom"] = {tileValue: this.field[x - 1][y + 1], x: x - 1, y: y + 1};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (x < this.fieldSize.x - 1) {
|
||||||
|
tiles["right"] = {tileValue: this.field[x + 1][y], x: x + 1, y: y};
|
||||||
|
if (y > 0)
|
||||||
|
tiles["right-top"] = {tileValue: this.field[x + 1][y - 1], x: x + 1, y: y - 1};
|
||||||
|
if (y < this.fieldSize.y - 1)
|
||||||
|
tiles["right-bottom"] = {tileValue: this.field[x + 1][y + 1], x: x + 1, y: y + 1};
|
||||||
|
}
|
||||||
|
if (y > 0)
|
||||||
|
tiles["top"] = {tileValue: this.field[x][y - 1], x: x, y: y - 1};
|
||||||
|
if (y < this.fieldSize.y - 1)
|
||||||
|
tiles["bottom"] = {tileValue: this.field[x][y + 1], x: x, y: y + 1};
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
initBombs(startX, startY) {
|
||||||
|
for (let i = 0; i < this.bombCount; i++) {
|
||||||
|
const ranX = Math.floor(Math.random() * this.fieldSize.x);
|
||||||
|
const ranY = Math.floor(Math.random() * this.fieldSize.y);
|
||||||
|
|
||||||
|
if (ranX === startX || ranX === startX - 1 || ranX === startX + 1 || ranY === startY || ranY === startY - 1 || ranY === startY + 1 || this.field[ranX][ranY].tileValue === true) {
|
||||||
|
i--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.field[ranX][ranY].tileValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let x = 0; x < this.fieldSize.x; x++) {
|
||||||
|
for (let y = 0; y < this.fieldSize.y; y++) {
|
||||||
|
if (this.field[x][y].tileValue !== true) {
|
||||||
|
this.field[x][y].tileValue = this.countBombs(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initEventListeners() {
|
||||||
|
this.layer2Canvas.addEventListener("click", (e) => {
|
||||||
|
if (this.isDragging)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const pos = this.getPosition(e);
|
||||||
|
|
||||||
|
if (this.isFirstClick) {
|
||||||
|
this.initBombs(pos.x, pos.y);
|
||||||
|
this.initTime();
|
||||||
|
this.isFirstClick = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tileClickEvent(pos.x, pos.y);
|
||||||
|
|
||||||
|
this.victoryEvent();
|
||||||
|
|
||||||
|
clicked(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.layer2Canvas.addEventListener("dblclick", (e) => {
|
||||||
|
if (this.isDragging)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const pos = this.getPosition(e);
|
||||||
|
|
||||||
|
this.tileDoubleClick(pos.x, pos.y);
|
||||||
|
|
||||||
|
this.victoryEvent();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.layer2Canvas.addEventListener("contextmenu", (e) => {
|
||||||
|
if (this.isDragging)
|
||||||
|
return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const pos = this.getPosition(e);
|
||||||
|
|
||||||
|
this.tileFlag(pos.x, pos.y);
|
||||||
|
|
||||||
|
this.updateBombs();
|
||||||
|
|
||||||
|
this.victoryEvent();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("keyup", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const changeRate = .05;
|
||||||
|
|
||||||
|
let newZoomFactor = this.zoomFactor;
|
||||||
|
let newWindowX = this.windowX;
|
||||||
|
let newWindowY = this.windowY;
|
||||||
|
|
||||||
|
if (e.code === "BracketRight") {
|
||||||
|
newZoomFactor -= changeRate;
|
||||||
|
} else if (e.code === "Slash") {
|
||||||
|
newZoomFactor += changeRate;
|
||||||
|
} else if (e.code === "ArrowLeft") {
|
||||||
|
newWindowX -= changeRate;
|
||||||
|
} else if (e.code === "ArrowRight") {
|
||||||
|
newWindowX += changeRate;
|
||||||
|
} else if (e.code === "ArrowUp") {
|
||||||
|
newWindowY -= changeRate;
|
||||||
|
} else if (e.code === "ArrowDown") {
|
||||||
|
newWindowY += changeRate;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newZoomFactor = Math.min(newZoomFactor, 1);
|
||||||
|
newZoomFactor = Math.max(newZoomFactor, .1);
|
||||||
|
|
||||||
|
newWindowX = Math.min(newWindowX, 1 - newZoomFactor);
|
||||||
|
newWindowY = Math.min(newWindowY, 1 - newZoomFactor);
|
||||||
|
newWindowX = Math.max(newWindowX, 0);
|
||||||
|
newWindowY = Math.max(newWindowY, 0);
|
||||||
|
|
||||||
|
if (newZoomFactor !== this.zoomFactor || newWindowX !== this.windowX || newWindowY !== this.windowY) {
|
||||||
|
this.zoomFactor = newZoomFactor;
|
||||||
|
this.windowX = newWindowX;
|
||||||
|
this.windowY = newWindowY;
|
||||||
|
this.applyScaling();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", (e) => {
|
||||||
|
if (e.button === 0) {
|
||||||
|
this.hasClicked = true;
|
||||||
|
this.startClientX = e.clientX;
|
||||||
|
this.startClientY = e.clientY;
|
||||||
|
this.startWindowX = this.windowX;
|
||||||
|
this.startWindowY = this.windowY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("mouseup", () => {
|
||||||
|
this.hasClicked = false;
|
||||||
|
if (this.isDragging) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isDragging = false;
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", (e) => {
|
||||||
|
if (this.hasClicked) {
|
||||||
|
this.isDragging = true;
|
||||||
|
|
||||||
|
const deltaX = e.clientX - this.startClientX;
|
||||||
|
const deltaY = e.clientY - this.startClientY;
|
||||||
|
|
||||||
|
this.windowX = this.startWindowX - deltaX / this.width;
|
||||||
|
this.windowY = this.startWindowY - deltaY / this.height;
|
||||||
|
|
||||||
|
this.windowX = Math.min(this.windowX, 1 - this.zoomFactor);
|
||||||
|
this.windowY = Math.min(this.windowY, 1 - this.zoomFactor);
|
||||||
|
this.windowX = Math.max(this.windowX, 0);
|
||||||
|
this.windowY = Math.max(this.windowY, 0);
|
||||||
|
|
||||||
|
this.applyScaling();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
this.scaleCanvas();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes game by creating the game field and setting bombs
|
||||||
|
*/
|
||||||
|
initGame() {
|
||||||
|
for (let x = 0; x < this.fieldSize.x; x++) {
|
||||||
|
this.field.push([]);
|
||||||
|
for (let y = 0; y < this.fieldSize.y; y++) {
|
||||||
|
this.field[x].push({tileValue: 0, clicked: false, flagged: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scaleCanvas();
|
||||||
|
this.updateBombs();
|
||||||
|
drawClickAnimation();
|
||||||
|
|
||||||
|
this.initEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
initTime() {
|
||||||
|
this.startTime = new Date().getTime();
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
const duration = (new Date().getTime() - this.startTime) / 1000;
|
||||||
|
const minutes = Math.floor(duration / 60);
|
||||||
|
const seconds = Math.floor(duration % 60);
|
||||||
|
|
||||||
|
this.timeEl.innerText = (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
restartGame() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleCanvas() {
|
||||||
|
let size = window.innerWidth / this.fieldSize.x * .9;
|
||||||
|
|
||||||
|
if (size * this.fieldSize.y > window.innerHeight) {
|
||||||
|
size = window.innerHeight / this.fieldSize.y * .9;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tileSize = {x: size, y: size};
|
||||||
|
|
||||||
|
this.width = this.canvas.width = this.layer1Canvas.width = this.layer2Canvas.width = this.fieldSize.x * size;
|
||||||
|
this.height = this.canvas.height = this.layer1Canvas.height = this.layer2Canvas.height = this.fieldSize.y * size;
|
||||||
|
|
||||||
|
this.statsContainer.style.width = this.width + "px";
|
||||||
|
|
||||||
|
this.applyScaling();
|
||||||
|
|
||||||
|
initBalls();
|
||||||
|
|
||||||
|
if (this.gameOver) {
|
||||||
|
this.gameOverEvent();
|
||||||
|
} else if (this.victoryCheck()) {
|
||||||
|
this.victoryEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testFlagPositions() {
|
||||||
|
for (let x = 0; x < this.fieldSize.x; x++) {
|
||||||
|
for (let y = 0; y < this.fieldSize.y; y++) {
|
||||||
|
if (this.field[x][y].flagged && this.field[x][y].tileValue !== true)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
tileClickEvent(x, y) {
|
||||||
|
if (this.gameOver || this.victoryCheck())
|
||||||
|
return;
|
||||||
|
this.uncoverTile(x, y);
|
||||||
|
if (!this.field[x][y].flagged && this.field[x][y].tileValue === true) {
|
||||||
|
this.gameOver = true;
|
||||||
|
this.gameOverEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tileDoubleClick(x, y) {
|
||||||
|
if (this.gameOver)
|
||||||
|
return;
|
||||||
|
if (this.field[x][y].clicked && !this.field[x][y].flagged && this.countFlaggedBombs(x, y) === this.field[x][y].tileValue) {
|
||||||
|
this.uncoverSurroundings(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tileFlag(x, y) {
|
||||||
|
if (this.gameOver)
|
||||||
|
return;
|
||||||
|
if (this.field[x][y].clicked && !this.field[x][y].flagged)
|
||||||
|
return;
|
||||||
|
this.field[x][y].flagged = !this.field[x][y].flagged;
|
||||||
|
this.field[x][y].clicked = this.field[x][y].flagged;
|
||||||
|
|
||||||
|
x -= this.renderingConfig.offsetX;
|
||||||
|
y -= this.renderingConfig.offsetY;
|
||||||
|
|
||||||
|
const drawX = (x - this.renderingConfig.tiltX) * this.renderingConfig.sizeX;
|
||||||
|
const drawY = (y - this.renderingConfig.tiltY) * this.renderingConfig.sizeY;
|
||||||
|
|
||||||
|
this.ctx.clearRect(drawX, drawY, this.renderingConfig.sizeX, this.renderingConfig.sizeY);
|
||||||
|
this.drawTile(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
uncoverSurroundings(x, y) {
|
||||||
|
const surrounding = this.getSurroundingTiles(x, y);
|
||||||
|
for (let tile in surrounding) {
|
||||||
|
if (surrounding.hasOwnProperty(tile)) {
|
||||||
|
this.uncoverTile(surrounding[tile].x, surrounding[tile].y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uncoverTile(x, y) {
|
||||||
|
if (this.field[x][y].clicked || this.field[x][y].flagged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.field[x][y].clicked = true;
|
||||||
|
this.drawTile(x - this.renderingConfig.offsetX, y - this.renderingConfig.offsetY);
|
||||||
|
if (this.field[x][y].tileValue === true) {
|
||||||
|
this.gameOverEvent();
|
||||||
|
}
|
||||||
|
if (this.field[x][y].tileValue === 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.uncoverSurroundings(x, y);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBombs() {
|
||||||
|
const remainingBombs = this.bombCount - this.countTotalFlags();
|
||||||
|
this.bombsEl.innerText = (remainingBombs < 100 ? "0" : 0) + (remainingBombs < 10 ? "0" : "") + remainingBombs;
|
||||||
|
}
|
||||||
|
|
||||||
|
victoryCheck() {
|
||||||
|
return !play && (this.countClickedTiles() === this.fieldSize.x * this.fieldSize.y - this.bombCount || (this.countTotalFlags() === this.bombCount && this.testFlagPositions()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
victoryEvent() {
|
||||||
|
if (this.victoryCheck()) {
|
||||||
|
animateVictory();
|
||||||
|
play = false;
|
||||||
|
const fontSize = this.tileSize.y * 1.33;
|
||||||
|
animateBackground(0, 0, this.canvas.width, this.canvas.height, 0, .01, new Date().getTime(), 300, {
|
||||||
|
r: 0,
|
||||||
|
g: 0,
|
||||||
|
b: 0,
|
||||||
|
a: 0
|
||||||
|
});
|
||||||
|
animateText(this.layer2, this.renderingConfig, "Victory!", this.fieldSize.x / 2 - .5, this.fieldSize.y / 2 - .5, 0, fontSize, new Date().getTime(), 300, "green", "Roboto");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
index.html
71
index.html
@@ -3,78 +3,23 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Minesweeper.js</title>
|
<title>Minesweeper.js</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="icons/icon.ico">
|
<link rel="shortcut icon" type="image/x-icon" href="icons/icon.ico">
|
||||||
<link rel="icon" type="image/png" href="icons/icon.png" sizes="32x32">
|
<link rel="icon" type="image/png" href="icons/icon.png" sizes="32x32">
|
||||||
<style>
|
|
||||||
html {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#minesweeper-overlay, #minesweeper-overlay2 {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#game-container {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 2.5%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
#game-stats {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(2.5vh / 2);
|
|
||||||
left: 50%;
|
|
||||||
height: 5vh;
|
|
||||||
width: 1592px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-container {
|
|
||||||
float: left;
|
|
||||||
background-color: black;
|
|
||||||
border-radius: 10px;
|
|
||||||
color: red;
|
|
||||||
font-size: 4vh;
|
|
||||||
font-family: "SF Digital Readout", Roboto, Arial, sans-serif;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-container.right {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
#time, #bombs {
|
|
||||||
margin: 0 15px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0">
|
<body>
|
||||||
|
|
||||||
<div id="game-stats">
|
<div class="start-background">
|
||||||
<div class="stat-container">
|
<div class="start-container">
|
||||||
<span id="bombs">
|
<h1>Minesweeper.js</h1>
|
||||||
000
|
<h3>A sweet, tasty, little Minesweeper game written in JavaScript with HTML5 <canvas> Element</h3>
|
||||||
</span>
|
<button id="startgame">Play!</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat-container right">
|
|
||||||
<span id="time">
|
|
||||||
00:00
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="game-container">
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript" src="animations/click.js"></script>
|
<script type="text/javascript" src="animations/click.js"></script>
|
||||||
<script type="text/javascript" src="animations/victory.js"></script>
|
<script type="text/javascript" src="animations/victory.js"></script>
|
||||||
|
<script type="text/javascript" src="game.js"></script>
|
||||||
<script type="text/javascript" src="minesweeper.js"></script>
|
<script type="text/javascript" src="minesweeper.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
534
minesweeper.js
534
minesweeper.js
@@ -1,26 +1,3 @@
|
|||||||
const canvas = document.getElementById('minesweeper-game');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const statsContainer = document.getElementById('game-stats');
|
|
||||||
const timeEl = document.getElementById('time');
|
|
||||||
const bombsEl = document.getElementById('bombs');
|
|
||||||
|
|
||||||
const fieldSize = {x: 16, y: 12};
|
|
||||||
let tileSize;
|
|
||||||
const bombCount = 25;
|
|
||||||
const field = [];
|
|
||||||
let gameOver = false;
|
|
||||||
const scaleFactor = .5;
|
|
||||||
let isFirstClick = true;
|
|
||||||
|
|
||||||
let windowX = 0;
|
|
||||||
let windowY = 0;
|
|
||||||
let zoomFactor = 1;
|
|
||||||
|
|
||||||
let renderingConfig;
|
|
||||||
|
|
||||||
let startTime = 0;
|
|
||||||
let timer;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines all possible colors for the tile numbers
|
* Defines all possible colors for the tile numbers
|
||||||
* @type {{ 1: string, 2: string, 3: string, 4: string, 6: string }}
|
* @type {{ 1: string, 2: string, 3: string, 4: string, 6: string }}
|
||||||
@@ -34,8 +11,6 @@ const colors = {
|
|||||||
6: "pink"
|
6: "pink"
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.scale(canvas.width / fieldSize.x * scaleFactor, canvas.height / fieldSize.y * scaleFactor);
|
|
||||||
|
|
||||||
function animateBackground(x, y, width, height, curOpacity, finalOpacity, startTime, duration, color) {
|
function animateBackground(x, y, width, height, curOpacity, finalOpacity, startTime, duration, color) {
|
||||||
const time = (new Date()).getTime() - startTime;
|
const time = (new Date()).getTime() - startTime;
|
||||||
|
|
||||||
@@ -60,7 +35,7 @@ function animateBackground(x, y, width, height, curOpacity, finalOpacity, startT
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function animateTile(x, y, curWidth, curHeight, finalWidth, finalHeight, curRadius, finalRadius, startTime, duration, color) {
|
function animateTile(ctx, x, y, curWidth, curHeight, finalWidth, finalHeight, curRadius, finalRadius, startTime, duration, color) {
|
||||||
const time = (new Date()).getTime() - startTime;
|
const time = (new Date()).getTime() - startTime;
|
||||||
|
|
||||||
if (curWidth === finalWidth && curHeight === finalHeight && curRadius === finalRadius)
|
if (curWidth === finalWidth && curHeight === finalHeight && curRadius === finalRadius)
|
||||||
@@ -88,16 +63,13 @@ function animateTile(x, y, curWidth, curHeight, finalWidth, finalHeight, curRadi
|
|||||||
drawRoundedRect(ctx, x + (finalWidth - curWidth) / 2, y + (finalHeight - curHeight) / 2, curWidth, curHeight, curRadius, color);
|
drawRoundedRect(ctx, x + (finalWidth - curWidth) / 2, y + (finalHeight - curHeight) / 2, curWidth, curHeight, curRadius, color);
|
||||||
|
|
||||||
requestAnimFrame(() => {
|
requestAnimFrame(() => {
|
||||||
animateTile(x, y, curWidth, curHeight, finalWidth, finalHeight, curRadius, finalRadius, startTime, duration, color);
|
animateTile(ctx, x, y, curWidth, curHeight, finalWidth, finalHeight, curRadius, finalRadius, startTime, duration, color);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function animateText(text, x, y, curFontSize, finalFontSize, startTime, duration, color, font, context) {
|
function animateText(ctx, renderingConfig, text, x, y, curFontSize, finalFontSize, startTime, duration, color, font) {
|
||||||
const time = (new Date()).getTime() - startTime;
|
const time = (new Date()).getTime() - startTime;
|
||||||
|
|
||||||
if (context === undefined)
|
|
||||||
context = ctx;
|
|
||||||
|
|
||||||
if (curFontSize === finalFontSize)
|
if (curFontSize === finalFontSize)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -115,82 +87,16 @@ function animateText(text, x, y, curFontSize, finalFontSize, startTime, duration
|
|||||||
if (font === undefined)
|
if (font === undefined)
|
||||||
font = "Roboto";
|
font = "Roboto";
|
||||||
|
|
||||||
context.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
context.font = "bold " + curFontSize + "px " + font;
|
ctx.font = "bold " + curFontSize + "px " + font;
|
||||||
context.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
context.fillText(text, textDrawX, textDrawY);
|
ctx.fillText(text, textDrawX, textDrawY);
|
||||||
|
|
||||||
requestAnimFrame(function () {
|
requestAnimFrame(function () {
|
||||||
animateText(text, x, y, curFontSize, finalFontSize, startTime, duration, color, font, context);
|
animateText(ctx, renderingConfig, text, x, y, curFontSize, finalFontSize, startTime, duration, color, font);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyScaling() {
|
|
||||||
renderingConfig = calcScaling();
|
|
||||||
|
|
||||||
drawGrid(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function calcScaling(field = fieldSize, tile = tileSize, zoom = zoomFactor) {
|
|
||||||
const width = Math.ceil(field.x * zoom) + 1;
|
|
||||||
const height = Math.ceil(field.y * zoom) + 1;
|
|
||||||
|
|
||||||
const offsetX = Math.floor(windowX * field.x);
|
|
||||||
const offsetY = Math.floor(windowY * field.y);
|
|
||||||
|
|
||||||
const tiltX = windowX * field.x - offsetX;
|
|
||||||
const tiltY = windowY * field.y - offsetY;
|
|
||||||
|
|
||||||
const sizeX = tile.x / zoom;
|
|
||||||
const sizeY = tile.y / zoom;
|
|
||||||
|
|
||||||
return {
|
|
||||||
width, height, offsetX, offsetY, tiltX, tiltY, sizeX, sizeY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function countBombs(x, y) {
|
|
||||||
const tiles = getSurroundingTiles(x, y);
|
|
||||||
return tiles.count(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 countFlaggedBombs(x, y) {
|
|
||||||
const tiles = getSurroundingTiles(x, y);
|
|
||||||
return tiles.countFlagged(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function countTotalFlags() {
|
|
||||||
let count = 0;
|
|
||||||
for(let x = 0; x < fieldSize.x; x++) {
|
|
||||||
for(let y = 0; y < fieldSize.y; y++) {
|
|
||||||
if(field[x][y].flagged)
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawGrid(animations = true) {
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
for (let x = 0; x < renderingConfig.width; x++) {
|
|
||||||
for (let y = 0; y < renderingConfig.height; y++) {
|
|
||||||
drawTile(x, y, animations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawRoundedRect(context, x, y, w, h, r, color) {
|
function drawRoundedRect(context, x, y, w, h, r, color) {
|
||||||
context.fillStyle = color;
|
context.fillStyle = color;
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
@@ -205,283 +111,12 @@ function drawRoundedRect(context, x, y, w, h, r, color) {
|
|||||||
context.fill();
|
context.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawTile(x, y, animations = true) {
|
|
||||||
const virtualX = renderingConfig.offsetX + x;
|
|
||||||
const virtualY = renderingConfig.offsetY + y;
|
|
||||||
|
|
||||||
if(virtualX >= fieldSize.x || virtualY >= fieldSize.y)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const content = field[virtualX][virtualY];
|
|
||||||
|
|
||||||
const width = .8 * renderingConfig.sizeX;
|
|
||||||
const height = .8 * renderingConfig.sizeY;
|
|
||||||
const drawX = (x + .1 - renderingConfig.tiltX) * renderingConfig.sizeX;
|
|
||||||
const drawY = (y + .1 - renderingConfig.tiltY) * renderingConfig.sizeY;
|
|
||||||
const radius = renderingConfig.sizeX * .1;
|
|
||||||
|
|
||||||
let color = getColor(virtualX, virtualY);
|
|
||||||
const fontSize = renderingConfig.sizeY * .5;
|
|
||||||
let fontFamily = "Roboto";
|
|
||||||
let textColor = "white";
|
|
||||||
let text = "";
|
|
||||||
|
|
||||||
ctx.textAlign = "center";
|
|
||||||
let duration = 0;
|
|
||||||
if (animations)
|
|
||||||
duration = 150;
|
|
||||||
|
|
||||||
if (!content.flagged && content.clicked) {
|
|
||||||
color = "#ddd";
|
|
||||||
if(content.tileValue === true) {
|
|
||||||
text = "";
|
|
||||||
fontFamily = "FontAwesome";
|
|
||||||
textColor = "#aa2211";
|
|
||||||
color = "#333";
|
|
||||||
} else if (content.tileValue !== 0) {
|
|
||||||
text = content.tileValue;
|
|
||||||
textColor = colors[content.tileValue];
|
|
||||||
}
|
|
||||||
} else if (content.flagged) {
|
|
||||||
color = "#ff0000";
|
|
||||||
fontFamily = "FontAwesome";
|
|
||||||
text = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
animateTile(drawX, drawY, 0, 0, width, height, 0, radius, new Date().getTime(), duration, color);
|
|
||||||
if(text !== "") {
|
|
||||||
animateText(text, x, y, 0, fontSize, new Date().getTime(), duration, textColor, fontFamily, ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function easeInOutCubic(t, b, c, d) {
|
function easeInOutCubic(t, b, c, d) {
|
||||||
t /= d;
|
t /= d;
|
||||||
t--;
|
t--;
|
||||||
return c * (Math.pow(t, 3) + 1) + b;
|
return c * (Math.pow(t, 3) + 1) + b;
|
||||||
}
|
}
|
||||||
|
|
||||||
function gameOverEvent() {
|
|
||||||
play = false;
|
|
||||||
animateBackground(0, 0, canvas.width, canvas.height, 0, .75, new Date().getTime(), 200, {r: 0, g: 0, b: 0, a: 0});
|
|
||||||
animateText("Game Over", fieldSize.x / 2 - .5, fieldSize.y / 2 - .5, 0, tileSize.y * 1.33, new Date().getTime(), 200, "orange", "Roboto", overlay2Ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getColor(x, y) {
|
|
||||||
x++;
|
|
||||||
y++;
|
|
||||||
const pos = x * y;
|
|
||||||
const limit = fieldSize.x * fieldSize.y;
|
|
||||||
|
|
||||||
let percentage = pos / limit * 360;
|
|
||||||
|
|
||||||
return `hsl(${percentage},100%,50%)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPosition(e) {
|
|
||||||
const x = (e.x - (window.innerWidth - W) / 2) / W * zoomFactor + windowX;
|
|
||||||
const y = (e.y - (window.innerHeight - H) / 2) / H * zoomFactor + windowY;
|
|
||||||
const fieldX = Math.floor(x * fieldSize.x);
|
|
||||||
const fieldY = Math.floor(y * fieldSize.y);
|
|
||||||
|
|
||||||
return {x: fieldX, y: fieldY};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
updateBombs();
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTime() {
|
|
||||||
startTime = new Date().getTime();
|
|
||||||
timer = setInterval(() => {
|
|
||||||
const duration = (new Date().getTime() - startTime) / 1000;
|
|
||||||
const minutes = Math.floor(duration / 60);
|
|
||||||
const seconds = Math.floor(duration % 60);
|
|
||||||
|
|
||||||
timeEl.innerText = (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function scaleCanvas() {
|
|
||||||
let size = window.innerWidth / fieldSize.x * .9;
|
|
||||||
|
|
||||||
if(size * fieldSize.y > window.innerHeight) {
|
|
||||||
size = window.innerHeight / fieldSize.y * .9;
|
|
||||||
}
|
|
||||||
|
|
||||||
tileSize = {x: size, y: size};
|
|
||||||
|
|
||||||
W = fieldSize.x * size;
|
|
||||||
H = fieldSize.y * size;
|
|
||||||
|
|
||||||
canvas.width = W;
|
|
||||||
canvas.height = H;
|
|
||||||
overlayCanvas.width = W;
|
|
||||||
overlayCanvas.height = H;
|
|
||||||
overlay2Canvas.width = W;
|
|
||||||
overlay2Canvas.height = H;
|
|
||||||
|
|
||||||
statsContainer.style.width = W + "px";
|
|
||||||
|
|
||||||
initBalls();
|
|
||||||
|
|
||||||
applyScaling();
|
|
||||||
|
|
||||||
if (gameOver) {
|
|
||||||
gameOverEvent();
|
|
||||||
} else if (victoryCheck()) {
|
|
||||||
victoryEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function testFlagPositions() {
|
|
||||||
for(let x = 0; x < fieldSize.x; x++) {
|
|
||||||
for(let y = 0; y < fieldSize.y; y++) {
|
|
||||||
if(field[x][y].flagged && field[x][y].tileValue !== true)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tileClickEvent(x, y) {
|
|
||||||
if (gameOver || victoryCheck())
|
|
||||||
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;
|
|
||||||
|
|
||||||
x -= renderingConfig.offsetX;
|
|
||||||
y -= renderingConfig.offsetY;
|
|
||||||
|
|
||||||
const drawX = (x - renderingConfig.tiltX) * renderingConfig.sizeX;
|
|
||||||
const drawY = (y - renderingConfig.tiltY) * renderingConfig.sizeY;
|
|
||||||
|
|
||||||
ctx.clearRect(drawX, drawY, renderingConfig.sizeX, renderingConfig.sizeY);
|
|
||||||
drawTile(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function uncoverTile(x, y) {
|
|
||||||
if (field[x][y].clicked || field[x][y].flagged) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
field[x][y].clicked = true;
|
|
||||||
drawTile(x - renderingConfig.offsetX, y - renderingConfig.offsetY);
|
|
||||||
if (field[x][y].tileValue === true) {
|
|
||||||
gameOverEvent();
|
|
||||||
}
|
|
||||||
if (field[x][y].tileValue === 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
uncoverSurroundings(x, y);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateBombs() {
|
|
||||||
const remainingBombs = bombCount - countTotalFlags();
|
|
||||||
bombsEl.innerText = (remainingBombs < 100 ? "0" : 0) + (remainingBombs < 10 ? "0" : "") + remainingBombs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function victoryCheck() {
|
|
||||||
return !play && (countClickedTiles() === fieldSize.x * fieldSize.y - bombCount || (countTotalFlags() === bombCount && testFlagPositions()));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function victoryEvent() {
|
|
||||||
if(victoryCheck()) {
|
|
||||||
animateVictory();
|
|
||||||
play = false;
|
|
||||||
const fontSize = tileSize.y * 1.33;
|
|
||||||
animateBackground(0, 0, canvas.width, canvas.height, 0, .01, new Date().getTime(), 300, {r: 0, g: 0, b: 0, a: 0});
|
|
||||||
animateText("Victory!", fieldSize.x / 2 - .5, fieldSize.y / 2 - .5, 0, fontSize, new Date().getTime(), 300, "green", "Roboto", overlay2Ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.prototype.count = function (val) {
|
Object.prototype.count = function (val) {
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
for (let el in this) {
|
for (let el in this) {
|
||||||
@@ -506,140 +141,27 @@ Object.prototype.countFlagged = function (val) {
|
|||||||
return counter;
|
return counter;
|
||||||
};
|
};
|
||||||
|
|
||||||
overlay2Canvas.addEventListener("click", (e) => {
|
String.prototype.toDOM=function(){
|
||||||
if(isDragging)
|
let d=document
|
||||||
return;
|
,i
|
||||||
|
,a=d.createElement("div")
|
||||||
|
,b=d.createDocumentFragment();
|
||||||
|
a.innerHTML=this;
|
||||||
|
while(i=a.firstChild)b.appendChild(i);
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
|
||||||
const pos = getPosition(e);
|
let game;
|
||||||
|
|
||||||
if (isFirstClick) {
|
const startContainer = document.getElementsByClassName('start-container')[0];
|
||||||
initBombs(pos.x, pos.y);
|
const startBackground = document.getElementsByClassName('start-background')[0];
|
||||||
initTime();
|
const startButton = document.getElementById('startgame');
|
||||||
isFirstClick = false;
|
startButton.addEventListener('click', () => {
|
||||||
}
|
startContainer.classList.add('slideDown');
|
||||||
|
startBackground.classList.add('transparent');
|
||||||
tileClickEvent(pos.x, pos.y);
|
game = new Game();
|
||||||
|
game.initGame();
|
||||||
victoryEvent();
|
|
||||||
|
|
||||||
clicked(e);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
overlay2Canvas.addEventListener("dblclick", (e) => {
|
// const game = new Game();
|
||||||
if(isDragging)
|
// game.initGame();
|
||||||
return;
|
|
||||||
|
|
||||||
const pos = getPosition(e);
|
|
||||||
|
|
||||||
tileDoubleClick(pos.x, pos.y);
|
|
||||||
|
|
||||||
victoryEvent();
|
|
||||||
});
|
|
||||||
|
|
||||||
overlay2Canvas.addEventListener("contextmenu", (e) => {
|
|
||||||
if(isDragging)
|
|
||||||
return;
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const pos = getPosition(e);
|
|
||||||
|
|
||||||
tileFlag(pos.x, pos.y);
|
|
||||||
|
|
||||||
updateBombs();
|
|
||||||
|
|
||||||
victoryEvent();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("keyup", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const changeRate = .05;
|
|
||||||
|
|
||||||
let newZoomFactor = zoomFactor;
|
|
||||||
let newWindowX = windowX;
|
|
||||||
let newWindowY = windowY;
|
|
||||||
|
|
||||||
if (e.code === "BracketRight") {
|
|
||||||
newZoomFactor -= changeRate;
|
|
||||||
} else if (e.code === "Slash") {
|
|
||||||
newZoomFactor += changeRate;
|
|
||||||
} else if (e.code === "ArrowLeft") {
|
|
||||||
newWindowX -= changeRate;
|
|
||||||
} else if (e.code === "ArrowRight") {
|
|
||||||
newWindowX += changeRate;
|
|
||||||
} else if (e.code === "ArrowUp") {
|
|
||||||
newWindowY -= changeRate;
|
|
||||||
} else if (e.code === "ArrowDown") {
|
|
||||||
newWindowY += changeRate;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
newZoomFactor = Math.min(newZoomFactor, 1);
|
|
||||||
newZoomFactor = Math.max(newZoomFactor, .1);
|
|
||||||
|
|
||||||
newWindowX = Math.min(newWindowX, 1 - newZoomFactor);
|
|
||||||
newWindowY = Math.min(newWindowY, 1 - newZoomFactor);
|
|
||||||
newWindowX = Math.max(newWindowX, 0);
|
|
||||||
newWindowY = Math.max(newWindowY, 0);
|
|
||||||
|
|
||||||
if(newZoomFactor !== zoomFactor || newWindowX !== windowX || newWindowY !== windowY) {
|
|
||||||
zoomFactor = newZoomFactor;
|
|
||||||
windowX = newWindowX;
|
|
||||||
windowY = newWindowY;
|
|
||||||
applyScaling();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let startClientX = 0;
|
|
||||||
let startClientY = 0;
|
|
||||||
let startWindowX = 0;
|
|
||||||
let startWindowY = 0;
|
|
||||||
|
|
||||||
let hasClicked = false;
|
|
||||||
let isDragging = false;
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", (e) => {
|
|
||||||
if(e.button === 0) {
|
|
||||||
hasClicked = true;
|
|
||||||
startClientX = e.clientX;
|
|
||||||
startClientY = e.clientY;
|
|
||||||
startWindowX = windowX;
|
|
||||||
startWindowY = windowY;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("mouseup", () => {
|
|
||||||
hasClicked = false;
|
|
||||||
if(isDragging) {
|
|
||||||
setTimeout(() => {
|
|
||||||
isDragging = false;
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("mousemove", (e) => {
|
|
||||||
if(hasClicked) {
|
|
||||||
isDragging = true;
|
|
||||||
|
|
||||||
const deltaX = e.clientX - startClientX;
|
|
||||||
const deltaY = e.clientY - startClientY;
|
|
||||||
|
|
||||||
windowX = startWindowX - deltaX / W;
|
|
||||||
windowY = startWindowY - deltaY / H;
|
|
||||||
|
|
||||||
windowX = Math.min(windowX, 1 - zoomFactor);
|
|
||||||
windowY = Math.min(windowY, 1 - zoomFactor);
|
|
||||||
windowX = Math.max(windowX, 0);
|
|
||||||
windowY = Math.max(windowY, 0);
|
|
||||||
|
|
||||||
applyScaling();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
scaleCanvas();
|
|
||||||
});
|
|
||||||
|
|
||||||
initGame();
|
|
Reference in New Issue
Block a user