Upload Source

This commit is contained in:
Felix Albrigtsen 2020-06-19 15:29:31 +02:00 committed by GitHub
parent c059159711
commit e04ff053b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 33858 additions and 0 deletions

148
minesweeper/ai.js Normal file
View File

@ -0,0 +1,148 @@
//Felix Albrigtsen 2019
//Auto logic for minesweeper, not required for actual gameplay.
var botTimer = undefined;
function autoMove() {
//Automatically chose the statistically best move to do. The machine only has the same info as the player.
//When it knows nothing: Select by random.
//For each untouched cell(not marked and not revealed), generate the probability of the cell being a bomb.
//For a cell with 100% chance of being a bomb, instantly mark it.
//For a cell with 0% chance of being a bomb, reveal it.
//If no cells have 100% or 0% chance, reveal the one with the highest probability
//The probability of a cell is calculated by taking the average of each revealed neighbor cell's (neighbor mines - marked neighbors).
// Unless one or more of its neighbors gives it a 0% or 100%.
timer_hasCheated = true;
if (finished) { clearInterval(botTimer); return; }
var probabilityBoard = [];
for(var index = 0; index < board.length; index++) {
probabilityBoard.push(calculateProbability(index));
}
var lowestChance = [1, -1]; // [value, index]
for (var i = 0; i < board.length; i++) {
if (probabilityBoard[i] == 1) {
interact(i%cols, (i-(i%cols))/cols, true);
return;
}
if (probabilityBoard[i] == 0) {
interact(i%cols, (i-(i%cols))/cols, false);
return;
}
if ((probabilityBoard[i] < lowestChance[0]) && (probabilityBoard[i] != -1)) {
lowestChance = [probabilityBoard[i], i];
}
}
var lowestChanceIndex = lowestChance[1];
var x = lowestChanceIndex%cols;
var y = (lowestChanceIndex - x) / cols;
interact(x, y, false);
console.log("Choosing " + lowestChanceIndex + " with a bomb chance of " + lowestChance[0].toString());
}
function calculateProbability(index) {
if (board[index].revealed) { //Can't be a bomb, don't even consider it
return -1;
}
if (board[index].marked) { //MUST be a bomb, ignore it
return 2;
}
var neighborIndexes = neighborIndexOffsets.map(x => x + index);
var usefulNeighbors = [];
for (var i = 0; i < neighborIndexes.length; i++) {
if ((neighborIndexes[i] < board.length) && (neighborIndexes[i] >= 0) && (abs((index%cols) - board[neighborIndexes[i]].x) <= 1)) {
if (board[neighborIndexes[i]].revealed) {
usefulNeighbors.push(neighborIndexes[i]);
}
}
}
if (usefulNeighbors.length == 0) { //If we dont know anything, use the random chance value
return ((mineCount-markedBombs) / (rows*cols));
}
var neighborChances = [];
for (var i = 0; i < usefulNeighbors.length; i++) {
var markedNeighbors = countMarkedNeighbors(usefulNeighbors[i]);
var missingMarks = board[usefulNeighbors[i]].neighbors - markedNeighbors;
var untouchedNeighbors = countUntouchedNeighbors(usefulNeighbors[i]);
if (missingMarks == untouchedNeighbors + markedNeighbors) {
return 1; //100 %
}
if (missingMarks == 0) {
return 0; //0%, all mines are marked
}
neighborChances.push(missingMarks / untouchedNeighbors);
}
//return average(neighborChances);
return maxValue(neighborChances);
}
function countMarkedNeighbors(index) {
var neighborIndexes = neighborIndexOffsets.map(x => x + index);
var markedNeighbors = 0;
for (var i = 0; i < neighborIndexes.length; i++) {
if ((neighborIndexes[i] >= 0) && (neighborIndexes[i] < board.length) && (abs((index%cols) - board[neighborIndexes[i]].x) <= 1)) {
if (board[neighborIndexes[i]].marked) {
markedNeighbors++;
}
}
}
return markedNeighbors;
}
function countUntouchedNeighbors(index) {
var neighborIndexes = neighborIndexOffsets.map(x => x + index);
var untouchedNeighbors = 0;
for (var i = 0; i < neighborIndexes.length; i++) {
if ((neighborIndexes[i] >= 0) && (neighborIndexes[i] < board.length) && (abs((index%cols) - board[neighborIndexes[i]].x) <= 1)) {
if ((!board[neighborIndexes[i]].marked) && (!board[neighborIndexes[i]].revealed)) {
untouchedNeighbors++;
}
}
}
return untouchedNeighbors;
}
function average(list) {
var sum = 0;
var len= list.length;
for (var i = 0; i < len; i++) {
sum += list[i];
}
return sum/len;
}
function maxValue(list) {
if (list.length == 0) { return -1; }
var largest = list[0];
for (var i = 1; i < list.length; i++) {
if (list[i] > largest) {
largest = list[i];
}
}
return largest;
}

