747 lines
24 KiB
HTML
747 lines
24 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
|
<title>Synchrotron Mini Crossword</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: dark;
|
|
--panel: rgba(10, 16, 25, 0.92);
|
|
--line: rgba(255, 255, 255, 0.16);
|
|
--text: #f8f3ea;
|
|
--muted: #aeb8c8;
|
|
--cyan: #65d8e8;
|
|
--gold: #f2c96b;
|
|
--green: #88d76f;
|
|
--red: #ef735f;
|
|
--cell: #f8f3ea;
|
|
--ink: #071014;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
html,
|
|
body {
|
|
min-height: 100%;
|
|
margin: 0;
|
|
}
|
|
|
|
body {
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
-webkit-tap-highlight-color: transparent;
|
|
background:
|
|
radial-gradient(circle at 14% 14%, rgba(101, 216, 232, 0.22), transparent 28%),
|
|
radial-gradient(circle at 86% 78%, rgba(242, 201, 107, 0.13), transparent 32%),
|
|
linear-gradient(140deg, #101926 0%, #1b242d 48%, #080b10 100%);
|
|
color: var(--text);
|
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
}
|
|
|
|
.shell {
|
|
min-height: 100vh;
|
|
display: grid;
|
|
grid-template-rows: auto 1fr auto;
|
|
gap: 14px;
|
|
padding: 16px;
|
|
}
|
|
|
|
header,
|
|
footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.brand {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.brand img {
|
|
width: clamp(42px, 6vw, 60px);
|
|
opacity: 0.58;
|
|
}
|
|
|
|
.brand small {
|
|
color: var(--cyan);
|
|
font-size: 0.78rem;
|
|
font-weight: 900;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
h1 {
|
|
margin: 0;
|
|
font-size: clamp(1.45rem, 3.5vw, 2.55rem);
|
|
line-height: 1;
|
|
}
|
|
|
|
.stats {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
}
|
|
|
|
.stat,
|
|
.panel,
|
|
button {
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
background: var(--panel);
|
|
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28);
|
|
backdrop-filter: blur(12px);
|
|
}
|
|
|
|
.stat {
|
|
min-width: 94px;
|
|
padding: 7px 9px;
|
|
}
|
|
|
|
.stat span {
|
|
display: block;
|
|
color: var(--muted);
|
|
font-size: 0.7rem;
|
|
font-weight: 900;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.stat strong {
|
|
display: block;
|
|
margin-top: 2px;
|
|
font-size: 1rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
main {
|
|
position: relative;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: minmax(500px, 1.08fr) minmax(300px, 0.92fr);
|
|
gap: 14px;
|
|
}
|
|
|
|
main::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 10% 16%;
|
|
background: url("assets/txproducts-logo-watermark.png") center / contain no-repeat;
|
|
opacity: 0.07;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.panel {
|
|
position: relative;
|
|
z-index: 1;
|
|
padding: 16px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.board-wrap {
|
|
display: block;
|
|
margin: 0;
|
|
}
|
|
|
|
.board {
|
|
width: min(100%, 540px);
|
|
max-width: 100%;
|
|
display: grid;
|
|
grid-template-columns: repeat(17, 1fr);
|
|
gap: 2px;
|
|
padding: 6px;
|
|
border-radius: 8px;
|
|
background: rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
.cell {
|
|
position: relative;
|
|
aspect-ratio: 1;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
padding: 0;
|
|
display: grid;
|
|
place-items: center;
|
|
border: 0;
|
|
border-radius: 4px;
|
|
color: var(--ink);
|
|
background: var(--cell);
|
|
font: inherit;
|
|
font-size: clamp(0.76rem, 1.45vw, 1.08rem);
|
|
font-weight: 950;
|
|
text-transform: uppercase;
|
|
cursor: pointer;
|
|
touch-action: manipulation;
|
|
box-shadow: none;
|
|
backdrop-filter: none;
|
|
}
|
|
|
|
.cell.block {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
cursor: default;
|
|
}
|
|
|
|
.cell.active {
|
|
background: var(--gold);
|
|
box-shadow: 0 0 0 3px rgba(242, 201, 107, 0.2);
|
|
}
|
|
|
|
.cell.related {
|
|
background: #d8f6fb;
|
|
}
|
|
|
|
.cell.correct {
|
|
background: var(--green);
|
|
}
|
|
|
|
.num {
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 3px;
|
|
color: rgba(7, 16, 20, 0.68);
|
|
font-size: 0.42rem;
|
|
font-weight: 950;
|
|
}
|
|
|
|
.side {
|
|
display: grid;
|
|
grid-template-rows: minmax(190px, 1fr) auto auto auto;
|
|
gap: 10px;
|
|
}
|
|
|
|
.message {
|
|
margin: 0 0 12px;
|
|
color: var(--gold);
|
|
font-size: clamp(1rem, 2vw, 1.25rem);
|
|
font-weight: 850;
|
|
line-height: 1.32;
|
|
}
|
|
|
|
.clues {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 10px;
|
|
max-height: none;
|
|
overflow: auto;
|
|
}
|
|
|
|
.clues h2 {
|
|
margin: 0 0 8px;
|
|
color: var(--cyan);
|
|
font-size: 0.82rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.clues ol {
|
|
margin: 0;
|
|
padding-left: 20px;
|
|
color: var(--muted);
|
|
font-size: clamp(0.76rem, 1.15vw, 0.9rem);
|
|
line-height: 1.25;
|
|
}
|
|
|
|
.clues li {
|
|
margin-bottom: 4px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.clues li.active {
|
|
color: var(--text);
|
|
font-weight: 850;
|
|
}
|
|
|
|
.keyboard {
|
|
display: grid;
|
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
gap: 7px;
|
|
}
|
|
|
|
.controls {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: 6px;
|
|
}
|
|
|
|
.difficulty {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(72px, 100px));
|
|
gap: 5px;
|
|
justify-content: start;
|
|
}
|
|
|
|
button {
|
|
min-height: 40px;
|
|
padding: 0 8px;
|
|
border-color: rgba(255, 255, 255, 0.18);
|
|
color: var(--text);
|
|
font: inherit;
|
|
font-weight: 900;
|
|
cursor: pointer;
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
button.primary {
|
|
color: #071014;
|
|
background: linear-gradient(135deg, var(--gold), #ffdc83);
|
|
border-color: rgba(255, 220, 131, 0.9);
|
|
}
|
|
|
|
button.active-level {
|
|
color: #071014;
|
|
background: var(--green);
|
|
border-color: rgba(136, 215, 111, 0.85);
|
|
}
|
|
|
|
button.letter {
|
|
min-height: 48px;
|
|
color: #071014;
|
|
background: var(--cyan);
|
|
border-color: rgba(101, 216, 232, 0.8);
|
|
font-size: clamp(1rem, 1.8vw, 1.24rem);
|
|
}
|
|
|
|
.controls button {
|
|
min-height: 38px;
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.difficulty button {
|
|
min-height: 28px;
|
|
font-size: 0.76rem;
|
|
padding: 0 6px;
|
|
}
|
|
|
|
.ghost {
|
|
color: var(--muted);
|
|
background: rgba(255, 255, 255, 0.07);
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
header,
|
|
footer {
|
|
align-items: flex-start;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.stats {
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
main {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.board {
|
|
width: min(100%, 540px);
|
|
}
|
|
|
|
.clues {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 680px) {
|
|
.board {
|
|
width: 100%;
|
|
}
|
|
|
|
.clues {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.keyboard {
|
|
grid-template-columns: repeat(9, minmax(0, 1fr));
|
|
}
|
|
|
|
.controls {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="shell">
|
|
<header>
|
|
<div class="brand">
|
|
<img src="assets/txproducts-logo-watermark.png" alt="">
|
|
<div>
|
|
<small>txproducts.de technical crossword</small>
|
|
<h1>Synchrotron Mini Crossword</h1>
|
|
</div>
|
|
</div>
|
|
<div class="stats">
|
|
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
|
|
<div class="stat"><span>Filled</span><strong id="filled">0%</strong></div>
|
|
<div class="stat"><span>Mode</span><strong id="mode">Across</strong></div>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
<section class="panel board-wrap">
|
|
<p class="message" id="message">Tap a square or clue, then fill the answer. Short, technical, and booth-friendly.</p>
|
|
<div class="board" id="board"></div>
|
|
</section>
|
|
|
|
<aside class="side">
|
|
<section class="panel clues">
|
|
<div>
|
|
<h2>Across</h2>
|
|
<ol id="acrossClues"></ol>
|
|
</div>
|
|
<div>
|
|
<h2>Down</h2>
|
|
<ol id="downClues"></ol>
|
|
</div>
|
|
</section>
|
|
<section class="difficulty" aria-label="Difficulty">
|
|
<button class="active-level" data-level="Easy" type="button">Easy</button>
|
|
<button data-level="Medium" type="button">Medium</button>
|
|
<button data-level="Hard" type="button">Hard</button>
|
|
</section>
|
|
<section class="keyboard" id="keyboard"></section>
|
|
<section class="controls">
|
|
<button class="primary" id="checkButton">Check</button>
|
|
<button id="revealButton">Reveal</button>
|
|
<button id="clearButton">Clear</button>
|
|
<button class="ghost" id="resetButton">Reset</button>
|
|
</section>
|
|
</aside>
|
|
</main>
|
|
|
|
<footer>
|
|
<button class="ghost" onclick="window.location.href='index.html'">Back to menu</button>
|
|
</footer>
|
|
</div>
|
|
|
|
<script>
|
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
|
const rows = 17;
|
|
const cols = 17;
|
|
const puzzleSets = {
|
|
Easy: [
|
|
{ num: 1, dir: "across", row: 2, col: 7, answer: "PICOSWITCH", clue: "Product that shortens x-ray pulses." },
|
|
{ num: 2, dir: "down", row: 2, col: 14, answer: "TXSERVICES", clue: "Support area for customers and experiments." },
|
|
{ num: 3, dir: "down", row: 4, col: 7, answer: "VALUE", clue: "What the right solution should create for a customer." },
|
|
{ num: 4, dir: "across", row: 5, col: 0, answer: "DATA", clue: "Measurement output before analysis." },
|
|
{ num: 5, dir: "across", row: 5, col: 10, answer: "PULSE", clue: "Short burst of x-rays or signal." },
|
|
{ num: 6, dir: "down", row: 5, col: 2, answer: "TXDATA", clue: "Turns measurements into insight." },
|
|
{ num: 7, dir: "down", row: 5, col: 10, answer: "PHOTON", clue: "Particle of light." },
|
|
{ num: 8, dir: "down", row: 6, col: 5, answer: "XRAY", clue: "Radiation used to probe structures." },
|
|
{ num: 9, dir: "across", row: 8, col: 4, answer: "WAVEGATE", clue: "Product for tailoring pulse timing." },
|
|
{ num: 10, dir: "across", row: 10, col: 0, answer: "BRAGG", clue: "Diffraction name linked to crystal planes." },
|
|
{ num: 11, dir: "across", row: 10, col: 7, answer: "SIGNAL", clue: "Useful information separated from noise." },
|
|
{ num: 12, dir: "down", row: 10, col: 0, answer: "BEAM", clue: "Focused stream used at a beamline." },
|
|
{ num: 13, dir: "down", row: 10, col: 4, answer: "GATE", clue: "Timing window that lets a pulse through." },
|
|
{ num: 14, dir: "down", row: 10, col: 8, answer: "ION", clue: "Charged particle." },
|
|
{ num: 15, dir: "across", row: 12, col: 4, answer: "TIMING", clue: "When pulses arrive." }
|
|
],
|
|
Medium: [
|
|
{ num: 1, dir: "across", row: 1, col: 1, answer: "WAVEGATE", clue: "TXProducts device for tailoring synchrotron pulse timing." },
|
|
{ num: 2, dir: "down", row: 3, col: 4, answer: "BEAMTIME", clue: "Scarce experiment time that teams want to use well." },
|
|
{ num: 3, dir: "across", row: 3, col: 6, answer: "TXSERVICES", clue: "Support area that helps teams get more from beamtime and experiments." },
|
|
{ num: 4, dir: "down", row: 3, col: 12, answer: "INSIGHT", clue: "What useful analysis gives a business after the measurement." },
|
|
{ num: 5, dir: "across", row: 5, col: 3, answer: "LATTICE", clue: "Ordered atomic structure studied in many materials experiments." },
|
|
{ num: 6, dir: "down", row: 6, col: 10, answer: "GATE", clue: "A timing window that lets the wanted pulse through." },
|
|
{ num: 7, dir: "across", row: 6, col: 12, answer: "ION", clue: "Charged particle found throughout accelerator physics vocabulary." },
|
|
{ num: 8, dir: "down", row: 8, col: 1, answer: "BRAGG", clue: "Diffraction condition central to selecting and switching x-rays." },
|
|
{ num: 9, dir: "across", row: 8, col: 3, answer: "PICOSWITCH", clue: "Photo-acoustic Bragg switch for shortening x-ray pulses." },
|
|
{ num: 10, dir: "down", row: 8, col: 7, answer: "SIGNAL", clue: "Useful measurement information that must be separated from noise." },
|
|
{ num: 11, dir: "across", row: 10, col: 0, answer: "VALUE", clue: "What the right product, data, and service should create for a customer." },
|
|
{ num: 12, dir: "down", row: 11, col: 11, answer: "DATA", clue: "Raw experiment output before analysis turns it into decisions." },
|
|
{ num: 13, dir: "across", row: 12, col: 6, answer: "WAVEGATE", clue: "Product that tailors synchrotron pulse structure from nanoseconds to seconds." },
|
|
{ num: 14, dir: "across", row: 14, col: 9, answer: "XRAY", clue: "Radiation used to probe structure and dynamics at beamlines." },
|
|
{ num: 15, dir: "down", row: 14, col: 10, answer: "RAY", clue: "A beam, line, or part of the x-ray term." }
|
|
],
|
|
Hard: [
|
|
{ num: 1, dir: "down", row: 0, col: 0, answer: "DETECTOR", clue: "Instrument that records the experimental signal." },
|
|
{ num: 2, dir: "down", row: 1, col: 6, answer: "SIGNAL", clue: "Measurement information separated from noise." },
|
|
{ num: 3, dir: "across", row: 2, col: 5, answer: "PICOSWITCH", clue: "Photo-acoustic Bragg switch for shortening x-ray pulses." },
|
|
{ num: 4, dir: "down", row: 2, col: 10, answer: "WAVEGATE", clue: "Product that tailors temporal pulse structure." },
|
|
{ num: 5, dir: "down", row: 3, col: 15, answer: "DYNAMICS", clue: "Time-dependent motion studied in materials." },
|
|
{ num: 6, dir: "across", row: 4, col: 0, answer: "COHERENCE", clue: "Wave property important in many advanced experiments." },
|
|
{ num: 7, dir: "across", row: 5, col: 9, answer: "BEAMLINE", clue: "Experimental path that delivers synchrotron radiation." },
|
|
{ num: 8, dir: "down", row: 7, col: 8, answer: "SAMPLE", clue: "Material placed in the beam for measurement." },
|
|
{ num: 9, dir: "down", row: 7, col: 13, answer: "ANALYSIS", clue: "Process that turns data into conclusions." },
|
|
{ num: 10, dir: "across", row: 8, col: 3, answer: "DIFFRACTION", clue: "Scattering pattern used to infer structure." },
|
|
{ num: 11, dir: "down", row: 10, col: 5, answer: "BRAGG", clue: "Diffraction condition named for a father-son team." },
|
|
{ num: 12, dir: "down", row: 10, col: 11, answer: "PHOTON", clue: "Quantum of light, including x-rays." },
|
|
{ num: 13, dir: "across", row: 12, col: 4, answer: "VALUE", clue: "Business result created by better data and support." },
|
|
{ num: 14, dir: "across", row: 13, col: 9, answer: "LATTICE", clue: "Ordered atomic arrangement in a crystal." },
|
|
{ num: 15, dir: "across", row: 15, col: 7, answer: "TIMING", clue: "Critical control variable for pulses." }
|
|
]
|
|
};
|
|
let entries = puzzleSets.Easy;
|
|
let grid = [];
|
|
let guesses = [];
|
|
let cellEntries = {};
|
|
let numbers = {};
|
|
const state = {
|
|
level: "Easy",
|
|
active: null,
|
|
row: 0,
|
|
col: 0,
|
|
score: 0
|
|
};
|
|
|
|
function buildGrid() {
|
|
entries = puzzleSets[state.level];
|
|
grid = Array.from({ length: rows }, () => Array.from({ length: cols }, () => null));
|
|
guesses = Array.from({ length: rows }, () => Array.from({ length: cols }, () => ""));
|
|
cellEntries = {};
|
|
numbers = {};
|
|
entries.forEach((entry) => {
|
|
numbers[`${entry.row},${entry.col}`] = entry.num;
|
|
entry.answer.split("").forEach((letter, index) => {
|
|
const row = entry.row + (entry.dir === "down" ? index : 0);
|
|
const col = entry.col + (entry.dir === "across" ? index : 0);
|
|
grid[row][col] = letter;
|
|
const entryKey = `${row},${col}`;
|
|
if (!cellEntries[entryKey]) cellEntries[entryKey] = [];
|
|
cellEntries[entryKey].push(entry);
|
|
});
|
|
});
|
|
state.active = entries[0];
|
|
state.row = entries[0].row;
|
|
state.col = entries[0].col;
|
|
}
|
|
|
|
const boardEl = document.getElementById("board");
|
|
const keyboardEl = document.getElementById("keyboard");
|
|
const acrossEl = document.getElementById("acrossClues");
|
|
const downEl = document.getElementById("downClues");
|
|
const messageEl = document.getElementById("message");
|
|
const scoreEl = document.getElementById("score");
|
|
const filledEl = document.getElementById("filled");
|
|
const modeEl = document.getElementById("mode");
|
|
const levelButtons = document.querySelectorAll("[data-level]");
|
|
|
|
function key(row, col) {
|
|
return `${row},${col}`;
|
|
}
|
|
|
|
function activeCells() {
|
|
const cells = [];
|
|
state.active.answer.split("").forEach((_, index) => {
|
|
cells.push({
|
|
row: state.active.row + (state.active.dir === "down" ? index : 0),
|
|
col: state.active.col + (state.active.dir === "across" ? index : 0)
|
|
});
|
|
});
|
|
return cells;
|
|
}
|
|
|
|
function chooseCell(row, col) {
|
|
if (!grid[row][col]) return;
|
|
const options = cellEntries[key(row, col)];
|
|
if (options.includes(state.active) && options.length > 1) {
|
|
state.active = options.find((entry) => entry !== state.active);
|
|
} else {
|
|
state.active = options[0];
|
|
}
|
|
state.row = row;
|
|
state.col = col;
|
|
render();
|
|
}
|
|
|
|
function chooseEntry(entry) {
|
|
state.active = entry;
|
|
state.row = entry.row;
|
|
state.col = entry.col;
|
|
render();
|
|
}
|
|
|
|
function renderBoard() {
|
|
const related = new Set(activeCells().map((cell) => key(cell.row, cell.col)));
|
|
boardEl.innerHTML = "";
|
|
for (let row = 0; row < rows; row += 1) {
|
|
for (let col = 0; col < cols; col += 1) {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "cell";
|
|
if (!grid[row][col]) {
|
|
button.classList.add("block");
|
|
button.disabled = true;
|
|
} else {
|
|
if (related.has(key(row, col))) button.classList.add("related");
|
|
if (row === state.row && col === state.col) button.classList.add("active");
|
|
if (guesses[row][col] === grid[row][col]) button.classList.add("correct");
|
|
button.textContent = guesses[row][col];
|
|
if (numbers[key(row, col)]) {
|
|
const num = document.createElement("span");
|
|
num.className = "num";
|
|
num.textContent = numbers[key(row, col)];
|
|
button.appendChild(num);
|
|
}
|
|
button.addEventListener("click", () => chooseCell(row, col));
|
|
}
|
|
boardEl.appendChild(button);
|
|
}
|
|
}
|
|
}
|
|
|
|
function renderClues() {
|
|
acrossEl.innerHTML = "";
|
|
downEl.innerHTML = "";
|
|
entries.forEach((entry) => {
|
|
const li = document.createElement("li");
|
|
li.value = entry.num;
|
|
li.textContent = entry.clue;
|
|
if (entry === state.active) li.className = "active";
|
|
li.addEventListener("click", () => chooseEntry(entry));
|
|
(entry.dir === "across" ? acrossEl : downEl).appendChild(li);
|
|
});
|
|
}
|
|
|
|
function renderKeyboard() {
|
|
keyboardEl.innerHTML = "";
|
|
alphabet.forEach((letter) => {
|
|
const button = document.createElement("button");
|
|
button.className = "letter";
|
|
button.textContent = letter;
|
|
button.addEventListener("click", () => typeLetter(letter));
|
|
keyboardEl.appendChild(button);
|
|
});
|
|
}
|
|
|
|
function renderStats() {
|
|
let total = 0;
|
|
let filled = 0;
|
|
for (let row = 0; row < rows; row += 1) {
|
|
for (let col = 0; col < cols; col += 1) {
|
|
if (grid[row][col]) {
|
|
total += 1;
|
|
if (guesses[row][col]) filled += 1;
|
|
}
|
|
}
|
|
}
|
|
scoreEl.textContent = state.score;
|
|
filledEl.textContent = `${Math.round((filled / total) * 100)}%`;
|
|
modeEl.textContent = `${state.level} ${state.active.dir === "across" ? "Across" : "Down"}`;
|
|
}
|
|
|
|
function render() {
|
|
renderBoard();
|
|
renderClues();
|
|
renderStats();
|
|
}
|
|
|
|
function nextCell() {
|
|
const cells = activeCells();
|
|
const current = cells.findIndex((cell) => cell.row === state.row && cell.col === state.col);
|
|
const next = cells[Math.min(current + 1, cells.length - 1)] || cells[0];
|
|
state.row = next.row;
|
|
state.col = next.col;
|
|
}
|
|
|
|
function previousCell() {
|
|
const cells = activeCells();
|
|
const current = cells.findIndex((cell) => cell.row === state.row && cell.col === state.col);
|
|
const previous = cells[Math.max(current - 1, 0)] || cells[0];
|
|
state.row = previous.row;
|
|
state.col = previous.col;
|
|
}
|
|
|
|
function typeLetter(letter) {
|
|
guesses[state.row][state.col] = letter;
|
|
nextCell();
|
|
render();
|
|
}
|
|
|
|
function backspace() {
|
|
if (guesses[state.row][state.col]) {
|
|
guesses[state.row][state.col] = "";
|
|
} else {
|
|
previousCell();
|
|
guesses[state.row][state.col] = "";
|
|
}
|
|
render();
|
|
}
|
|
|
|
function checkPuzzle() {
|
|
let wrong = 0;
|
|
let correct = 0;
|
|
for (let row = 0; row < rows; row += 1) {
|
|
for (let col = 0; col < cols; col += 1) {
|
|
if (grid[row][col] && guesses[row][col]) {
|
|
if (guesses[row][col] === grid[row][col]) correct += 1;
|
|
else wrong += 1;
|
|
}
|
|
}
|
|
}
|
|
state.score = correct * 20;
|
|
messageEl.textContent = wrong ? `${wrong} squares need another look.` : "Everything filled so far is correct.";
|
|
if (isComplete() && !wrong) {
|
|
state.score += 400;
|
|
messageEl.textContent = "Crossword complete: product, physics, data, and service all connected.";
|
|
}
|
|
render();
|
|
}
|
|
|
|
function isComplete() {
|
|
for (let row = 0; row < rows; row += 1) {
|
|
for (let col = 0; col < cols; col += 1) {
|
|
if (grid[row][col] && guesses[row][col] !== grid[row][col]) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function revealActive() {
|
|
activeCells().forEach((cell, index) => {
|
|
guesses[cell.row][cell.col] = state.active.answer[index];
|
|
});
|
|
messageEl.textContent = `${state.active.num} ${state.active.dir} revealed.`;
|
|
render();
|
|
}
|
|
|
|
function clearActive() {
|
|
activeCells().forEach((cell) => {
|
|
guesses[cell.row][cell.col] = "";
|
|
});
|
|
messageEl.textContent = "Current answer cleared.";
|
|
render();
|
|
}
|
|
|
|
function resetPuzzle() {
|
|
buildGrid();
|
|
state.score = 0;
|
|
messageEl.textContent = `${state.level} crossword ready. Tap a square or clue, then fill the answer.`;
|
|
render();
|
|
}
|
|
|
|
function setLevel(level) {
|
|
state.level = level;
|
|
levelButtons.forEach((button) => button.classList.toggle("active-level", button.dataset.level === level));
|
|
resetPuzzle();
|
|
}
|
|
|
|
levelButtons.forEach((button) => button.addEventListener("click", () => setLevel(button.dataset.level)));
|
|
document.getElementById("checkButton").addEventListener("click", checkPuzzle);
|
|
document.getElementById("revealButton").addEventListener("click", revealActive);
|
|
document.getElementById("clearButton").addEventListener("click", clearActive);
|
|
document.getElementById("resetButton").addEventListener("click", resetPuzzle);
|
|
window.addEventListener("keydown", (event) => {
|
|
const letter = event.key.toUpperCase();
|
|
if (alphabet.includes(letter)) typeLetter(letter);
|
|
if (event.key === "Backspace") backspace();
|
|
if (event.key === "Enter") checkPuzzle();
|
|
if (event.key === "Tab") {
|
|
event.preventDefault();
|
|
const index = entries.indexOf(state.active);
|
|
chooseEntry(entries[(index + 1) % entries.length]);
|
|
}
|
|
if (event.key.toLowerCase() === "r") resetPuzzle();
|
|
});
|
|
|
|
buildGrid();
|
|
renderKeyboard();
|
|
render();
|
|
</script>
|
|
</body>
|
|
</html>
|