Files
games/all-games/wheel-of-photons.html
T
2026-06-17 09:13:06 +02:00

658 lines
20 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>Wheel of Photons</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;
}
* { 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 16% 12%, rgba(101, 216, 232, 0.22), transparent 28%),
radial-gradient(circle at 86% 80%, rgba(242, 201, 107, 0.12), 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(48px, 8vw, 72px);
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.5rem);
line-height: 1;
}
.stats {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
.stat,
.tip,
button,
.panel {
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: 108px;
padding: 8px 10px;
}
.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: 1.1rem;
font-variant-numeric: tabular-nums;
}
main {
min-height: 0;
display: grid;
grid-template-columns: minmax(260px, 0.82fr) minmax(340px, 1.18fr);
gap: 14px;
}
.wheel-panel {
position: relative;
display: grid;
place-items: center;
min-height: 420px;
overflow: hidden;
}
.wheel-panel::before {
content: "";
position: absolute;
inset: 12%;
background: url("assets/txproducts-logo-watermark.png") center / contain no-repeat;
opacity: 0.1;
transform: rotate(-18deg);
pointer-events: none;
}
.wheel {
position: relative;
width: min(72vmin, 420px);
aspect-ratio: 1;
border: 10px solid rgba(255, 255, 255, 0.12);
border-radius: 50%;
background:
conic-gradient(
from 0deg,
#f2c96b 0 30deg,
#65d8e8 30deg 60deg,
#88d76f 60deg 90deg,
#293544 90deg 120deg,
#f2c96b 120deg 150deg,
#ef735f 150deg 180deg,
#65d8e8 180deg 210deg,
#88d76f 210deg 240deg,
#293544 240deg 270deg,
#f2c96b 270deg 300deg,
#65d8e8 300deg 330deg,
#ef735f 330deg 360deg
);
box-shadow: 0 26px 80px rgba(0, 0, 0, 0.45), inset 0 0 60px rgba(0, 0, 0, 0.42);
transition: transform 1100ms cubic-bezier(0.16, 0.82, 0.28, 1);
}
.wheel::after {
content: "";
position: absolute;
inset: 28%;
border: 1px solid rgba(255, 255, 255, 0.24);
border-radius: 50%;
background: rgba(6, 10, 15, 0.78);
}
.pointer {
position: absolute;
top: calc(50% - min(36vmin, 210px) - 16px);
left: 50%;
width: 0;
height: 0;
z-index: 3;
transform: translateX(-50%);
border-left: 16px solid transparent;
border-right: 16px solid transparent;
border-top: 26px solid var(--text);
filter: drop-shadow(0 5px 8px rgba(0, 0, 0, 0.35));
}
.spin-readout {
position: absolute;
z-index: 4;
display: grid;
place-items: center;
width: 45%;
aspect-ratio: 1;
border: 1px solid var(--line);
border-radius: 50%;
background: rgba(8, 13, 20, 0.86);
text-align: center;
}
.spin-readout span {
color: var(--muted);
font-size: 0.75rem;
font-weight: 900;
text-transform: uppercase;
}
.spin-readout strong {
display: block;
margin-top: 4px;
color: var(--gold);
font-size: clamp(1.7rem, 5vw, 3rem);
line-height: 1;
}
.game-panel {
display: grid;
gap: 14px;
padding: 16px;
}
.category {
display: inline-flex;
width: fit-content;
border: 1px solid rgba(101, 216, 232, 0.34);
border-radius: 999px;
padding: 6px 10px;
color: var(--cyan);
font-size: 0.78rem;
font-weight: 900;
text-transform: uppercase;
}
.phrase {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-content: center;
min-height: 180px;
}
.word {
display: inline-flex;
gap: 5px;
margin-right: 8px;
}
.tile {
display: grid;
place-items: center;
width: clamp(30px, 5vw, 52px);
height: clamp(38px, 6vw, 64px);
border: 1px solid rgba(255, 255, 255, 0.24);
border-radius: 8px;
background: rgba(255, 255, 255, 0.08);
color: var(--text);
font-size: clamp(1.15rem, 3vw, 2rem);
font-weight: 1000;
text-transform: uppercase;
}
.tile.space {
width: 18px;
border: 0;
background: transparent;
}
.keyboard {
display: grid;
grid-template-columns: repeat(13, 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;
color: var(--text);
font: inherit;
font-weight: 900;
cursor: pointer;
touch-action: manipulation;
}
button.primary {
border-color: rgba(242, 201, 107, 0.62);
background: linear-gradient(180deg, #f3d16e, #d89a37);
color: #211609;
}
button.active-level {
color: #071014;
background: var(--green);
border-color: rgba(136, 215, 111, 0.85);
}
.letter.used {
opacity: 0.32;
}
.letter.good {
border-color: rgba(136, 215, 111, 0.62);
color: var(--green);
}
.letter.bad {
border-color: rgba(239, 115, 95, 0.62);
color: var(--red);
}
.actions,
.footer-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.difficulty button {
min-height: 28px;
padding: 0 6px;
font-size: 0.76rem;
}
.actions button {
min-height: 38px;
font-size: 0.88rem;
}
.keyboard .letter {
min-height: 48px;
font-size: clamp(1rem, 1.8vw, 1.24rem);
}
.explain {
min-height: 54px;
color: var(--muted);
font-size: clamp(1rem, 1.7vw, 1.15rem);
line-height: 1.45;
}
.tip {
padding: 8px 10px;
color: var(--muted);
font-size: 0.9rem;
}
@media (max-width: 920px) {
body {
overflow: auto;
}
.shell {
min-height: 100vh;
height: auto;
padding: 10px;
}
header,
footer {
align-items: stretch;
flex-direction: column;
}
main {
grid-template-columns: 1fr;
}
.wheel-panel {
min-height: 330px;
}
.stats,
.footer-actions {
justify-content: flex-start;
}
.keyboard {
grid-template-columns: repeat(7, 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 photon puzzle</small>
<h1>Wheel of Photons</h1>
</div>
</div>
<div class="stats">
<div class="stat"><span>Score</span><strong id="score">0</strong></div>
<div class="stat"><span>Beamtime</span><strong id="beamtime">6</strong></div>
<div class="stat"><span>Puzzle</span><strong id="round">1 / 6</strong></div>
</div>
</header>
<main>
<section class="panel wheel-panel">
<div class="pointer"></div>
<div class="wheel" id="wheel"></div>
<div class="spin-readout">
<div><span>Spin value</span><strong id="spinValue">Spin</strong></div>
</div>
</section>
<section class="panel game-panel">
<div class="category" id="category">Category</div>
<div 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>
</div>
<div class="phrase" id="phrase" aria-label="Hidden phrase"></div>
<div class="keyboard" id="keyboard" aria-label="Letter keyboard"></div>
<div class="actions">
<button class="primary" id="spinButton" type="button">Spin</button>
<button id="solveButton" type="button">Solve / Reveal</button>
<button id="nextButton" type="button">Next phrase</button>
</div>
<div class="explain" id="explain">Spin, pick letters, then use Next phrase for another puzzle.</div>
</section>
</main>
<footer>
<div class="tip">Passerby mode: one spin, one letter, quick reveal. Good for drawing people into the booth.</div>
<div class="footer-actions">
<button id="resetButton" type="button">Reset</button>
<button type="button" onclick="window.location.href='index.html'">Back to menu</button>
</div>
</footer>
</div>
<script>
const puzzleSets = {
Easy: [
{ category: "Easy Product", phrase: "WAVEGATE", explain: "WaveGate tailors synchrotron pulse timing." },
{ category: "Easy Product", phrase: "PICOSWITCH", explain: "PicoSwitch shortens x-ray pulses." },
{ category: "Easy Data", phrase: "TXDATA", explain: "TXData turns measurements into insight." },
{ category: "Easy Service", phrase: "TXSERVICES", explain: "TXServices supports better experiments." },
{ category: "Easy Physics", phrase: "XRAY PULSE", explain: "X-ray pulses are the timing target." },
{ category: "Easy Business", phrase: "BETTER BEAMTIME", explain: "Good support helps teams get more from beamtime." }
],
Medium: [
{ category: "Product", phrase: "WAVEGATE", explain: "WaveGate tailors synchrotron pulse structure from nanoseconds to seconds." },
{ category: "Product", phrase: "PICOSWITCH", explain: "PicoSwitch shortens x-ray pulses into the 5-10 ps range." },
{ category: "Physics", phrase: "BRAGG DIFFRACTION", explain: "Bragg diffraction links x-ray scattering to ordered crystal structure." },
{ category: "Timing", phrase: "TEMPORAL PULSE STRUCTURE", explain: "Timing structure describes when pulses arrive and which pulses are transmitted." },
{ category: "Beamline", phrase: "SYNCHROTRON RADIATION", explain: "Synchrotrons produce bright, tunable radiation for advanced experiments." },
{ category: "Experiment", phrase: "COHERENT LATTICE DYNAMICS", explain: "Short x-ray pulses help observe fast collective motion in crystals." },
{ category: "Business", phrase: "ACTIONABLE INSIGHT", explain: "TXData and TXServices can help convert complex measurements into useful decisions." },
{ category: "Service", phrase: "BETTER BEAMTIME", explain: "Good products, data, and support help teams get more value from scarce beamtime." }
],
Hard: [
{ category: "Hard Product", phrase: "PHOTO ACOUSTIC BRAGG SWITCH", explain: "PicoSwitch uses photo-acoustic Bragg switching to shorten x-ray pulses." },
{ category: "Hard Timing", phrase: "NANOSECONDS TO SECONDS", explain: "WaveGate covers broad temporal pulse tailoring." },
{ category: "Hard Experiment", phrase: "COHERENT LATTICE DYNAMICS", explain: "Short pulses can reveal ultrafast motion inside materials." },
{ category: "Hard Business", phrase: "MEASUREMENTS INTO INSIGHT", explain: "TXData and TXServices help convert complex measurements into decisions." },
{ category: "Hard Physics", phrase: "DIFFRACTION EFFICIENCY", explain: "Diffraction efficiency matters when choosing repetition rates and switching conditions." },
{ category: "Hard Beamline", phrase: "TEMPORAL SYNCHROTRON PULSE STRUCTURE", explain: "Temporal structure controls which pulses reach the experiment." }
]
};
let puzzles = puzzleSets.Easy;
const wheelValues = [100, 150, 200, 250, 300, 350, 400, 500, 600, 750, "BANKRUPT", "PASS"];
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
const state = {
index: 0,
level: "Easy",
score: 0,
beamtime: 6,
currentValue: 0,
rotation: 0,
guessed: new Map(),
solved: false
};
const scoreEl = document.querySelector("#score");
const beamtimeEl = document.querySelector("#beamtime");
const roundEl = document.querySelector("#round");
const spinValueEl = document.querySelector("#spinValue");
const wheel = document.querySelector("#wheel");
const categoryEl = document.querySelector("#category");
const phraseEl = document.querySelector("#phrase");
const keyboardEl = document.querySelector("#keyboard");
const explainEl = document.querySelector("#explain");
const spinButton = document.querySelector("#spinButton");
const solveButton = document.querySelector("#solveButton");
const nextButton = document.querySelector("#nextButton");
const resetButton = document.querySelector("#resetButton");
const levelButtons = document.querySelectorAll("[data-level]");
function render() {
const puzzle = puzzles[state.index];
categoryEl.textContent = `${state.level} / ${puzzle.category}`;
scoreEl.textContent = state.score;
beamtimeEl.textContent = state.beamtime;
roundEl.textContent = `${state.index + 1} / ${puzzles.length}`;
spinValueEl.textContent = state.currentValue || "Spin";
renderPhrase(puzzle.phrase);
renderKeyboard(puzzle.phrase);
}
function renderPhrase(phrase) {
phraseEl.innerHTML = "";
for (const word of phrase.split(" ")) {
const wordEl = document.createElement("span");
wordEl.className = "word";
for (const char of word) {
const tile = document.createElement("span");
tile.className = "tile";
tile.textContent = state.solved || state.guessed.get(char) ? char : "";
wordEl.appendChild(tile);
}
phraseEl.appendChild(wordEl);
}
}
function renderKeyboard(phrase) {
keyboardEl.innerHTML = "";
for (const letter of letters) {
const button = document.createElement("button");
const used = state.guessed.has(letter);
const good = used && phrase.includes(letter);
const bad = used && !phrase.includes(letter);
button.type = "button";
button.className = `letter${used ? " used" : ""}${good ? " good" : ""}${bad ? " bad" : ""}`;
button.textContent = letter;
button.disabled = used || state.solved || !state.currentValue || state.currentValue === "BANKRUPT" || state.currentValue === "PASS";
button.addEventListener("click", () => guessLetter(letter));
button.addEventListener("touchend", (event) => {
event.preventDefault();
guessLetter(letter);
}, { passive: false });
keyboardEl.appendChild(button);
}
}
function spin() {
if (state.solved) return;
const value = wheelValues[Math.floor(Math.random() * wheelValues.length)];
state.currentValue = value;
state.rotation += 720 + Math.floor(Math.random() * 720);
wheel.style.transform = `rotate(${state.rotation}deg)`;
if (value === "BANKRUPT") {
state.score = Math.max(0, Math.floor(state.score * 0.5));
state.beamtime = Math.max(0, state.beamtime - 1);
explainEl.textContent = "Beamtime penalty: half the score is lost. Spin again.";
} else if (value === "PASS") {
state.beamtime = Math.max(0, state.beamtime - 1);
explainEl.textContent = "No useful pulse this turn. Spin again.";
} else {
explainEl.textContent = `Spin value ${value}. Pick a consonant or vowel from the board.`;
}
render();
checkBeamtime();
}
function guessLetter(letter) {
const puzzle = puzzles[state.index];
if (state.guessed.has(letter) || state.solved || typeof state.currentValue !== "number") return;
const count = puzzle.phrase.split("").filter((char) => char === letter).length;
state.guessed.set(letter, true);
if (count > 0) {
state.score += count * state.currentValue;
explainEl.textContent = `${letter} appears ${count} time${count === 1 ? "" : "s"}.`;
} else {
state.beamtime = Math.max(0, state.beamtime - 1);
explainEl.textContent = `${letter} is not in the phrase. Beamtime drops by one.`;
}
state.currentValue = 0;
if (isSolved(puzzle.phrase)) solve(true);
render();
checkBeamtime();
}
function isSolved(phrase) {
return phrase.split("").every((char) => char === " " || state.guessed.has(char));
}
function solve(auto = false) {
const puzzle = puzzles[state.index];
state.solved = true;
explainEl.textContent = auto ? `${puzzle.phrase}. ${puzzle.explain}` : `${puzzle.phrase}: ${puzzle.explain}`;
render();
}
function nextPuzzle() {
state.index = (state.index + 1) % puzzles.length;
state.beamtime = 6;
state.currentValue = 0;
state.guessed = new Map();
state.solved = false;
explainEl.textContent = "Spin, pick letters, then use Next phrase for another puzzle.";
render();
}
function resetGame() {
state.index = 0;
state.score = 0;
state.beamtime = 6;
state.currentValue = 0;
state.rotation = 0;
state.guessed = new Map();
state.solved = false;
explainEl.textContent = "Spin, pick letters, then use Next phrase for another puzzle.";
wheel.style.transform = "rotate(0deg)";
render();
}
function setLevel(level) {
state.level = level;
puzzles = puzzleSets[level];
levelButtons.forEach((button) => button.classList.toggle("active-level", button.dataset.level === level));
resetGame();
}
function checkBeamtime() {
if (state.beamtime <= 0 && !state.solved) {
solve(false);
explainEl.textContent = `Beamtime expired. ${puzzles[state.index].phrase}: ${puzzles[state.index].explain}`;
}
}
spinButton.addEventListener("click", spin);
solveButton.addEventListener("click", () => solve(false));
nextButton.addEventListener("click", nextPuzzle);
resetButton.addEventListener("click", resetGame);
levelButtons.forEach((button) => button.addEventListener("click", () => setLevel(button.dataset.level)));
window.addEventListener("keydown", (event) => {
const letter = event.key.toUpperCase();
if (letters.includes(letter)) guessLetter(letter);
if (event.key === " ") {
event.preventDefault();
spin();
}
if (event.key.toLowerCase() === "r") resetGame();
});
render();
</script>
</body>
</html>