53
minesweeper/cell.js Normal file
View File

@ -0,0 +1,53 @@
//Felix Albrigtsen 2019
//Cell object and methods for minesweeper.
class Cell {
constructor(x_, y_) {
this.mine = false;
this.revealed = false;
this.marked = false;
this.x = x_;
this.y = y_;
this.boardIndex = (this.y*cols) + this.x;
}
mark() {
this.marked = !this.marked;
}
countNeighbors() {
var neighborIndexes = neighborIndexOffsets.map(x => x + this.boardIndex);
var neighborMines = 0;
for (var i = 0; i < neighborIndexes.length; i++) {
if (((neighborIndexes[i] >= 0) && (neighborIndexes[i] < board.length)) && (abs(this.x - board[neighborIndexes[i]].x) <= 1)) {
if (board[neighborIndexes[i]].mine) { neighborMines++; }
}
}
this.neighbors = neighborMines;
}
reveal() {
if (this.mine) {
//Game over, instant loss
gameover = true;
for (var i = 0; i < board.length; i++) {
if (!board[i].mine) { board[i].reveal(); }
}
} else {
this.revealed = true;
}
if (this.neighbors == 0) {
var neighborIndexes = neighborIndexOffsets.map(x => x + this.boardIndex);
for (var i = 0; i < neighborIndexes.length; i++) {
if (((neighborIndexes[i] >= 0) && (neighborIndexes[i] < board.length)) && (abs(this.x - board[neighborIndexes[i]].x) <= 1)) {
if (!board[neighborIndexes[i]].revealed) {
board[neighborIndexes[i]].reveal();
}
}
}
}
}
}

BIN
minesweeper/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

195
minesweeper/index.html Normal file
View File

