parent
9bb75784a8
commit
ebb56ee7c1
|
@ -7,9 +7,6 @@ module.exports = grunt => {
|
|||
default: ["dist/"],
|
||||
},
|
||||
copy: {
|
||||
css: {
|
||||
files: [{expand: true, cwd: "src/main/", src: "**/*.css", dest: "dist/"}]
|
||||
},
|
||||
html: {
|
||||
files: [{expand: true, cwd: "src/main/", src: "**/*.html", dest: "dist/"}]
|
||||
},
|
||||
|
@ -44,7 +41,7 @@ module.exports = grunt => {
|
|||
watch: {
|
||||
css: {
|
||||
files: ["src/main/**/*.css"],
|
||||
tasks: ["copy:css"],
|
||||
tasks: ["webpack:dev", "replace:dev"],
|
||||
},
|
||||
html: {
|
||||
files: ["src/main/**/*.html"],
|
||||
|
@ -114,7 +111,6 @@ module.exports = grunt => {
|
|||
// Pre
|
||||
"clean",
|
||||
// Copy files
|
||||
"copy:css",
|
||||
"copy:html",
|
||||
// Compile TS
|
||||
"webpack:dev",
|
||||
|
@ -125,7 +121,6 @@ module.exports = grunt => {
|
|||
// Pre
|
||||
"clean",
|
||||
// Copy files
|
||||
"copy:css",
|
||||
"copy:html",
|
||||
// Compile TS
|
||||
"webpack:deploy",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "minesweeper",
|
||||
"version": "0.0.41",
|
||||
"version": "0.0.42",
|
||||
"description": "Just Minesweeper!",
|
||||
"author": "Felix W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
#mainContainer {
|
||||
max-width: 100%;
|
||||
padding: 6rem;
|
||||
}
|
||||
|
||||
#canvasContainer {
|
||||
text-align: center;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
display: inline;
|
||||
}
|
|
@ -25,35 +25,34 @@
|
|||
<div id="contents">
|
||||
<div id="header"></div>
|
||||
|
||||
<section class="container">
|
||||
<section class="container" id="mainContainer">
|
||||
<div class="row">
|
||||
<div class="column column-60">
|
||||
<!-- Field -->
|
||||
<canvas id="canvas" width="1" height="1"></canvas>
|
||||
<div class="column column-75">
|
||||
<!-- Canvas -->
|
||||
<div id="canvasContainer">
|
||||
<canvas id="canvas" width="1" height="1"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column column-40">
|
||||
<!-- Sidebar -->
|
||||
<div class="column column-25">
|
||||
<!-- Solver -->
|
||||
<h3>Controls</h3>
|
||||
<form id="solveForm">
|
||||
<button>Solve</button>
|
||||
</form>
|
||||
<form id="controlForm">
|
||||
<label for="displayScale">Scale</label>
|
||||
<input type="number" id="displayScale" value="30" min="1" />
|
||||
</form>
|
||||
|
||||
<!-- Settings -->
|
||||
<h3>Settings</h3>
|
||||
<form id="settingsForm">
|
||||
<label for="settingsWidth">Width</label>
|
||||
<input type="number" id="settingsWidth" value="9" />
|
||||
<input type="number" id="settingsWidth" min="3" max="99" value="9" />
|
||||
|
||||
<label for="settingsHeight">Height</label>
|
||||
<input type="number" id="settingsHeight" value="9" />
|
||||
<input type="number" id="settingsHeight" min="3" max="99" value="9" />
|
||||
|
||||
<label for="settingsMines">Mines</label>
|
||||
<input type="number" id="settingsMines" value="10" />
|
||||
<input type="number" id="settingsMines" min="0" value="10" />
|
||||
|
||||
<label for="settingsSeed">Seed</label>
|
||||
<input type="number" id="settingsSeed" value="" />
|
||||
|
|
|
@ -6,10 +6,10 @@ import {Field, Square} from "./Field";
|
|||
* Displays a Minesweeper field.
|
||||
*/
|
||||
export class Display {
|
||||
private readonly scale: number = 30;
|
||||
private readonly canvas: HTMLCanvasElement;
|
||||
|
||||
field: Field;
|
||||
private scale: number;
|
||||
private field: Field | null = null;
|
||||
mouseSquare: Square | null;
|
||||
mouseHoldChord: boolean;
|
||||
|
||||
|
@ -29,14 +29,12 @@ export class Display {
|
|||
* @param canvas the canvas to draw the field in
|
||||
* @param field the field to draw
|
||||
*/
|
||||
constructor(canvas: HTMLCanvasElement, field: Field) {
|
||||
constructor(canvas: HTMLCanvasElement, field: Field | null) {
|
||||
this.canvas = canvas;
|
||||
this.field = field;
|
||||
this.scale = 10;
|
||||
this.setField(field);
|
||||
|
||||
this.mouseSquare = null;
|
||||
this.mouseHoldChord = false;
|
||||
this.initSymbols();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -142,7 +140,7 @@ export class Display {
|
|||
*/
|
||||
posToSquare(pos: { x: number, y: number }): Square | null {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
return this.field.getSquareOrElse(
|
||||
return this.field?.getSquareOrElse(
|
||||
Math.floor((pos.x - rect.left) / this.scale),
|
||||
Math.floor((pos.y - rect.top) / this.scale),
|
||||
null
|
||||
|
@ -150,12 +148,14 @@ export class Display {
|
|||
}
|
||||
|
||||
/**
|
||||
* Rescales the display appropriately.
|
||||
* Changes the field to draw.
|
||||
*
|
||||
* @param scale the size of a square in pixels
|
||||
* @param field the field to draw, or `null` if no field should be drawn
|
||||
*/
|
||||
setScale(scale: number): void {
|
||||
this.scale = scale;
|
||||
setField(field: Field | null): void {
|
||||
this.field = field;
|
||||
if (this.field === null) return;
|
||||
|
||||
this.canvas.width = this.field.width * this.scale;
|
||||
this.canvas.height = this.field.height * this.scale + this.scale;
|
||||
this.initSymbols();
|
||||
|
@ -186,6 +186,7 @@ export class Display {
|
|||
ctx.fillStyle = "#bdbdbd";
|
||||
ctx.fillRect(0, 0, rect.width, rect.height);
|
||||
ctx.restore();
|
||||
if (this.field === null) return;
|
||||
|
||||
// Create grid
|
||||
ctx.save();
|
||||
|
@ -224,6 +225,8 @@ export class Display {
|
|||
ctx.textBaseline = "middle";
|
||||
ctx.textAlign = "center";
|
||||
this.field.squareList.forEach(square => {
|
||||
if (this.field === null) return;
|
||||
|
||||
let icon;
|
||||
if (square.hasFlag) {
|
||||
if (this.field.lost && !square.hasMine)
|
||||
|
|
|
@ -28,6 +28,9 @@ export class Field {
|
|||
* @param seed the seed to generate the field with
|
||||
*/
|
||||
constructor(width: number, height: number, mineCount: number, seed: number | undefined = undefined) {
|
||||
if (mineCount > Field.maxMines(width, height))
|
||||
throw new Error(`Mine count must be at most ${Field.maxMines(width, height)}, but was ${mineCount}.`);
|
||||
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.mineCount = mineCount;
|
||||
|
@ -106,6 +109,16 @@ export class Field {
|
|||
return this.squareList.filter(it => it.hasFlag).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of mines that can be placed in a `width` x `height` field.
|
||||
*
|
||||
* @param width the width of the field
|
||||
* @param height the height of the field
|
||||
*/
|
||||
static maxMines(width: number, height: number): number {
|
||||
return width * height - 9;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles the event when a square is clicked, which includes moving the mine if the player hits a mine on the first
|
||||
|
|
|
@ -11,14 +11,12 @@ import {Solver} from "./Solver";
|
|||
export class Game {
|
||||
private readonly canvas: HTMLCanvasElement;
|
||||
private readonly solveForm: HTMLFormElement;
|
||||
private readonly controlForm: HTMLFormElement;
|
||||
private readonly displayScale: HTMLInputElement;
|
||||
private readonly settingsForm: HTMLFormElement;
|
||||
private readonly widthInput: HTMLInputElement;
|
||||
private readonly heightInput: HTMLInputElement;
|
||||
private readonly minesInput: HTMLInputElement;
|
||||
private readonly seedInput: HTMLInputElement;
|
||||
private field: Field;
|
||||
private field: Field | null;
|
||||
private display: Display;
|
||||
private leftDown: boolean;
|
||||
private rightDown: boolean;
|
||||
|
@ -32,8 +30,6 @@ export class Game {
|
|||
this.canvas = $("#canvas");
|
||||
|
||||
this.solveForm = $("#solveForm");
|
||||
this.controlForm = $("#controlForm");
|
||||
this.displayScale = $("#displayScale");
|
||||
|
||||
this.settingsForm = $("#settingsForm");
|
||||
this.widthInput = $("#settingsWidth");
|
||||
|
@ -41,9 +37,8 @@ export class Game {
|
|||
this.minesInput = $("#settingsMines");
|
||||
this.seedInput = $("#settingsSeed");
|
||||
|
||||
this.field = this.createNewField();
|
||||
this.field = null; // Placeholder
|
||||
this.display = new Display(this.canvas, this.field);
|
||||
this.display.setScale(+this.displayScale.value);
|
||||
this.display.startDrawLoop();
|
||||
|
||||
this.leftDown = false;
|
||||
|
@ -51,39 +46,29 @@ export class Game {
|
|||
this.holdsAfterChord = false;
|
||||
|
||||
|
||||
// Set up event handlers
|
||||
this.solveForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
new Solver().solve(this.field);
|
||||
}
|
||||
);
|
||||
this.controlForm.addEventListener(
|
||||
"submit",
|
||||
event => event.preventDefault()
|
||||
);
|
||||
this.displayScale.addEventListener(
|
||||
"change",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
this.display.setScale(+this.displayScale.value);
|
||||
|
||||
if (this.field !== null)
|
||||
new Solver().solve(this.field);
|
||||
}
|
||||
);
|
||||
|
||||
this.widthInput.addEventListener("change", _ => this.setMineLimit());
|
||||
this.heightInput.addEventListener("change", _ => this.setMineLimit());
|
||||
this.settingsForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
if (+this.widthInput.value * +this.heightInput.value < +this.minesInput.value + 9) {
|
||||
window.alert("Field must contain at least 9 empty squares.")
|
||||
return;
|
||||
}
|
||||
|
||||
this.field = this.createNewField();
|
||||
this.display.field = this.field;
|
||||
this.display.setScale(+this.displayScale.value);
|
||||
this.display.setField(this.field);
|
||||
}
|
||||
);
|
||||
|
||||
this.canvas.addEventListener(
|
||||
"mousemove",
|
||||
event => this.display.mouseSquare = this.display.posToSquare({x: event.clientX, y: event.clientY})
|
||||
|
@ -105,7 +90,7 @@ export class Game {
|
|||
this.canvas.addEventListener(
|
||||
"mousedown",
|
||||
event => {
|
||||
event.preventDefault()
|
||||
event.preventDefault();
|
||||
|
||||
const square = this.display.posToSquare({x: event.clientX, y: event.clientY});
|
||||
switch (event.button) {
|
||||
|
@ -153,6 +138,14 @@ export class Game {
|
|||
this.display.mouseHoldChord = this.leftDown && this.rightDown;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Create field with current settings
|
||||
this.setMineLimit();
|
||||
if (this.settingsForm.reportValidity()) {
|
||||
this.field = this.createNewField();
|
||||
this.display.setField(this.field);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -169,4 +162,11 @@ export class Game {
|
|||
+this.seedInput.value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the limits on the mine count input field.
|
||||
*/
|
||||
setMineLimit(): void {
|
||||
this.minesInput.max = "" + Field.maxMines(+this.widthInput.value, +this.heightInput.value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import "../css/main.css";
|
||||
import "fork-awesome/css/fork-awesome.css";
|
||||
// @ts-ignore
|
||||
import {$, doAfterLoad, footer, header, nav} from "@fwdekker/template";
|
||||
import "fork-awesome/css/fork-awesome.css";
|
||||
import {waitForForkAwesome} from "./Common";
|
||||
import {Game} from "./Game";
|
||||
|
||||
|
@ -8,10 +9,7 @@ import {Game} from "./Game";
|
|||
doAfterLoad(() => {
|
||||
// Initialize template
|
||||
$("#nav").appendChild(nav("/Tools/Minesweeper/"));
|
||||
$("#header").appendChild(header({
|
||||
title: "Minesweeper",
|
||||
description: "Just Minesweeper!"
|
||||
}));
|
||||
$("#header").appendChild(header({title: "Minesweeper"}));
|
||||
$("#footer").appendChild(footer({
|
||||
author: "Felix W. Dekker",
|
||||
authorURL: "https://fwdekker.com/",
|
||||
|
|
Loading…
Reference in New Issue