Remove scale, validate inputs

Fixes #25. Fixes #31.
This commit is contained in:
Florine W. Dekker 2020-08-01 01:38:08 +02:00
parent 9bb75784a8
commit ebb56ee7c1
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
8 changed files with 82 additions and 61 deletions

View File

@ -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",

View File

@ -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",

13
src/main/css/main.css Normal file
View File

@ -0,0 +1,13 @@
#mainContainer {
max-width: 100%;
padding: 6rem;
}
#canvasContainer {
text-align: center;
overflow: auto;
}
#canvas {
display: inline;
}

View File

@ -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="" />

View File

@ -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)

View File

@ -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

View File

@ -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);
}
}

View File

@ -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/",