@ -0,0 +1,195 @@
<!doctype html>
<html>
<head>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-153522520-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-153522520-1');
</script>
<script language="javascript" type="text/javascript" src="libraries/p5.js"></script>
<script language="javascript" type="text/javascript" src="cell.js"></script>
<script language="javascript" type="text/javascript" src="ai.js"></script>
<script language="javascript" type="text/javascript" src="timer.js"></script>
<meta charset="utf-8">
<title>Minesweeper</title>
<link href="style.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="gameDiv">
<h2 id="titleHead">Minesweeper.no</h2>
<span><button id="btn_new_0">Easy</button>
<button id="btn_new_1">Medium</button>
<button id="btn_new_2">Hard</button>
<button id="btn_new_3">Extreme</button></span>
<div id="canvasDiv"></div>
</div>
<div id="leftPane">
<button id="btn_start" class="paneBtn">Start Auto-Solve</button>
<button id="btn_stop" class="paneBtn">Stop Auto-Solve</button>
<button id="btn_step" class="paneBtn">Step Auto-Solve</button>
<br><br><br>
<button id="btn_help" class="paneBtn">Help</button>
<br><br><br>
<h2 id="timerText"></h2>
<br><br>
<p>Felix Albrigtsen</p>
</div>
<script language="javascript" type="text/javascript" src="sketch.js"></script>
<div id="helpOverlay">
<h1>Instructions</h1>
<br>
<p class="helpText">The game of minesweeper is a logical puzzle game with an element of chance.<br>
Your goal is to "open" all cells, either by revealing them (clicking) or marking them (shift + click).<br>
Some squares are mines/bombs, but most are safe to click. If you accidentally click a mine, the game is over.
When you fail, all cells will be opened, and you must restart the game(See below).<br>
If you click a square where no "neighbors", meaning the 8 squares directly touching it, are bombs, it will be blank
and light gray. It will also reveal all it's neighbors. If a square has one or more neighbors that ARE bombs, they will
display the number of explosive neighbors. You must use these numbers to find out what sqaures are safe.
When you have successfully categorized all the squares as mines or safe, they will be marked in green, and you won.<br>
</p>
<br>
<h2>Inputs / Controls &nbsp &nbsp Difficulties</h2>
<div id="tableDiv">
<table class="helpTable">
<thead>
<th>Function</th>
<th>Button</th>
</thead>
<tr>
<td>Reveal</td>
<td>W or Click</td>
</tr>
<tr>
<td>Mark as unsafe</td>
<td>Q or Shift + click </td>
</tr>
<tr>
<td>Restart game</td>
<td>R</td>
</tr>
<tr>
<td>Automatic / best move</td>
<td>A</td>
</tr>
<tr>
<td>Restart and change difficulty</td>
<td>Buttons above the play field</td>
</tr>
</table>
<table class="helpTable">
<thead>
<th>Difficulty</th>
<th>Rows</th>
<th>Columns</th>
<th>Mines</th>
<th>Mine percentage</th>
</thead>
<tr>
<td>Easy</td>
<td>10</td>
<td>10</td>
<td>10</td>
<td>10%</td>
</tr>
<tr>
<td>Medium</td>
<td>16</td>
<td>16</td>
<td>38</td>
<td>15%</td>
</tr>
<tr>
<td>Hard</td>
<td>16</td>
<td>26</td>
<td>83</td>
<td>20%</td>
</tr>
<tr>
<td>Extreme</td>
<td>30</td>
<td>36</td>
<td>216</td>
<td>20%</td>
</tr>
</table>
</div>
<br>
<h2>Auto-Move info</h2>
<p class="helpText">
This version of Minesweeper is equipped with an auto-move function. This "AI" software does not have access to any more information than the player does.
It can only see what cells are revealed, and the numbers inside the revealed cells. Using this data, it will try calculate the probability of each cell
being a mine, and mark/reveal it accordingly. It can make mistakes, as minesweeper also is a game of chance. Remember: Using the auto-move function will stop the timer!
</p>
</div>
<div id="bottomPadding">
<br>
</div>
<script>
document.getElementById("btn_new_0").onclick = function() {
if (refreshPage) {
window.location.href = window.location.pathname + "?d=0";
} else {
setDifficulty(0);
}
}
document.getElementById("btn_new_1").onclick = function() {
if (refreshPage) {
window.location.href = window.location.pathname + "?d=1";
} else {
setDifficulty(1);
}
}
document.getElementById("btn_new_2").onclick = function() {
if (refreshPage) {
window.location.href = window.location.pathname + "?d=2";
} else {
setDifficulty(2);
}
}
document.getElementById("btn_new_3").onclick = function() {
if (refreshPage) {
window.location.href = window.location.pathname + "?d=3";
} else {
setDifficulty(3);
}
}
document.getElementById("btn_start").onclick = function() {
if (botTimer != undefined) { return; }
botTimer = setInterval(autoMove, 700);
}
document.getElementById("btn_stop").onclick = function() {
clearInterval(botTimer);
botTimer = undefined;
}
document.getElementById("btn_step").onclick = function() {
autoMove();
}
document.getElementById("btn_help").onclick = function() {
document.getElementById("helpOverlay").style.display = "block";
}
document.getElementById("helpOverlay").onclick = function() {
document.getElementById("helpOverlay").style.display = "none";
}
</script>
</body>
</html>

33029
minesweeper/libraries/p5.js Normal file

File diff suppressed because it is too large Load Diff

239
minesweeper/sketch.js Normal file
View File

