From 5b0265c1ab4e458fb90f055276296db6b7e01442 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 11 May 2018 12:27:39 +0200 Subject: [PATCH 1/4] Create class "Game" This class can now have multiple instances (currently not at the same time) --- game.js | 523 +++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 5 +- minesweeper.js | 519 +----------------------------------------------- 3 files changed, 535 insertions(+), 512 deletions(-) create mode 100644 game.js diff --git a/game.js b/game.js new file mode 100644 index 0000000..6549ad4 --- /dev/null +++ b/game.js @@ -0,0 +1,523 @@ +class Game { + // tileSize; + // renderingConfig; + // timer; + + constructor() { + this.canvas = document.getElementById('minesweeper-game'); + this.ctx = this.canvas.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); + + this.initGame(); + } + + 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(overlay2Ctx, 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() { + overlay2Canvas.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); + }); + + overlay2Canvas.addEventListener("dblclick", (e) => { + if (this.isDragging) + return; + + const pos = this.getPosition(e); + + this.tileDoubleClick(pos.x, pos.y); + + this.victoryEvent(); + }); + + overlay2Canvas.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(); + + 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.fieldSize.x * size; + this.height = this.fieldSize.y * size; + + this.canvas.width = this.width; + this.canvas.height = this.height; + overlayCanvas.width = this.width; + overlayCanvas.height = this.height; + overlay2Canvas.width = this.width; + overlay2Canvas.height = this.height; + + this.statsContainer.style.width = this.width + "px"; + + initBalls(); + + this.applyScaling(); + + 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(overlay2Ctx, this.renderingConfig, "Victory!", this.fieldSize.x / 2 - .5, this.fieldSize.y / 2 - .5, 0, fontSize, new Date().getTime(), 300, "green", "Roboto"); + } + } + +} \ No newline at end of file diff --git a/index.html b/index.html index 8b3c844..919a017 100644 --- a/index.html +++ b/index.html @@ -3,8 +3,8 @@ Minesweeper.js - - + + +
+
+

Minesweeper.js

+

A sweet, tasty, little Minesweeper game written in JavaScript with HTML5 <canvas> Element

+ +
+
+ diff --git a/minesweeper.js b/minesweeper.js index 60235a8..c514cc0 100644 --- a/minesweeper.js +++ b/minesweeper.js @@ -151,6 +151,17 @@ String.prototype.toDOM=function(){ return b; }; +let game; -const game = new Game(); -game.initGame(); \ No newline at end of file +const startContainer = document.getElementsByClassName('start-container')[0]; +const startBackground = document.getElementsByClassName('start-background')[0]; +const startButton = document.getElementById('startgame'); +startButton.addEventListener('click', () => { + startContainer.classList.add('slideDown'); + startBackground.classList.add('transparent'); + game = new Game(); + game.initGame(); +}); + +// const game = new Game(); +// game.initGame(); \ No newline at end of file From 3269e2ef203bf5bbe957268b289e197467926401 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 11 May 2018 14:20:19 +0200 Subject: [PATCH 4/4] Put styles into seperate file --- css/style.css | 136 +++++++++++++++++++++++++++++++++++++++++++++++ index.html | 144 ++------------------------------------------------ 2 files changed, 140 insertions(+), 140 deletions(-) create mode 100644 css/style.css diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..10acc9b --- /dev/null +++ b/css/style.css @@ -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; + } +} \ No newline at end of file diff --git a/index.html b/index.html index d853f99..2154f23 100644 --- a/index.html +++ b/index.html @@ -3,147 +3,11 @@ Minesweeper.js - - - + + + - +