Upgrade template to v3
This commit is contained in:
parent
0e0238687a
commit
b4f2586aa8
Binary file not shown.
18
package.json
18
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "minesweeper",
|
||||
"version": "0.82.16",
|
||||
"version": "0.83.0",
|
||||
"description": "Just Minesweeper!",
|
||||
"author": "Florine W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
@ -17,21 +17,21 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"alea": "^1.0.1",
|
||||
"canvas-confetti": "^1.5.1"
|
||||
"canvas-confetti": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"grunt": "^1.4.1",
|
||||
"grunt": "^1.5.3",
|
||||
"grunt-cli": "^1.4.3",
|
||||
"grunt-contrib-clean": "^2.0.0",
|
||||
"grunt-contrib-clean": "^2.0.1",
|
||||
"grunt-contrib-copy": "^1.0.0",
|
||||
"grunt-contrib-watch": "^1.1.0",
|
||||
"grunt-focus": "^1.0.0",
|
||||
"grunt-text-replace": "^0.4.0",
|
||||
"grunt-webpack": "^5.0.0",
|
||||
"ts-loader": "^9.2.6",
|
||||
"ts-node": "^10.5.0",
|
||||
"typescript": "^4.5.5",
|
||||
"webpack": "^5.69.1",
|
||||
"webpack-cli": "^4.9.2"
|
||||
"ts-loader": "^9.4.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.3",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,71 +1,42 @@
|
|||
form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
form button.cancel {
|
||||
background-color: #606c76;
|
||||
border-color: #606c76;
|
||||
}
|
||||
|
||||
|
||||
/* Controls */
|
||||
.row.controls {
|
||||
text-align: center;
|
||||
.controls-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.row.controls input, .row.controls button {
|
||||
display: inline;
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
|
||||
flex-wrap: wrap;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.row.controls select, .row.controls input {
|
||||
.controls select {
|
||||
width: unset;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Canvas */
|
||||
#canvasContainer {
|
||||
#canvas-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
display: inline;
|
||||
box-sizing: border-box;
|
||||
border: 4mm ridge #bdbdbd;
|
||||
}
|
||||
|
||||
#canvas.invisible {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
|
||||
/* Overlay */
|
||||
.overlayWrapper {
|
||||
z-index: 20;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
/* Dialogs */
|
||||
#high-scores-dialog article,
|
||||
#statistics-dialog article {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
padding: 6rem;
|
||||
min-width: 33%;
|
||||
max-height: 80%;
|
||||
overflow: auto;
|
||||
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
|
||||
#statisticsOverlay td {
|
||||
#statistics-dialog td {
|
||||
text-align: right;
|
||||
}
|
||||
|
|
|
@ -8,18 +8,24 @@
|
|||
<meta name="description" content="Just Minesweeper!" />
|
||||
<meta name="theme-color" content="#0033cc" />
|
||||
|
||||
<meta name="fwd:auto:show-main" />
|
||||
<meta name="fwd:nav:target" content="#nav" />
|
||||
<meta name="fwd:nav:highlight-path" content="/Tools/Minesweeper/" />
|
||||
<meta name="fwd:footer:target" content="#footer" />
|
||||
<meta name="fwd:footer:vcs-url" content="https://git.fwdekker.com/tools/minesweeper/" />
|
||||
<meta name="fwd:footer:version" content="v%%VERSION_NUMBER%%" />
|
||||
|
||||
<title>Minesweeper | FWDekker</title>
|
||||
|
||||
<link rel="stylesheet" href="https://static.fwdekker.com/fonts/roboto/roboto.css" />
|
||||
<link rel="stylesheet" href="https://static.fwdekker.com/fonts/fork-awesome/1.x.x/fork-awesome.css" />
|
||||
<link rel="stylesheet" href="https://static.fwdekker.com/lib/template/2.x.x/template.css" />
|
||||
<link rel="stylesheet" href="https://static.fwdekker.com/lib/template/3.x.x/template.css?v=%%VERSION_NUMBER%%" />
|
||||
<!--suppress HtmlUnknownTarget -->
|
||||
<link rel="stylesheet" href="main.css?v=%%VERSION_NUMBER%%" />
|
||||
<script async src="https://stats.fwdekker.com/count.js"
|
||||
data-goatcounter="https://stats.fwdekker.com/count"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<noscript class="fwd-js-notice">
|
||||
<img src="https://stats.fwdekker.com/count?p=/tools/minesweeper/" alt="Counting pixel" />
|
||||
|
||||
<p>
|
||||
|
@ -28,184 +34,166 @@
|
|||
instructions on how to enable JavaScript in your web browser</a>.
|
||||
</p>
|
||||
</noscript>
|
||||
<main class="hidden">
|
||||
<div id="nav"></div>
|
||||
<div id="contents">
|
||||
<div id="header"></div>
|
||||
<nav id="nav"></nav>
|
||||
<main class="container hidden">
|
||||
<div role="document">
|
||||
<section>
|
||||
<header class="fwd-header">
|
||||
<hgroup>
|
||||
<h1><a href=".">Minesweeper</a></h1>
|
||||
<h2>Just Minesweeper!</h2>
|
||||
</hgroup>
|
||||
</header>
|
||||
|
||||
<section class="container">
|
||||
<!-- Controls -->
|
||||
<div class="row controls">
|
||||
<div class="column">
|
||||
<!-- Preferences -->
|
||||
<form id="preferencesOpenForm">
|
||||
<button><i class="fa fa-cogs"></i> Preferences</button>
|
||||
</form>
|
||||
|
||||
<!-- Statistics -->
|
||||
<form id="statisticsOpenForm">
|
||||
<button><i class="fa fa-tachometer"></i> Statistics</button>
|
||||
</form>
|
||||
|
||||
<!-- High scores -->
|
||||
<form id="highScoresOpenForm">
|
||||
<button><i class="fa fa-trophy"></i> High scores</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row controls">
|
||||
<div class="column">
|
||||
<!-- Difficulty -->
|
||||
<label for="difficulty"></label>
|
||||
<select id="difficulty">
|
||||
<option>Select difficulty</option>
|
||||
</select>
|
||||
|
||||
<!-- New game -->
|
||||
<form id="newGameForm">
|
||||
<button><i class="fa fa-random"></i> New game</button>
|
||||
</form>
|
||||
|
||||
<!-- Restart -->
|
||||
<form id="restartForm">
|
||||
<button><i class="fa fa-sync"></i> Restart game</button>
|
||||
</form>
|
||||
|
||||
<!-- Seed -->
|
||||
<form id="seedOpenForm">
|
||||
<button><i class="fa fa-tree"></i> Enter seed</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row controls">
|
||||
<div class="column">
|
||||
<!-- Undo -->
|
||||
<form id="undoForm">
|
||||
<button><i class="fa fa-undo"></i> Undo</button>
|
||||
</form>
|
||||
|
||||
<!-- Redo -->
|
||||
<form id="redoForm">
|
||||
<button><i class="fa fa-repeat"></i> Redo</button>
|
||||
</form>
|
||||
|
||||
<!-- Hint -->
|
||||
<form id="hintForm">
|
||||
<button><i class="fa fa-lightbulb-o"></i> Hint</button>
|
||||
</form>
|
||||
|
||||
<!-- Solver -->
|
||||
<form id="solveForm">
|
||||
<button><i class="fa fa-key"></i> Solve</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<!-- Field -->
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<!-- Canvas -->
|
||||
<div id="canvasContainer">
|
||||
<canvas id="canvas" class="invisible" width="1" height="1">
|
||||
Your browser must support the <canvas> element to run this game.
|
||||
</canvas>
|
||||
<article>
|
||||
<header class="controls-group">
|
||||
<div class="controls">
|
||||
<a role="button" href="#" id="preferences-open"><i class="fa fa-cogs"></i> Preferences</a>
|
||||
<a role="button" href="#" id="statistics-open"><i class="fa fa-tachometer"></i> Statistics</a>
|
||||
<a role="button" href="#" id="high-scores-open">
|
||||
<i class="fa fa-trophy"></i> High scores</a>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<a role="button" href="#" id="new-game"><i class="fa fa-random"></i> New game</a>
|
||||
<a role="button" href="#" id="restart"><i class="fa fa-sync"></i> Restart game</a>
|
||||
<a role="button" href="#" id="seed-open"><i class="fa fa-tree"></i> Enter seed</a>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<!--suppress HtmlFormInputWithoutLabel -->
|
||||
<select id="difficulty">
|
||||
<option>Select difficulty</option>
|
||||
</select>
|
||||
</div>
|
||||
</header>
|
||||
<div id="canvas-container">
|
||||
<canvas id="canvas" class="hidden" width="1" height="1">
|
||||
Your browser must support the <canvas> element to run this game.
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="controls">
|
||||
<a role="button" href="#" id="undo"><i class="fa fa-undo"></i> Undo</a>
|
||||
<a role="button" href="#" id="redo"><i class="fa fa-repeat"></i> Redo</a>
|
||||
<a role="button" href="#" id="hint"><i class="fa fa-lightbulb-o"></i> Hint</a>
|
||||
<a role="button" href="#" id="solve"><i class="fa fa-key"></i> Solve</a>
|
||||
</footer>
|
||||
</article>
|
||||
</section>
|
||||
<footer id="footer"></footer>
|
||||
</div>
|
||||
<div id="footer"></div>
|
||||
|
||||
<!-- Custom difficulty overlay -->
|
||||
<div class="overlayWrapper hidden" id="customDifficultyOverlay">
|
||||
<div class="overlay">
|
||||
<h2>Custom difficulty</h2>
|
||||
<form id="customDifficultyForm">
|
||||
<label for="settingsWidth">Width</label>
|
||||
<input type="number" id="settingsWidth" min="3" max="99" value="9" />
|
||||
<!-- Custom difficulty dialog -->
|
||||
<dialog id="custom-difficulty-dialog">
|
||||
<article>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1>Custom difficulty</h1>
|
||||
<h2>Configure a custom difficulty.</h2>
|
||||
</hgroup>
|
||||
</header>
|
||||
<form id="custom-difficulty-form">
|
||||
<label for="settings-width">Width</label>
|
||||
<input type="number" id="settings-width" min="3" max="99" value="9" autofocus />
|
||||
|
||||
<label for="settingsHeight">Height</label>
|
||||
<input type="number" id="settingsHeight" min="3" max="99" value="9" />
|
||||
<label for="settings-height">Height</label>
|
||||
<input type="number" id="settings-height" min="3" max="99" value="9" />
|
||||
|
||||
<label for="settingsMines">Mines</label>
|
||||
<input type="number" id="settingsMines" min="0" value="10" />
|
||||
<label for="settings-mines">Mines</label>
|
||||
<input type="number" id="settings-mines" min="0" value="10" />
|
||||
|
||||
<label for="settingsSolvable">Ensure solvability (slow for complex settings)</label>
|
||||
<input type="checkbox" id="settingsSolvable" />
|
||||
<br /><br />
|
||||
|
||||
<button>New game</button>
|
||||
<input type="checkbox" id="settings-solvable" />
|
||||
<label for="settings-solvable">Ensure solvability. <small>(Slow for complex settings.)</small></label>
|
||||
</form>
|
||||
<form id="customDifficultyCancelForm">
|
||||
<button class="cancel">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Seed input overlay -->
|
||||
<div class="overlayWrapper hidden" id="seedOverlay">
|
||||
<div class="overlay">
|
||||
<h2>Seed</h2>
|
||||
<form id="seedForm">
|
||||
<footer>
|
||||
<a role="button" href="#" id="custom-difficulty-cancel" class="secondary">Cancel</a>
|
||||
<a role="button" href="#" id="custom-difficulty-submit">New game</a>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
<!-- Seed input dialog -->
|
||||
<dialog id="seed-dialog">
|
||||
<article>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1>Seed</h1>
|
||||
<h2>
|
||||
Enter a number to generate a minefield with.
|
||||
Given a difficulty level and first click position, the same seed always generates the exact same
|
||||
minefield.
|
||||
</h2>
|
||||
</hgroup>
|
||||
</header>
|
||||
<form id="seed-form">
|
||||
<!--suppress HtmlFormInputWithoutLabel Only one input, so header already explains everything -->
|
||||
<input id="seed" />
|
||||
|
||||
<button>New game</button>
|
||||
<input id="seed" autofocus />
|
||||
</form>
|
||||
<form id="seedCancelForm">
|
||||
<button class="cancel">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Preferences overlay -->
|
||||
<div class="overlayWrapper hidden" id="preferencesOverlay">
|
||||
<div class="overlay">
|
||||
<h2>Preferences</h2>
|
||||
<form id="preferencesForm">
|
||||
<label for="preferencesEnableMarks">Enable question marks</label>
|
||||
<input type="checkbox" id="preferencesEnableMarks" />
|
||||
|
||||
<label for="preferencesShowTooManyFlagsHints">Highlight squares with too many flags around them</label>
|
||||
<input type="checkbox" id="preferencesShowTooManyFlagsHints" />
|
||||
<footer>
|
||||
<a role="button" href="#" id="seed-cancel" class="secondary">Cancel</a>
|
||||
<a role="button" href="#" id="seed-submit">New game</a>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
<!-- Preferences dialog -->
|
||||
<dialog id="preferences-dialog">
|
||||
<article>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1>Preferences</h1>
|
||||
<h2>
|
||||
Configure the gameplay to your liking.
|
||||
</h2>
|
||||
</hgroup>
|
||||
</header>
|
||||
<form id="preferences-form">
|
||||
<input role="switch" type="checkbox" id="preferences-enable-marks" autofocus />
|
||||
<label for="preferences-enable-marks">Right-clicking a square twice places a question mark.</label>
|
||||
<br /><br />
|
||||
|
||||
<button>Save</button>
|
||||
<input role="switch" type="checkbox" id="preferences-show-too-many-flags-hints" />
|
||||
<label for="preferences-show-too-many-flags-hints">
|
||||
Highlight squares with too many flags around them.
|
||||
</label>
|
||||
</form>
|
||||
<form id="preferencesCancelForm">
|
||||
<button class="cancel">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Statistics overlay -->
|
||||
<div class="overlayWrapper hidden" id="statisticsOverlay">
|
||||
<div class="overlay">
|
||||
<h2>Statistics</h2>
|
||||
<div id="statisticsDiv"></div>
|
||||
<form id="statisticsResetForm">
|
||||
<button>Reset</button>
|
||||
</form>
|
||||
<form id="statisticsCloseForm">
|
||||
<button class="cancel">Close</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- High scores overlay -->
|
||||
<div class="overlayWrapper hidden" id="highScoresOverlay">
|
||||
<div class="overlay">
|
||||
<h2>High scores</h2>
|
||||
<div id="highScoresDiv"></div>
|
||||
<form id="highScoresResetForm">
|
||||
<button>Reset</button>
|
||||
</form>
|
||||
<form id="highScoresCloseForm">
|
||||
<button class="cancel">Close</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<a role="button" href="#" id="preferences-cancel" class="secondary">Cancel</a>
|
||||
<a role="button" href="#" id="preferences-submit">Save</a>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
<!-- Statistics dialog -->
|
||||
<dialog id="statistics-dialog">
|
||||
<article tabindex="-1" autofocus>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1>Statistics</h1>
|
||||
<h2>Your achievements expressed in numbers.</h2>
|
||||
</hgroup>
|
||||
</header>
|
||||
<div id="statistics-div"></div>
|
||||
<footer>
|
||||
<a role="button" href="#" id="statistics-close" class="secondary">Close</a>
|
||||
<a role="button" href="#" id="statistics-reset" class="secondary">Reset</a>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
<!-- High scores dialog -->
|
||||
<dialog id="high-scores-dialog">
|
||||
<article tabindex="-1" autofocus>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1>High scores</h1>
|
||||
<h2>Your best moments playing Minesweeper.</h2>
|
||||
</hgroup>
|
||||
</header>
|
||||
<div id="high-scores-div"></div>
|
||||
<footer>
|
||||
<a role="button" href="#" id="high-scores-close" class="secondary">Close</a>
|
||||
<a role="button" href="#" id="high-scores-reset" class="secondary">Reset</a>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
</main>
|
||||
|
||||
<script src="https://static.fwdekker.com/lib/template/2.x.x/template.js"></script>
|
||||
<script src="https://static.fwdekker.com/lib/template/2.x.x/storage.js"></script>
|
||||
<script src="https://static.fwdekker.com/lib/template/3.x.x/template.js?v=%%VERSION_NUMBER%%"></script>
|
||||
<!--suppress HtmlUnknownTarget -->
|
||||
<script src="bundle.js?v=%%VERSION_NUMBER%%"></script>
|
||||
</body>
|
||||
|
|
|
@ -119,7 +119,7 @@ export function waitForForkAwesome(onSuccess: () => void, onFailure: () => void,
|
|||
ctx.textBaseline = "middle";
|
||||
|
||||
const startTime = performance.now();
|
||||
const failTime = timeout === null ? null : startTime + timeout;
|
||||
const failTime = timeout == null ? null : startTime + timeout;
|
||||
requestAnimationFrame(fontOnload);
|
||||
|
||||
/**
|
||||
|
@ -129,7 +129,7 @@ export function waitForForkAwesome(onSuccess: () => void, onFailure: () => void,
|
|||
*/
|
||||
function fontOnload(time: number): void {
|
||||
const currentCount = getPixelCount();
|
||||
if (failTime !== null && time > failTime) onFailure();
|
||||
if (failTime != null && time > failTime) onFailure();
|
||||
else if (currentCount < targetPixelCount) requestAnimationFrame(fontOnload);
|
||||
else onSuccess();
|
||||
}
|
||||
|
|
|
@ -156,7 +156,7 @@ export class Display {
|
|||
* @private
|
||||
*/
|
||||
private getFieldOffset(): { x: number, y: number } {
|
||||
if (this.field === null) return {x: 0, y: 0};
|
||||
if (this.field == null) return {x: 0, y: 0};
|
||||
if (this.field.width >= this.minSquareWidth) return {x: 0, y: 0};
|
||||
return {x: Math.floor((this.minSquareWidth - this.field.width) * this.scale / 2), y: 0};
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ export class Display {
|
|||
setField(field: Field | null): void {
|
||||
this.hintSquare = null;
|
||||
this.field = field;
|
||||
if (this.field === null) return;
|
||||
if (this.field == null) return;
|
||||
|
||||
this.canvas.width = Math.max(this.minSquareWidth, this.field.width) * this.scale;
|
||||
this.canvas.height = this.field.height * this.scale + this.scale;
|
||||
|
@ -213,7 +213,7 @@ export class Display {
|
|||
const {x, y} = this.getFieldOffset();
|
||||
|
||||
this.clearCanvas(ctx);
|
||||
if (this.field === null) return;
|
||||
if (this.field == null) return;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
|
@ -247,7 +247,7 @@ export class Display {
|
|||
* @private
|
||||
*/
|
||||
private drawGrid(ctx: CanvasRenderingContext2D): void {
|
||||
if (this.field === null) return;
|
||||
if (this.field == null) return;
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
|
@ -271,7 +271,7 @@ export class Display {
|
|||
* @private
|
||||
*/
|
||||
private drawCovers(ctx: CanvasRenderingContext2D): void {
|
||||
if (this.field === null) return;
|
||||
if (this.field == null) return;
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = "#555";
|
||||
|
@ -279,7 +279,7 @@ export class Display {
|
|||
.filter(it => it.isCovered)
|
||||
.filter(it => {
|
||||
// True if square should be covered
|
||||
if (this.field!.hasLost || this.field!.hasWon || this.mouseSquare === null)
|
||||
if (this.field!.hasLost || this.field!.hasWon || this.mouseSquare == null)
|
||||
return true;
|
||||
if (this.mouseHoldUncover && this.mouseSquare === it)
|
||||
return it.hasFlag || it.hasMark;
|
||||
|
@ -299,7 +299,7 @@ export class Display {
|
|||
* @private
|
||||
*/
|
||||
private drawHints(ctx: CanvasRenderingContext2D): void {
|
||||
if (this.field === null) return;
|
||||
if (this.field == null) return;
|
||||
|
||||
if (this.preferences.showTooManyFlagsHints) {
|
||||
ctx.save();
|
||||
|
@ -311,7 +311,7 @@ export class Display {
|
|||
ctx.restore();
|
||||
}
|
||||
|
||||
if (this.hintSquare !== null) {
|
||||
if (this.hintSquare != null) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = "rgba(0, 255, 0, 0.3)";
|
||||
ctx.fillRect(this.hintSquare.x * this.scale, this.hintSquare.y * this.scale, this.scale, this.scale);
|
||||
|
@ -326,11 +326,11 @@ export class Display {
|
|||
* @private
|
||||
*/
|
||||
private drawSymbols(ctx: CanvasRenderingContext2D): void {
|
||||
if (this.field === null) return;
|
||||
if (this.field == null) return;
|
||||
|
||||
ctx.save();
|
||||
this.field.squareList.forEach(square => {
|
||||
if (this.field === null) return;
|
||||
if (this.field == null) return;
|
||||
|
||||
let icon;
|
||||
if (square.hasFlag)
|
||||
|
@ -354,7 +354,7 @@ export class Display {
|
|||
* @private
|
||||
*/
|
||||
private drawStatusBar(ctx: CanvasRenderingContext2D): void {
|
||||
if (this.field === null) return;
|
||||
if (this.field == null) return;
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = "#000";
|
||||
|
@ -378,7 +378,7 @@ export class Display {
|
|||
// Deaths
|
||||
let deathsSymbol;
|
||||
if (this.field.hasLost) {
|
||||
if (this.loseTime === null)
|
||||
if (this.loseTime == null)
|
||||
this.loseTime = Date.now();
|
||||
|
||||
deathsSymbol = Math.floor((Date.now() - this.loseTime) / 1000) % 2 === 0
|
||||
|
@ -422,10 +422,10 @@ export class Display {
|
|||
* @private
|
||||
*/
|
||||
private drawWinConfetti(): void {
|
||||
if (this.field === null) return;
|
||||
if (this.field == null) return;
|
||||
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
if (this.field.hasWon && this.winTime === null) {
|
||||
if (this.field.hasWon && this.winTime == null) {
|
||||
confetti({
|
||||
origin: {
|
||||
x: (rect.left + rect.width / 2) / document.documentElement.clientWidth,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
const {MemoryStorage} = (window as any).fwdekker.storage;
|
||||
|
||||
// @ts-ignore
|
||||
import alea from "alea";
|
||||
import {Action, ActionHistory} from "./Action";
|
||||
|
@ -7,8 +9,6 @@ import {HighScores} from "./HighScores";
|
|||
import {Solver} from "./Solver";
|
||||
import {Statistics} from "./Statistics";
|
||||
import {Timer} from "./Timer";
|
||||
// @ts-ignore
|
||||
const {MemoryStorage} = window.fwdekker.storage;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -127,13 +127,15 @@ export class Field {
|
|||
|
||||
|
||||
/**
|
||||
* Returns the square at the given coordinates, or `orElse` if there is no square there.
|
||||
* Returns the square at the given coordinates, `orElse` if there is no square there, or `undefined` if there is no
|
||||
* square there and `orElse` is not given.
|
||||
*
|
||||
* @param coords the coordinates of the square to look up
|
||||
* @param orElse the value to return if there is no square at the given coordinates
|
||||
* @returns the square at the given coordinates, or `orElse` if there is no square there
|
||||
* @returns the square at the given coordinates, `orElse` if there is no square there, or `undefined` if there is no
|
||||
* square there and `orElse` is not given
|
||||
*/
|
||||
getSquareOrElse(coords: { x: number, y: number }, orElse: any = null): Square | any {
|
||||
getSquareOrElse<T = void>(coords: { x: number, y: number }, orElse?: T): Square | T {
|
||||
return this.squares[coords.y]?.[coords.x] ?? orElse;
|
||||
}
|
||||
|
||||
|
@ -523,15 +525,15 @@ export class Square {
|
|||
get neighbors(): Square[] {
|
||||
if (this._neighbors === undefined) {
|
||||
this._neighbors = [
|
||||
this.field.getSquareOrElse({x: this.x - 1, y: this.y - 1}),
|
||||
this.field.getSquareOrElse({x: this.x, y: this.y - 1}),
|
||||
this.field.getSquareOrElse({x: this.x + 1, y: this.y - 1}),
|
||||
this.field.getSquareOrElse({x: this.x - 1, y: this.y}),
|
||||
this.field.getSquareOrElse({x: this.x + 1, y: this.y}),
|
||||
this.field.getSquareOrElse({x: this.x - 1, y: this.y + 1}),
|
||||
this.field.getSquareOrElse({x: this.x, y: this.y + 1}),
|
||||
this.field.getSquareOrElse({x: this.x + 1, y: this.y + 1}),
|
||||
].filter(it => it !== null);
|
||||
this.field.getSquareOrElse({x: this.x - 1, y: this.y - 1}, null),
|
||||
this.field.getSquareOrElse({x: this.x, y: this.y - 1}, null),
|
||||
this.field.getSquareOrElse({x: this.x + 1, y: this.y - 1}, null),
|
||||
this.field.getSquareOrElse({x: this.x - 1, y: this.y}, null),
|
||||
this.field.getSquareOrElse({x: this.x + 1, y: this.y}, null),
|
||||
this.field.getSquareOrElse({x: this.x - 1, y: this.y + 1}, null),
|
||||
this.field.getSquareOrElse({x: this.x, y: this.y + 1}, null),
|
||||
this.field.getSquareOrElse({x: this.x + 1, y: this.y + 1}, null),
|
||||
].filter((it): it is Square => it !== null);
|
||||
}
|
||||
|
||||
return this._neighbors!;
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
// @ts-ignore
|
||||
const {$} = window.fwdekker;
|
||||
const {$, stringToHtml} = (window as any).fwdekker;
|
||||
|
||||
// @ts-ignore
|
||||
import alea from "alea";
|
||||
import {blurActiveElement, stringToHash} from "./Common";
|
||||
import {stringToHash} from "./Common";
|
||||
import {customDifficulty, defaultDifficulty, difficulties} from "./Difficulty";
|
||||
import {Display} from "./Display";
|
||||
import {Field} from "./Field";
|
||||
import {HighScores} from "./HighScores";
|
||||
import {ModalDialog} from "./ModalDialog";
|
||||
import {Preferences} from "./Preferences";
|
||||
import {Solver} from "./Solver";
|
||||
import {Statistics} from "./Statistics";
|
||||
import {Overlay} from "./UI";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -22,33 +22,13 @@ export class Game {
|
|||
private statisticsTimer: number | undefined;
|
||||
|
||||
private readonly canvas: HTMLCanvasElement;
|
||||
private readonly difficultySelect: HTMLSelectElement;
|
||||
private readonly newGameForm: HTMLFormElement;
|
||||
private readonly restartForm: HTMLFormElement;
|
||||
private readonly seedOverlay: Overlay;
|
||||
private readonly seedOpenForm: HTMLFormElement;
|
||||
private readonly seedInput: HTMLFormElement;
|
||||
private readonly undoForm: HTMLFormElement;
|
||||
private readonly redoForm: HTMLFormElement;
|
||||
private readonly hintForm: HTMLFormElement;
|
||||
private readonly solveForm: HTMLFormElement;
|
||||
private readonly customDifficultyOverlay: Overlay;
|
||||
private readonly customDifficultyOverlay: ModalDialog;
|
||||
private readonly widthInput: HTMLInputElement;
|
||||
private readonly heightInput: HTMLInputElement;
|
||||
private readonly minesInput: HTMLInputElement;
|
||||
private readonly solvableInput: HTMLInputElement;
|
||||
private readonly preferencesOverlay: Overlay;
|
||||
private readonly enableMarksInput: HTMLInputElement;
|
||||
private readonly showTooManyFlagsHintsInput: HTMLInputElement;
|
||||
private readonly preferencesOpenForm: HTMLFormElement;
|
||||
private readonly statisticsOverlay: Overlay;
|
||||
private readonly statisticsDiv: HTMLDivElement;
|
||||
private readonly statisticsResetForm: HTMLFormElement;
|
||||
private readonly statisticsOpenForm: HTMLFormElement;
|
||||
private readonly highScoresOverlay: Overlay;
|
||||
private readonly highScoresDiv: HTMLDivElement;
|
||||
private readonly highScoresResetForm: HTMLFormElement;
|
||||
private readonly highScoresOpenForm: HTMLFormElement;
|
||||
|
||||
private readonly rng: any;
|
||||
private seed: string;
|
||||
|
@ -71,7 +51,7 @@ export class Game {
|
|||
this.field = null; // Placeholder until `initNewField`
|
||||
this.display = new Display(this.canvas, this.field, preferences);
|
||||
this.display.startDrawLoop();
|
||||
this.canvas.classList.remove("invisible");
|
||||
this.canvas.classList.remove("hidden");
|
||||
|
||||
this.rng = alea("" + Date.now());
|
||||
this.seed = "" + this.rng.uint32();
|
||||
|
@ -83,44 +63,40 @@ export class Game {
|
|||
|
||||
|
||||
// Settings
|
||||
this.difficultySelect = $("#difficulty");
|
||||
const difficultySelect = $("#difficulty");
|
||||
difficulties.forEach(it => {
|
||||
const option = document.createElement("option");
|
||||
option.value = it.name;
|
||||
option.innerHTML = `${it.name}${it.description !== null ? ` (${it.description})` : ""}`;
|
||||
this.difficultySelect.add(option);
|
||||
const description = `${it.name}${it.description != null ? ` (${it.description})` : ""}`;
|
||||
difficultySelect.appendChild(stringToHtml(`<option value=${it.name}>${description}</option>`));
|
||||
});
|
||||
this.difficultySelect.addEventListener(
|
||||
difficultySelect.addEventListener("click", (event: MouseEvent) => event.stopPropagation());
|
||||
difficultySelect.addEventListener(
|
||||
"change",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
const difficulty = difficulties[this.difficultySelect.selectedIndex - 1];
|
||||
this.difficultySelect.selectedIndex = 0;
|
||||
() => {
|
||||
const difficulty = difficulties[difficultySelect.selectedIndex - 1];
|
||||
difficultySelect.selectedIndex = 0;
|
||||
if (difficulty === undefined) return;
|
||||
|
||||
if (difficulty.name !== customDifficulty.name) {
|
||||
this.difficultySelect.selectedIndex = 0;
|
||||
if (difficulty.name === customDifficulty.name)
|
||||
this.customDifficultyOverlay.open();
|
||||
else
|
||||
this.initNewField(difficulty.width, difficulty.height, difficulty.mineCount, difficulty.solvable);
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.customDifficultyOverlay.show();
|
||||
// Custom difficulty
|
||||
this.customDifficultyOverlay = new ModalDialog({
|
||||
dialog: $("#custom-difficulty-dialog"),
|
||||
onOpen: () => {
|
||||
this.widthInput.value = "" + (this.field?.width ?? defaultDifficulty.width);
|
||||
this.heightInput.value = "" + (this.field?.height ?? defaultDifficulty.height);
|
||||
this.minesInput.value = "" + (this.field?.mineCount ?? defaultDifficulty.mineCount);
|
||||
this.solvableInput.checked = this.field?.isSolvable ?? defaultDifficulty.solvable;
|
||||
this.setMineLimit();
|
||||
this.widthInput.focus();
|
||||
}
|
||||
);
|
||||
|
||||
// Custom difficulty
|
||||
this.customDifficultyOverlay = new Overlay(
|
||||
$("#customDifficultyOverlay"),
|
||||
$("#customDifficultyForm"),
|
||||
$("#customDifficultyCancelForm"),
|
||||
() => {
|
||||
},
|
||||
form: $("#custom-difficulty-form"),
|
||||
closeButton: $("#custom-difficulty-cancel"),
|
||||
submitButton: $("#custom-difficulty-submit"),
|
||||
onSubmit: () => {
|
||||
this.initNewField(
|
||||
+this.widthInput.value,
|
||||
+this.heightInput.value,
|
||||
|
@ -128,218 +104,135 @@ export class Game {
|
|||
this.solvableInput.checked
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
this.widthInput = $("#settingsWidth");
|
||||
this.widthInput = $("#settings-width");
|
||||
this.widthInput.addEventListener("change", _ => this.setMineLimit());
|
||||
this.heightInput = $("#settingsHeight");
|
||||
this.heightInput = $("#settings-height");
|
||||
this.heightInput.addEventListener("change", _ => this.setMineLimit());
|
||||
this.minesInput = $("#settingsMines");
|
||||
this.solvableInput = $("#settingsSolvable");
|
||||
this.minesInput = $("#settings-mines");
|
||||
this.solvableInput = $("#settings-solvable");
|
||||
|
||||
// New game form
|
||||
this.newGameForm = $("#newGameForm");
|
||||
this.newGameForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
this.initNewField(this.field?.width, this.field?.height, this.field?.mineCount, this.field?.isSolvable);
|
||||
blurActiveElement();
|
||||
}
|
||||
$("#new-game").addEventListener(
|
||||
"click",
|
||||
() =>
|
||||
this.initNewField(this.field?.width, this.field?.height, this.field?.mineCount, this.field?.isSolvable)
|
||||
);
|
||||
|
||||
// Restart
|
||||
this.restartForm = $("#restartForm");
|
||||
this.restartForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
this.field?.undo(); // Undoes all
|
||||
blurActiveElement();
|
||||
}
|
||||
);
|
||||
$("#restart").addEventListener("click", () => this.field?.undo()); // Undoes all
|
||||
|
||||
// Seed
|
||||
this.seedInput = $("#seed");
|
||||
this.seedOpenForm = $("#seedOpenForm");
|
||||
this.seedOpenForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
this.seedOverlay.show();
|
||||
this.seedInput.value = this.seed;
|
||||
this.seedInput.focus();
|
||||
setTimeout(() => this.seedInput.select(), 0);
|
||||
}
|
||||
);
|
||||
this.seedOverlay = new Overlay(
|
||||
$("#seedOverlay"),
|
||||
$("#seedForm"),
|
||||
$("#seedCancelForm"),
|
||||
() => {
|
||||
const seedInput = $("#seed");
|
||||
const seedDialog = new ModalDialog({
|
||||
dialog: $("#seed-dialog"),
|
||||
openButton: $("#seed-open"),
|
||||
onOpen: () => seedInput.value = this.seed,
|
||||
form: $("#seed-form"),
|
||||
closeButton: $("#seed-cancel"),
|
||||
submitButton: $("#seed-submit"),
|
||||
onSubmit: () => {
|
||||
this.initNewField(
|
||||
this.field?.width,
|
||||
this.field?.height,
|
||||
this.field?.mineCount,
|
||||
this.field?.isSolvable,
|
||||
this.seedInput.value
|
||||
seedInput.value
|
||||
);
|
||||
seedDialog.close();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Undo
|
||||
this.undoForm = $("#undoForm");
|
||||
this.undoForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
this.field?.undo(1);
|
||||
blurActiveElement();
|
||||
}
|
||||
);
|
||||
$("#undo").addEventListener("click", () => this.field?.undo(1));
|
||||
|
||||
// Redo
|
||||
this.redoForm = $("#redoForm");
|
||||
this.redoForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
this.field?.redo(1);
|
||||
blurActiveElement();
|
||||
}
|
||||
);
|
||||
$("#redo").addEventListener("click", () => this.field?.redo(1));
|
||||
|
||||
// Hint
|
||||
this.hintForm = $("#hintForm");
|
||||
this.hintForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.field !== null) {
|
||||
$("#hint").addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
if (this.field != null) {
|
||||
this.statistics.hintsRequested++;
|
||||
this.display.hintSquare = Solver.getHint(this.field);
|
||||
}
|
||||
blurActiveElement();
|
||||
}
|
||||
);
|
||||
|
||||
// Solve
|
||||
this.solveForm = $("#solveForm");
|
||||
this.solveForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.field !== null) {
|
||||
$("#solve").addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
if (this.field != null) {
|
||||
this.statistics.solverUsages++;
|
||||
Solver.solve(this.field);
|
||||
}
|
||||
blurActiveElement();
|
||||
}
|
||||
);
|
||||
|
||||
// Preferences
|
||||
this.enableMarksInput = $("#preferencesEnableMarks");
|
||||
this.showTooManyFlagsHintsInput = $("#preferencesShowTooManyFlagsHints");
|
||||
this.preferencesOpenForm = $("#preferencesOpenForm");
|
||||
this.preferencesOpenForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
const enableMarksInput = $("#preferences-enable-marks");
|
||||
const showTooManyFlagsHintsInput = $("#preferences-show-too-many-flags-hints");
|
||||
const preferencesDialog = new ModalDialog({
|
||||
dialog: $("#preferences-dialog"),
|
||||
openButton: $("#preferences-open"),
|
||||
onOpen: () => {
|
||||
enableMarksInput.checked = preferences.marksEnabled;
|
||||
showTooManyFlagsHintsInput.checked = preferences.showTooManyFlagsHints;
|
||||
},
|
||||
form: $("#preferences-form"),
|
||||
closeButton: $("#preferences-cancel"),
|
||||
submitButton: $("#preferences-submit"),
|
||||
onSubmit: () => {
|
||||
preferences.marksEnabled = enableMarksInput.checked;
|
||||
preferences.showTooManyFlagsHints = showTooManyFlagsHintsInput.checked;
|
||||
|
||||
this.enableMarksInput.checked = preferences.marksEnabled;
|
||||
this.showTooManyFlagsHintsInput.checked = preferences.showTooManyFlagsHints;
|
||||
this.preferencesOverlay.show();
|
||||
blurActiveElement();
|
||||
preferencesDialog.close();
|
||||
}
|
||||
);
|
||||
this.preferencesOverlay = new Overlay(
|
||||
$("#preferencesOverlay"),
|
||||
$("#preferencesForm"),
|
||||
$("#preferencesCancelForm"),
|
||||
() => {
|
||||
preferences.marksEnabled = this.enableMarksInput.checked;
|
||||
preferences.showTooManyFlagsHints = this.showTooManyFlagsHintsInput.checked;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Statistics
|
||||
this.statisticsDiv = $("#statisticsDiv");
|
||||
this.statisticsOpenForm = $("#statisticsOpenForm");
|
||||
this.statisticsOpenForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
this.statisticsOverlay.show();
|
||||
blurActiveElement();
|
||||
}
|
||||
);
|
||||
this.statisticsOverlay = new Overlay(
|
||||
$("#statisticsOverlay"),
|
||||
null,
|
||||
$("#statisticsCloseForm")
|
||||
);
|
||||
this.statisticsResetForm = $("#statisticsResetForm");
|
||||
this.statisticsResetForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
this.statisticsDiv = $("#statistics-div");
|
||||
new ModalDialog({
|
||||
dialog: $("#statistics-dialog"),
|
||||
openButton: $("#statistics-open"),
|
||||
onOpen: () => this.updateStatistics(),
|
||||
closeButton: $("#statistics-close"),
|
||||
submitButton: $("#statistics-reset"),
|
||||
onSubmit: () => {
|
||||
if (!window.confirm("Are you sure you want to reset all statistics? This cannot be undone."))
|
||||
return;
|
||||
|
||||
this.statistics.clear();
|
||||
this.updateStatistics();
|
||||
}
|
||||
);
|
||||
this.updateStatistics();
|
||||
});
|
||||
|
||||
// High scores
|
||||
this.highScoresDiv = $("#highScoresDiv");
|
||||
this.highScoresOpenForm = $("#highScoresOpenForm");
|
||||
this.highScoresOpenForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
this.highScoresDiv.innerHTML = this.highScores.generateHtmlReport();
|
||||
this.highScoresOverlay.show();
|
||||
blurActiveElement();
|
||||
}
|
||||
);
|
||||
this.highScoresOverlay = new Overlay(
|
||||
$("#highScoresOverlay"),
|
||||
null,
|
||||
$("#highScoresCloseForm")
|
||||
);
|
||||
this.highScoresResetForm = $("#highScoresResetForm");
|
||||
this.highScoresResetForm.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
this.highScoresDiv = $("#high-scores-div");
|
||||
new ModalDialog({
|
||||
dialog: $("#high-scores-dialog"),
|
||||
openButton: $("#high-scores-open"),
|
||||
onOpen: () => this.highScoresDiv.innerHTML = this.highScores.generateHtmlReport(),
|
||||
closeButton: $("#high-scores-close"),
|
||||
submitButton: $("#high-scores-reset"),
|
||||
onSubmit: () => {
|
||||
if (!window.confirm("Are you sure you want to reset all high scores? This cannot be undone."))
|
||||
return;
|
||||
|
||||
this.highScores.clear();
|
||||
this.highScoresDiv.innerHTML = this.highScores.generateHtmlReport();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Canvas
|
||||
this.canvas.addEventListener(
|
||||
"mousemove",
|
||||
event => {
|
||||
this.display.mouseSquare = this.field?.getSquareOrElse(
|
||||
this.display.posToSquare({x: event.clientX, y: event.clientY}),
|
||||
null
|
||||
);
|
||||
const squarePos = this.display.posToSquare({x: event.clientX, y: event.clientY});
|
||||
this.display.mouseSquare = this.field?.getSquareOrElse(squarePos, null) ?? null;
|
||||
}
|
||||
);
|
||||
this.canvas.addEventListener(
|
||||
|
@ -357,11 +250,11 @@ export class Game {
|
|||
"mousedown",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
if (this.field === null) return;
|
||||
if (this.field == null) return;
|
||||
|
||||
this.field.runUndoably(() => {
|
||||
const coords = this.display.posToSquare({x: event.clientX, y: event.clientY});
|
||||
if (this.field === null || !this.field.hasSquareAt(coords)) return;
|
||||
if (this.field == null || !this.field.hasSquareAt(coords)) return;
|
||||
|
||||
switch (event.button) {
|
||||
case 0:
|
||||
|
@ -370,7 +263,7 @@ export class Game {
|
|||
case 2:
|
||||
if (!this.leftDown) {
|
||||
const square = this.field.getSquareOrElse(coords);
|
||||
if (square !== null) {
|
||||
if (square != null) {
|
||||
if (square.hasFlag) {
|
||||
this.field.toggleFlag(coords);
|
||||
if (preferences.marksEnabled)
|
||||
|
@ -397,11 +290,11 @@ export class Game {
|
|||
"mouseup",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
if (this.field === null) return;
|
||||
if (this.field == null) return;
|
||||
|
||||
this.field.runUndoably(() => {
|
||||
const coords = this.display.posToSquare({x: event.clientX, y: event.clientY});
|
||||
if (this.field === null || !this.field.hasSquareAt(coords)) return;
|
||||
if (this.field == null || !this.field.hasSquareAt(coords)) return;
|
||||
|
||||
switch (event.button) {
|
||||
case 0:
|
||||
|
@ -459,7 +352,7 @@ export class Game {
|
|||
* @param height the height of the field
|
||||
* @param mineCount the number of mines to place in the field
|
||||
* @param solvable whether the field is guaranteed to be solvable
|
||||
* @param seed the seed to generate the field width, or `null` if a new field should be chosen
|
||||
* @param seed the seed to generate the field width, or `undefined` to use a random seed
|
||||
* @private
|
||||
*/
|
||||
private initNewField(
|
||||
|
@ -467,7 +360,7 @@ export class Game {
|
|||
height: number = defaultDifficulty.height,
|
||||
mineCount: number = defaultDifficulty.mineCount,
|
||||
solvable: boolean = defaultDifficulty.solvable,
|
||||
seed: string | null = null
|
||||
seed?: string
|
||||
) {
|
||||
this.seed = seed ?? "" + this.rng.uint32();
|
||||
this.field = new Field(
|
||||
|
@ -482,13 +375,11 @@ export class Game {
|
|||
let lastTime: number | null = null;
|
||||
window.clearInterval(this.statisticsTimer);
|
||||
this.statisticsTimer = window.setInterval(() => {
|
||||
if (this.field === null) return;
|
||||
if (this.field == null) return;
|
||||
|
||||
const elapsedTime = this.field.elapsedTime;
|
||||
this.statistics.timeSpent += elapsedTime - (lastTime ?? 0);
|
||||
lastTime = elapsedTime;
|
||||
|
||||
this.updateStatistics();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const {LocalStorage} = (window as any).fwdekker.storage;
|
||||
|
||||
import {formatTime} from "./Common";
|
||||
import {difficulties, Difficulty} from "./Difficulty";
|
||||
// @ts-ignore
|
||||
const {Storage, LocalStorage} = window.fwdekker.storage;
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @ts-ignore
|
||||
const {$, doAfterLoad, footer, header, nav} = window.fwdekker;
|
||||
const {doAfterLoad} = (window as any).fwdekker;
|
||||
|
||||
import {waitForForkAwesome} from "./Common";
|
||||
import {BasicIconFont, ForkAwesomeFont} from "./Display";
|
||||
import {Game} from "./Game";
|
||||
|
@ -7,17 +7,6 @@ import {Preferences} from "./Preferences";
|
|||
|
||||
|
||||
doAfterLoad(() => {
|
||||
// Initialize template
|
||||
$("#nav").appendChild(nav("/Tools/Minesweeper/"));
|
||||
$("#header").appendChild(header({title: "Minesweeper"}));
|
||||
$("#footer").appendChild(footer({
|
||||
vcsURL: "https://git.fwdekker.com/tools/minesweeper/",
|
||||
version: "v%%VERSION_NUMBER%%"
|
||||
}));
|
||||
$("main").classList.remove("hidden");
|
||||
|
||||
|
||||
// Start game
|
||||
const preferences = new Preferences();
|
||||
waitForForkAwesome(
|
||||
() => {
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
const {$} = (window as any).fwdekker;
|
||||
|
||||
import {blurActiveElement} from "./Common";
|
||||
|
||||
|
||||
/**
|
||||
* A modal dialog displayed in HTML.
|
||||
*/
|
||||
export class ModalDialog {
|
||||
private readonly dialog: HTMLElement;
|
||||
private readonly openButton?: HTMLElement;
|
||||
private readonly onOpen?: () => void;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new modal dialog wrapper.
|
||||
*
|
||||
* @param dialog the dialog maintained by this instance
|
||||
* @param openButton the element that opens the dialog when clicked
|
||||
* @param onOpen the callback to invoke when the dialog is opened
|
||||
* @param form the form contained in the dialog
|
||||
* @param closeButton the element that closes the dialog's form when clicked
|
||||
* @param submitButton the element that submits the dialog's form when clicked
|
||||
* @param onSubmit the callback to invoke when the dialog's form is submitted
|
||||
*/
|
||||
constructor(
|
||||
{
|
||||
dialog,
|
||||
openButton,
|
||||
onOpen,
|
||||
form,
|
||||
closeButton,
|
||||
submitButton,
|
||||
onSubmit
|
||||
}: {
|
||||
dialog: HTMLElement,
|
||||
openButton?: HTMLElement,
|
||||
onOpen?: (() => void),
|
||||
form?: HTMLFormElement,
|
||||
closeButton?: HTMLElement,
|
||||
submitButton?: HTMLElement,
|
||||
onSubmit?: (() => void)
|
||||
}
|
||||
) {
|
||||
this.dialog = dialog;
|
||||
this.openButton = openButton;
|
||||
this.onOpen = onOpen;
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
event => {
|
||||
if (!(event.target instanceof Node)) return;
|
||||
|
||||
console.log("close");
|
||||
if (event.target !== openButton && !this.dialog.contains(event.target) || this.dialog === event.target)
|
||||
this.close();
|
||||
}
|
||||
);
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
event => {
|
||||
if (event.key === "Escape" && this.isOpen())
|
||||
this.close();
|
||||
}
|
||||
);
|
||||
|
||||
openButton?.addEventListener(
|
||||
"click",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
this.open();
|
||||
}
|
||||
);
|
||||
closeButton?.addEventListener(
|
||||
"click",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
);
|
||||
|
||||
form?.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
onSubmit?.();
|
||||
}
|
||||
);
|
||||
submitButton?.addEventListener(
|
||||
"click",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
onSubmit?.();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Opens the dialog.
|
||||
*/
|
||||
open(): void {
|
||||
console.log("opening");
|
||||
blurActiveElement();
|
||||
setTimeout(() => $("[autofocus]", this.dialog)?.focus(), 100);
|
||||
|
||||
this.dialog.setAttribute("open", "true");
|
||||
this.onOpen?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the dialog.
|
||||
*/
|
||||
close(): void {
|
||||
if (this.isOpen()) {
|
||||
this.dialog.removeAttribute("open");
|
||||
setTimeout(() => this.openButton?.focus(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if and only if this dialog is currently open.
|
||||
*/
|
||||
isOpen(): boolean {
|
||||
return this.dialog.hasAttribute("open");
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
const {LocalStorage} = (window as any).fwdekker.storage;
|
||||
|
||||
import {BasicIconFont, IconFont} from "./Display";
|
||||
// @ts-ignore
|
||||
const {Storage, LocalStorage} = window.fwdekker.storage;
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -220,7 +220,8 @@ export class Solver {
|
|||
if (!adjacentSquaresOnly)
|
||||
matrix.push(Array(unknowns.length).fill(1).concat(field.mineCount - field.flagCount));
|
||||
|
||||
return new Matrix(matrix).solveBinary()
|
||||
return (new Matrix(matrix))
|
||||
.solveBinary()
|
||||
.map((it, i) => it === undefined ? undefined : [it, unknowns[i]]);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const {LocalStorage} = (window as any).fwdekker.storage;
|
||||
|
||||
import {formatTime} from "./Common";
|
||||
// @ts-ignore
|
||||
const {Storage, LocalStorage} = window.fwdekker.storage;
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
import {blurActiveElement} from "./Common";
|
||||
|
||||
|
||||
/**
|
||||
* An overlay displayed in HTML.
|
||||
*/
|
||||
export class Overlay {
|
||||
private readonly overlay: HTMLDivElement;
|
||||
private readonly submitForm: HTMLFormElement | null;
|
||||
private readonly cancelForm: HTMLFormElement | null;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new overlay.
|
||||
*
|
||||
* @param overlay the overlay element to show and hide
|
||||
* @param submitForm the form that invokes `onSubmit` and closes the overlay when submitted
|
||||
* @param cancelForm the form that closes the overlay when submitted
|
||||
* @param onSubmit the callback to invoke when the form is submit
|
||||
*/
|
||||
constructor(
|
||||
overlay: HTMLDivElement,
|
||||
submitForm: HTMLFormElement | null,
|
||||
cancelForm: HTMLFormElement | null,
|
||||
onSubmit: (() => void) | null = null
|
||||
) {
|
||||
this.overlay = overlay;
|
||||
overlay.addEventListener(
|
||||
"mousedown",
|
||||
event => {
|
||||
if (event.target === overlay)
|
||||
this.hide();
|
||||
}
|
||||
);
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
event => {
|
||||
if (event.key === "Escape" && this.isVisible())
|
||||
this.hide();
|
||||
}
|
||||
);
|
||||
|
||||
this.submitForm = submitForm;
|
||||
submitForm?.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
this.hide();
|
||||
onSubmit?.();
|
||||
}
|
||||
);
|
||||
|
||||
this.cancelForm = cancelForm;
|
||||
cancelForm?.addEventListener(
|
||||
"submit",
|
||||
event => {
|
||||
event.preventDefault();
|
||||
|
||||
this.hide();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows the overlay.
|
||||
*/
|
||||
show(): void {
|
||||
this.overlay.classList.remove("hidden");
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the overlay.
|
||||
*/
|
||||
hide(): void {
|
||||
this.overlay.classList.add("hidden");
|
||||
blurActiveElement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if and only if this overlay is currently visible.
|
||||
*/
|
||||
isVisible(): boolean {
|
||||
return !this.overlay.classList.contains("hidden");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue