mirror of
https://github.com/litruv/lit.ruv.wtf.git
synced 2026-07-24 02:36:02 +10:00
Add Number Match game page and update sitemap
This commit is contained in:
498
src/numbermatch.html
Normal file
498
src/numbermatch.html
Normal file
@@ -0,0 +1,498 @@
|
||||
<!--
|
||||
METADATA:{
|
||||
"title": "Number Match - Litruv",
|
||||
"metaDescription": "Play Number Match right on Litruv's site. Pair numbers that match or add to ten and clear the board.",
|
||||
"csp": "script-src 'self' 'unsafe-inline';",
|
||||
"ogTitle": "Number Match - Litruv",
|
||||
"ogDescription": "Clear the board by pairing numbers that match or add to ten.",
|
||||
"ogUrl": "https://lit.ruv.wtf/numbermatch.html",
|
||||
"twitterTitle": "Number Match - Litruv",
|
||||
"twitterDescription": "Clear the board by pairing numbers that match or add to ten.",
|
||||
"twitterUrl": "https://lit.ruv.wtf/numbermatch.html",
|
||||
"useCommonHeader": true,
|
||||
"useCommonFooter": true
|
||||
}
|
||||
-->
|
||||
<!-- START_STYLES -->
|
||||
<style>
|
||||
.numbermatch-page {
|
||||
margin-top: 2.5rem;
|
||||
padding: 0 1rem 3.5rem;
|
||||
color: #1f2933;
|
||||
}
|
||||
|
||||
.numbermatch-page .sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.numbermatch-page .app {
|
||||
display: grid;
|
||||
gap: 2.5rem;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.numbermatch-page .app__game,
|
||||
.numbermatch-page .app__info {
|
||||
background: var(--post-bg);
|
||||
border: 2px solid var(--post-border);
|
||||
box-shadow: 4px 6px 0 var(--shadow-light);
|
||||
padding: 2rem 2.25rem;
|
||||
position: relative;
|
||||
transform-origin: center;
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
|
||||
.numbermatch-page .app__game {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.numbermatch-page .app__info {
|
||||
transform: rotate(0.55deg);
|
||||
}
|
||||
|
||||
.numbermatch-page .app__game:hover {
|
||||
transform: scale(1.01);
|
||||
box-shadow: 6px 9px 0 var(--shadow-light);
|
||||
}
|
||||
|
||||
.numbermatch-page .app__info:hover {
|
||||
transform: rotate(0deg) scale(1.01);
|
||||
box-shadow: 6px 9px 0 var(--shadow-light);
|
||||
}
|
||||
|
||||
.numbermatch-page .app__title {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
text-shadow: 1px 1px 0 #fff;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.numbermatch-page .app__subtitle {
|
||||
margin: 0.5rem 0 0;
|
||||
color: #364152;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.numbermatch-page .controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.85rem;
|
||||
margin: 2rem 0 1.5rem;
|
||||
}
|
||||
|
||||
.numbermatch-page .control-button {
|
||||
border: 2px solid var(--post-border);
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
padding: 0.65rem 1.75rem;
|
||||
box-shadow: 3px 4px 0 var(--shadow-light);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.numbermatch-page .control-button:hover,
|
||||
.numbermatch-page .control-button:focus {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 5px 6px 0 var(--shadow-light);
|
||||
}
|
||||
|
||||
.numbermatch-page .control-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 2px 2px 0 var(--shadow-light);
|
||||
}
|
||||
|
||||
.numbermatch-page .control-button--secondary {
|
||||
background: var(--main-bg);
|
||||
color: var(--main-fg);
|
||||
}
|
||||
|
||||
.numbermatch-page .status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
background: rgba(36, 36, 36, 0.04);
|
||||
border: 1px dashed rgba(0, 0, 0, 0.12);
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.numbermatch-page .status__message {
|
||||
min-height: 1.5rem;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: #1f2933;
|
||||
}
|
||||
|
||||
.numbermatch-page .status__message.status__message--error {
|
||||
color: #be123c;
|
||||
}
|
||||
|
||||
.numbermatch-page .status__message.status__message--success {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.numbermatch-page .status__metrics {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
font-size: 0.95rem;
|
||||
color: #364152;
|
||||
}
|
||||
|
||||
.numbermatch-page .status__metric strong {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.numbermatch-page .board {
|
||||
margin-top: 1.75rem;
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.numbermatch-page .board-actions {
|
||||
margin-top: 1.75rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.numbermatch-page .board-action {
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--post-border);
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
color: var(--main-bg);
|
||||
background: var(--accent);
|
||||
box-shadow: 3px 4px 0 var(--shadow-light);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.numbermatch-page .board-action:hover,
|
||||
.numbermatch-page .board-action:focus {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 5px 6px 0 var(--shadow-light);
|
||||
}
|
||||
|
||||
.numbermatch-page .board-action:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 2px 2px 0 var(--shadow-light);
|
||||
}
|
||||
|
||||
.numbermatch-page .board-action--secondary {
|
||||
background: var(--post-bg);
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.numbermatch-page .board-action__badge {
|
||||
position: absolute;
|
||||
top: -0.45rem;
|
||||
right: -0.45rem;
|
||||
min-width: 1.5rem;
|
||||
padding: 0.2rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
background: var(--like);
|
||||
color: #fff;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 6px 12px rgba(219, 39, 119, 0.35);
|
||||
}
|
||||
|
||||
.numbermatch-page .board-action__badge.board-action__badge--empty {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
color: #111827;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.numbermatch-page .board-action--attention {
|
||||
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.18), 0 12px 24px rgba(37, 99, 235, 0.3);
|
||||
animation: numbermatch-attention 0.95s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes numbermatch-attention {
|
||||
from {
|
||||
transform: translateY(-2px) scale(1);
|
||||
}
|
||||
to {
|
||||
transform: translateY(-3px) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.numbermatch-page .tile {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
border-radius: 14px;
|
||||
border: 1.5px solid var(--post-border);
|
||||
font-size: clamp(1.1rem, 4vw, 1.55rem);
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
background: linear-gradient(165deg, #fffef4 0%, #f7f0b7 100%);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 2px 3px 0 rgba(0, 0, 0, 0.15);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.numbermatch-page .tile:hover,
|
||||
.numbermatch-page .tile:focus {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 4px 6px 0 rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.numbermatch-page .tile--selected {
|
||||
background: linear-gradient(160deg, #fde68a 0%, #fb923c 100%);
|
||||
box-shadow: 4px 6px 0 rgba(251, 191, 36, 0.4);
|
||||
}
|
||||
|
||||
.numbermatch-page .tile--hinted:not(.tile--selected) {
|
||||
background: linear-gradient(160deg, #bfdbfe 0%, #93c5fd 100%);
|
||||
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.35), 4px 6px 0 rgba(37, 99, 235, 0.25);
|
||||
}
|
||||
|
||||
.numbermatch-page .tile--empty {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: 1.5px dashed rgba(0, 0, 0, 0.25);
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.numbermatch-page .tile--empty:hover,
|
||||
.numbermatch-page .tile--empty:focus {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.numbermatch-page .app__info h2,
|
||||
.numbermatch-page .app__info h3 {
|
||||
margin-top: 0;
|
||||
color: #111827;
|
||||
text-shadow: 1px 1px 0 #fff;
|
||||
}
|
||||
|
||||
.numbermatch-page .app__info p,
|
||||
.numbermatch-page .app__info li {
|
||||
color: #364152;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.numbermatch-page .app__info ul {
|
||||
padding-left: 1.25rem;
|
||||
margin: 0.75rem 0 1.5rem;
|
||||
}
|
||||
|
||||
.numbermatch-dialog-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.numbermatch-dialog-backdrop.is-visible {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.numbermatch-dialog {
|
||||
background: var(--post-bg);
|
||||
border: 2px solid var(--post-border);
|
||||
box-shadow: 6px 8px 0 var(--shadow-light);
|
||||
padding: 1.75rem 2rem;
|
||||
max-width: 420px;
|
||||
width: calc(100% - 2rem);
|
||||
transform: rotate(-1deg);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.numbermatch-dialog h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1.35rem;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.numbermatch-dialog p {
|
||||
margin: 0 0 1.5rem;
|
||||
color: #374151;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.numbermatch-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.numbermatch-dialog-button {
|
||||
border: 2px solid var(--post-border);
|
||||
padding: 0.55rem 1.35rem;
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow: 3px 4px 0 var(--shadow-light);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||
background: var(--post-bg);
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.numbermatch-dialog-button:hover,
|
||||
.numbermatch-dialog-button:focus-visible {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 5px 6px 0 var(--shadow-light);
|
||||
}
|
||||
|
||||
.numbermatch-dialog-button--confirm {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.numbermatch-dialog-button--danger {
|
||||
background: var(--like);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.numbermatch-page .app {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.numbermatch-page .app__game,
|
||||
.numbermatch-page .app__info {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.numbermatch-page {
|
||||
padding: 0 0 3rem;
|
||||
}
|
||||
|
||||
.numbermatch-page .app__game,
|
||||
.numbermatch-page .app__info {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.numbermatch-page .board {
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.numbermatch-page .board-action {
|
||||
width: 3.1rem;
|
||||
height: 3.1rem;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.numbermatch-page .board-action__badge {
|
||||
top: -0.25rem;
|
||||
right: -0.25rem;
|
||||
min-width: 1.35rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- END_STYLES -->
|
||||
|
||||
<!-- START_SCRIPTS_DEFER -->
|
||||
<script src="numbermatch.js" defer></script>
|
||||
<!-- END_SCRIPTS_DEFER -->
|
||||
|
||||
<div class="numbermatch-page">
|
||||
<div class="app">
|
||||
<section class="app__game" aria-label="Number Match board and controls">
|
||||
<header class="app__header">
|
||||
<h1 class="app__title">Number Match</h1>
|
||||
<p class="app__subtitle">Clear the board by pairing numbers that match or add to ten.</p>
|
||||
</header>
|
||||
<div class="controls" role="group" aria-label="Game controls">
|
||||
<button id="newGameButton" type="button" class="control-button control-button--secondary">New Game</button>
|
||||
</div>
|
||||
<section class="status" aria-live="polite">
|
||||
<p id="message" class="status__message" role="status"></p>
|
||||
<div class="status__metrics">
|
||||
<span class="status__metric">
|
||||
Matches cleared: <strong id="matchesCount">0</strong>
|
||||
</span>
|
||||
<span class="status__metric">
|
||||
Tiles remaining: <strong id="tilesRemaining">0</strong>
|
||||
</span>
|
||||
<span class="status__metric">
|
||||
Add Numbers used: <strong id="addNumbersUsed">0/3</strong>
|
||||
</span>
|
||||
<span class="status__metric">
|
||||
Hints used: <strong id="hintsUsed">0</strong>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
<section class="board" id="board" aria-label="Number grid"></section>
|
||||
<div class="board-actions">
|
||||
<button id="addNumbersButton" type="button" class="board-action board-action--primary" aria-label="Add numbers">
|
||||
<span aria-hidden="true" class="board-action__icon">+</span>
|
||||
<span id="addNumbersCounter" class="board-action__badge" aria-hidden="true">3</span>
|
||||
<span class="sr-only">Add numbers</span>
|
||||
</button>
|
||||
<button id="hintButton" type="button" class="board-action board-action--secondary" aria-label="Hint">
|
||||
<span aria-hidden="true">?</span>
|
||||
<span class="sr-only">Hint</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<aside class="app__info" aria-labelledby="info-heading">
|
||||
<h2 id="info-heading">How To Play</h2>
|
||||
<p>Select two tiles that are either identical (like 8 and 8) or sum to ten (for example, 3 and 7). Clear every number to win.</p>
|
||||
<h3>Match Rules</h3>
|
||||
<ul>
|
||||
<li>Paths can run horizontally, vertically, diagonally, or wrap in reading order.</li>
|
||||
<li>Every cell between selections must be empty along the chosen path.</li>
|
||||
<li>Wrap-around allows the end of a row to connect to the start of the next when the path is clear.</li>
|
||||
</ul>
|
||||
<h3>Strategy & Tips</h3>
|
||||
<ul>
|
||||
<li>Scan rows and columns for classic pairs like 9-1, 8-2, 7-3, 6-4, and 5-5.</li>
|
||||
<li>Use diagonal and wrap-around matches to reach blocked numbers.</li>
|
||||
<li><em>Add Numbers</em> unlocks fresh tiles in three batches—save it for when the board runs dry.</li>
|
||||
</ul>
|
||||
<h3>Frequently Asked Questions</h3>
|
||||
<p><strong>Which numbers appear?</strong> Numbers 1-9 are distributed to encourage plenty of identical and ten-sum pairs.</p>
|
||||
<p><strong>What counts as wrap-around?</strong> Reading order connections across a row break, provided every intervening cell is empty.</p>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="numbermatch-dialog-backdrop" id="newGameDialogBackdrop" hidden>
|
||||
<div class="numbermatch-dialog" id="newGameDialog" role="dialog" aria-modal="true" aria-labelledby="newGameDialogTitle" aria-describedby="newGameDialogDescription" tabindex="-1">
|
||||
<h2 id="newGameDialogTitle">Start a fresh puzzle?</h2>
|
||||
<p id="newGameDialogDescription">Your current board and progress will be replaced with a new layout. You can always reload the page to resume later.</p>
|
||||
<div class="numbermatch-dialog-actions">
|
||||
<button type="button" class="numbermatch-dialog-button" id="cancelNewGameButton">Keep playing</button>
|
||||
<button type="button" class="numbermatch-dialog-button numbermatch-dialog-button--danger" id="confirmNewGameButton">New game</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,5 +26,6 @@
|
||||
<a href="https://mates.dev" target="_blank">MatesMedia</a>
|
||||
<a href="https://lit.ruv.wtf/materials/" target="_blank">UE Material Snippets</a>
|
||||
<a href="https://lit.ruv.wtf/docs/" target="_blank">Documentation</a>
|
||||
<a href="https://lit.ruv.wtf/docs/numbermatch/">Number Match</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -508,6 +508,11 @@ class NumberMatchUI {
|
||||
this.addNumbersButton = /** @type {HTMLButtonElement} */ (document.getElementById("addNumbersButton"));
|
||||
this.hintButton = /** @type {HTMLButtonElement} */ (document.getElementById("hintButton"));
|
||||
this.newGameButton = /** @type {HTMLButtonElement} */ (document.getElementById("newGameButton"));
|
||||
this.newGameDialogBackdrop = /** @type {HTMLElement | null} */ (document.getElementById("newGameDialogBackdrop"));
|
||||
this.newGameDialog = /** @type {HTMLElement | null} */ (document.getElementById("newGameDialog"));
|
||||
this.newGameConfirmButton = /** @type {HTMLButtonElement | null} */ (document.getElementById("confirmNewGameButton"));
|
||||
this.newGameCancelButton = /** @type {HTMLButtonElement | null} */ (document.getElementById("cancelNewGameButton"));
|
||||
this.previouslyFocusedElement = null;
|
||||
|
||||
this.selectedIndices = [];
|
||||
this.matchesCleared = 0;
|
||||
@@ -524,9 +529,16 @@ class NumberMatchUI {
|
||||
*/
|
||||
init() {
|
||||
this.bindControlEvents();
|
||||
const restored = this.restoreState();
|
||||
this.renderBoard();
|
||||
this.updateMetrics();
|
||||
this.showMessage("Select two tiles that match or sum to ten.");
|
||||
this.hintButton.disabled = this.game.isComplete();
|
||||
if (restored) {
|
||||
this.showMessage("Welcome back! Your last board was restored.", "success");
|
||||
} else {
|
||||
this.showMessage("Select two tiles that match or sum to ten.");
|
||||
}
|
||||
this.persistState();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -546,6 +558,7 @@ class NumberMatchUI {
|
||||
this.clearHighlights();
|
||||
this.renderBoard();
|
||||
this.updateMetrics();
|
||||
this.persistState();
|
||||
} else {
|
||||
this.showMessage("No numbers remain to duplicate.", "error");
|
||||
this.updateAddNumbersPrompt();
|
||||
@@ -565,11 +578,36 @@ class NumberMatchUI {
|
||||
this.renderBoard();
|
||||
this.updateMetrics();
|
||||
this.showMessage("Try matching the highlighted tiles.");
|
||||
this.persistState();
|
||||
});
|
||||
|
||||
this.newGameButton.addEventListener("click", () => {
|
||||
this.startNewGame();
|
||||
this.openNewGameDialog();
|
||||
});
|
||||
|
||||
if (this.newGameDialogBackdrop && this.newGameConfirmButton && this.newGameCancelButton) {
|
||||
this.newGameConfirmButton.addEventListener("click", () => {
|
||||
this.closeNewGameDialog();
|
||||
this.startNewGame();
|
||||
});
|
||||
|
||||
this.newGameCancelButton.addEventListener("click", () => {
|
||||
this.closeNewGameDialog();
|
||||
});
|
||||
|
||||
this.newGameDialogBackdrop.addEventListener("mousedown", (event) => {
|
||||
if (event.target === this.newGameDialogBackdrop) {
|
||||
this.closeNewGameDialog();
|
||||
}
|
||||
});
|
||||
|
||||
this.newGameDialogBackdrop.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
this.closeNewGameDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -620,6 +658,7 @@ class NumberMatchUI {
|
||||
this.hintButton.disabled = true;
|
||||
this.clearHighlights();
|
||||
}
|
||||
this.persistState();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -677,6 +716,7 @@ class NumberMatchUI {
|
||||
this.hintButton.disabled = true;
|
||||
this.clearHighlights();
|
||||
}
|
||||
this.persistState();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -825,6 +865,49 @@ class NumberMatchUI {
|
||||
this.renderBoard();
|
||||
this.updateMetrics();
|
||||
this.showMessage("New puzzle loaded. Good luck!", "success");
|
||||
this.hintButton.disabled = false;
|
||||
this.persistState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the custom new game confirmation dialog.
|
||||
* @returns {void}
|
||||
*/
|
||||
openNewGameDialog() {
|
||||
if (!this.newGameDialogBackdrop || !this.newGameDialog || !this.newGameConfirmButton) {
|
||||
this.startNewGame();
|
||||
return;
|
||||
}
|
||||
|
||||
this.previouslyFocusedElement = document.activeElement;
|
||||
this.newGameDialogBackdrop.hidden = false;
|
||||
requestAnimationFrame(() => {
|
||||
this.newGameDialogBackdrop.classList.add("is-visible");
|
||||
this.newGameDialogBackdrop.setAttribute("aria-hidden", "false");
|
||||
this.newGameDialog.focus({ preventScroll: true });
|
||||
this.newGameConfirmButton.focus({ preventScroll: true });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the custom new game dialog and restore focus.
|
||||
* @returns {void}
|
||||
*/
|
||||
closeNewGameDialog() {
|
||||
if (!this.newGameDialogBackdrop) {
|
||||
return;
|
||||
}
|
||||
this.newGameDialogBackdrop.classList.remove("is-visible");
|
||||
this.newGameDialogBackdrop.setAttribute("aria-hidden", "true");
|
||||
setTimeout(() => {
|
||||
if (this.newGameDialogBackdrop) {
|
||||
this.newGameDialogBackdrop.hidden = true;
|
||||
}
|
||||
}, 180);
|
||||
|
||||
if (this.previouslyFocusedElement instanceof HTMLElement) {
|
||||
this.previouslyFocusedElement.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -850,8 +933,105 @@ class NumberMatchUI {
|
||||
this.updateAddNumbersBadge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the current board and score metrics to localStorage.
|
||||
* @returns {void}
|
||||
*/
|
||||
persistState() {
|
||||
try {
|
||||
const storage = window.localStorage;
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
const state = {
|
||||
version: NumberMatchUI.STORAGE_VERSION,
|
||||
width: this.game.width,
|
||||
tiles: this.game.getTiles(),
|
||||
reserveTiles: Array.isArray(this.game.reserveTiles) ? [...this.game.reserveTiles] : undefined,
|
||||
matchesCleared: this.matchesCleared,
|
||||
addNumbersUsed: this.addNumbersUsed,
|
||||
hintsUsed: this.hintsUsed,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
storage.setItem(NumberMatchUI.STORAGE_KEY, JSON.stringify(state));
|
||||
} catch (error) {
|
||||
console.error("[NumberMatch] Failed to persist state", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to restore board and metrics from localStorage.
|
||||
* @returns {boolean} True when a valid state was restored.
|
||||
*/
|
||||
restoreState() {
|
||||
try {
|
||||
const storage = window.localStorage;
|
||||
if (!storage) {
|
||||
return false;
|
||||
}
|
||||
const raw = storage.getItem(NumberMatchUI.STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || parsed.version !== NumberMatchUI.STORAGE_VERSION) {
|
||||
return false;
|
||||
}
|
||||
if (parsed.width !== this.game.width || !Array.isArray(parsed.tiles)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tiles = parsed.tiles.map((value) => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? numeric : null;
|
||||
});
|
||||
|
||||
if (tiles.length === 0 || tiles.every((value) => value === undefined)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.game.tiles = tiles;
|
||||
if (Array.isArray(parsed.reserveTiles)) {
|
||||
this.game.reserveTiles = parsed.reserveTiles
|
||||
.map((value) => Number(value))
|
||||
.filter((value) => Number.isFinite(value));
|
||||
}
|
||||
|
||||
const numericOrDefault = (value, fallback, clampMin = null, clampMax = null) => {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return fallback;
|
||||
}
|
||||
let result = numeric;
|
||||
if (clampMin !== null) {
|
||||
result = Math.max(clampMin, result);
|
||||
}
|
||||
if (clampMax !== null) {
|
||||
result = Math.min(clampMax, result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
this.matchesCleared = numericOrDefault(parsed.matchesCleared, 0, 0, Number.MAX_SAFE_INTEGER);
|
||||
this.addNumbersUsed = numericOrDefault(parsed.addNumbersUsed, 0, 0, NumberMatchUI.ADD_NUMBERS_LIMIT);
|
||||
this.hintsUsed = numericOrDefault(parsed.hintsUsed, 0, 0, Number.MAX_SAFE_INTEGER);
|
||||
this.selectedIndices = [];
|
||||
this.highlightedIndices = [];
|
||||
this.dragStartIndex = null;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[NumberMatch] Failed to restore state", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a valid pair of tiles for hint functionality.
|
||||
NumberMatchUI.STORAGE_KEY = "numbermatch-state-v1";
|
||||
NumberMatchUI.STORAGE_VERSION = 1;
|
||||
* @returns {[number, number] | null} Tuple of indices or null when no match exists.
|
||||
*/
|
||||
findHintPair() {
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
<lastmod>2025-02-09T22:00:52+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://lit.ruv.wtf/numbermatch.html</loc>
|
||||
<lastmod>2025-11-19T00:00:00+00:00</lastmod>
|
||||
<priority>0.70</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://lit.ruv.wtf/docs/sitemap.xml</loc>
|
||||
<lastmod>2025-02-09T22:00:52+00:00</lastmod>
|
||||
@@ -20,7 +25,7 @@
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://lit.ruv.wtf/privacy.html</loc>
|
||||
<lastmod>2025-02-09T22:00:52+00:00</lastmod> <!-- Consider updating this to the actual modification date -->
|
||||
<lastmod>2025-02-09T22:00:52+00:00</lastmod>
|
||||
<priority>0.50</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
|
||||
Reference in New Issue
Block a user