@ -0,0 +1,239 @@
//Felix Albrigtsen 2019
//Main code for minesweeper, dependent on cell.js, and partly on ai.js.
//Display and board settings
var cellSize = 23;
var padding = 3;
var rows = 16;
var cols = 16;
var mineCount = 30;
var canvWidth = (cols * (cellSize + padding)) + 1.5*padding;
var canvHeight = (rows * (cellSize + padding)) + 1.5*padding;
//Game state variables
var finished = false;
var gameover = false;
var markedBombs = 0;
var board = [];
//Calculate these values once, instead of each time
var neighborIndexOffsets = [-cols-1, -cols, -cols+1, -1, 1, cols-1, cols, cols+1];
var refreshPage = true; //Refresh when changing difficulty
var refreshPage_reset = false; // Do not refresh when hitting R, it takes too long
function setDifficulty(level) {
switch (level) {
case 0:
rows = 10;
cols = 10;
padding = 3;
mineCount = 10;
break;
case 1:
rows = 16;
cols = 16;
padding = 3;
mineCount = 38;
break;
case 2:
rows = 20;
cols = 22;
padding = 2;
mineCount = 88;
break;
case 3:
rows = 30;
cols = 32;
padding = 2;
mineCount = 192;
break;
default:
rows = 16;
cols = 16;
padding = 3;
mineCount = 38;
}
//CanvWidth = cols * (padding + cellsize)
//cellsize = canvWidth / cols - padding
//var boardMaxHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0) * 0.75;
//cellSize = ((boardMaxHeight / rows)-padding) + (3-level);
var boardMaxWidth = Math.max(document.documentElement.clientHeight, window.innerHeight || 0) * 0.7;
cellSize = ((boardMaxWidth / cols)-padding) + (3-level);
while (cols * (padding + cellSize) > Math.max(document.documentElement.clientWidth, window.innerWidth || 0)) {
cellSize--;
}
canvWidth = (cols * (cellSize + padding)) + 1.5*padding;
canvHeight = (rows * (cellSize + padding)) + 1.5*padding;
neighborIndexOffsets = [-cols-1, -cols, -cols+1, -1, 1, cols-1, cols, cols+1]
reset();
}
function setup() {
noLoop();
reset();
if (getURLParams().d != undefined) {
setDifficulty(parseInt(getURLParams().d));
} else {
setDifficulty(1);
}
}
function reset() {
createCanvas(canvWidth, canvHeight).parent("canvasDiv");
textSize(cellSize);
board = [];
finished = false;
gameover = false;
markedBombs = false;
for (var i = 0; i < rows; i++) {
for (var j = 0; j < cols; j++) {
board.push(new Cell(j, i));
}
}
placeMines();
for (var i = 0; i < board.length; i++) {
board[i].countNeighbors();
}
draw();
timer_running = false;
if (timerInterval) { clearInterval(timerInterval); }
timerInterval = undefined;
timer_hasCheated = false;
timer_ms = -1;
timerTick();
}
function draw() {
markedBombs = 0;
background(50);
if (gameover || finished) {
stopTimer();
}
for (var i = 0; i < board.length; i++) {
if (board[i].revealed) {
fill(200);
} else {
fill(100);
if (board[i].marked) {
fill(255, 100, 0);
markedBombs++;
}
if (finished) {
fill(0, 255, 0);
}
}
if (gameover && board[i].mine) {
fill(255,0,0);
}
rect(board[i].x * (cellSize + padding) + padding, board[i].y * (cellSize + padding) + padding, cellSize, cellSize);
if (board[i].revealed) {
fill(50);
if (board[i].neighbors != 0) {
text(board[i].neighbors, (board[i].x + 0.32) * (cellSize + padding), (board[i].y+0.9) * (cellSize + padding));
}
}
}
}
function placeMines() {
var placed = 0;
while (placed != mineCount) {
//Place mines, make sure that the same square isn't selected again
var i = Math.floor(Math.random() * board.length);
if (!board[i].mine) {
board[i].mine = true;
placed++;
}
}
}
function test_finished() {
for (var i = 0; i < board.length; i++) {
if (board[i].mine && !board[i].marked) { return false;}
if (!board[i].revealed && !board[i].mine) { return false; }
}
return true;
}
function interact(x, y, mark) {
if (document.getElementById("helpOverlay").style.display == "block") { return; }
var index = (y * cols) + x;
//console.log("x: " + x.toString() + " y: " + y.toString());
if (mark) {
board[index].mark();
} else {
board[index].reveal();
}
finished = test_finished();
if (!timer_running) {
startTimer();
}
draw();
}
function mouseClicked() {
if ((mouseX < 0) || (mouseX >= canvWidth)) { return; }
if ((mouseY < 0) || (mouseY >= canvHeight)) { return; }
var ix = Math.floor((mouseX-padding) / (cellSize + padding));
var iy = Math.floor((mouseY-padding) / (cellSize + padding));
interact(ix, iy, keyIsDown(SHIFT));
}
function keyPressed() {
if (keyIsDown(82)) { //Reset when you click R
if (refreshPage_reset) {
window.location.reload();
} else {
reset();
}
}
if (keyIsDown(65)) {
autoMove(false); //Automove when you click A
}
if (keyIsDown(87)) { //Click with W
var ix = Math.floor((mouseX-padding) / (cellSize + padding));
var iy = Math.floor((mouseY-padding) / (cellSize + padding));
interact(ix, iy, false);
}
if (keyIsDown(81)) { //Mark with Q
var ix = Math.floor((mouseX-padding) / (cellSize + padding));
var iy = Math.floor((mouseY-padding) / (cellSize + padding));
interact(ix, iy, true);
}
}

155
minesweeper/style.css Normal file
View File

@ -0,0 +1,155 @@
@charset "utf-8";
body {
display: grid;
grid-template-columns: 1fr 5fr 2fr 2fr;
grid-template-rows: 1fr 7fr 6fr;
background: rgb(175,175,175);
background: radial-gradient(circle, rgba(175,175,175,1) 0%, rgba(53,53,54,1) 100%);
max-height: 100vh;
}
#helpOverlay {
display: none;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
width: 100vw;
height: 200vh;
background-color: rgba(0,0,0,0.85);
cursor: pointer;
color: white;
text-align: center;
}
.helpText {
width: 60%;
margin: 0 auto;
font-size: 18px;
}
.helpTable {
display: inline-block;
color: white;
margin: 0 auto;
font-size: 18px;
padding: 10px;
border: 2px solid white;
height: 158px;
}
.helpTable > td {
padding-left: 10px;
padding-right: 10px;
}
#titleHead {
color: white;
font-size: 22px;
font-family: "Franklin Gothic Bold", "Arial Black", "sans-serif";
text-align: center;
margin: auto;
margin-bottom: 2%;
padding: 20px;
background-color: midnightblue;
width: 400px;
height: 70%;
border-radius: 10px;
}
#gameDiv {
grid-column: 2 / 3;
grid-row: 2 / 3;
text-align: center;
margin: auto 0 600px auto;
}
#canvasDiv {
margin-top: 5px;
}
#leftPane {
grid-column: 3 / 4;
grid-row: 1 / 3;
margin: 10px;
padding: 30px;
text-align: center;
border: 2px solid black;
border-radius: 10px;
background-color: whitesmoke;
max-width: 200px;
height: 45%;
margin: 20% auto auto 5%;
}
#timerText {
font-size: 26px;
border: 1px solid black;
border-radius: 5px;
padding: 10px 0px;
}
#bottomPadding {
grid-column: 1/3;
grid-row: 3/4;
height: 100%;
}
.paneBtn {
margin: 6px;
padding: 3px;
font-size: 16px;
width: 80%;
}
button {
border-radius: 4px;
}
@media (max-width: 750px) {
body {
grid-template-columns: 1fr 2fr;
grid-template-rows: 1fr 3fr;
max-height: none;
}
#gameDiv {
grid-column: 1 / 3;
grid-row: 1 / 2;
margin: 15px auto;
}
#leftPane {
grid-column: 1 / 3;
grid-row: 2 / 3;
float: center;
margin: 10px auto 0px auto;
min-width: 60%;
height: 600px;
}
}
/* Temporary */
img {
padding-left: 10px;
padding-right: 10px;
padding-top: 4px;
padding-bottom: 4px;
}

39
minesweeper/timer.js Normal file
View File

@ -0,0 +1,39 @@
// Felix Albrigtsen
var timerInterval = undefined;
var timer_hasCheated = false;
var timer_running = false;
var timer_ms = 0;
function startTimer() {
if (timer_running) { return; }
timer_ms = 0;
timer_running = true;
timerInterval = setInterval(timerTick, 10);
}
function stopTimer() {
if (!timer_running) { return; }
timer_running = false;
clearInterval(timerInterval);
timerInterval = undefined;
}
function timerTick() {
timer_ms++;
var t_centisecond = (timer_ms % 100);
var t_second = ((timer_ms - t_centisecond) % (100*60))/100;
var t_minute = (timer_ms - (t_second*100) - t_centisecond) / (100*60);
t_centisecond = ("00" + t_centisecond).substr(-2, 2);
t_second = ("00" + t_second).substr(-2, 2);
t_minute = ("00" + t_minute).substr(-2, 2);
document.getElementById("timerText").innerHTML = t_minute + ":" + t_second + ":" + t_centisecond;
if (timer_hasCheated) {
document.getElementById("timerText").style.color = "red";
} else {
document.getElementById("timerText").style.color = "green";
}
}