diff --git a/README.md b/README.md index 2dd86b3..d0701ae 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,20 @@ small apps for various use cases ## Apps Available +### π Jass Scoreboard (Schiefertafel Z/Z) +A browser-based digital Jass scoreboard that replicates a traditional Swiss chalk slate board with the classic Z/Z layout. + +**Features:** +- Classic Z/Z slate board look with chalk tally marks (e.g. `||||\ ||||\ |`) +- Two teams, editable names and target score (default 2500) +- Win detection with animated overlay (gold glow, chalk particles) +- Undo last entry, reset game, flip board orientation +- Game state persisted to localStorage β resume on page reload +- Responsive: works on mobile (320px+), tablet, and desktop +- No external frameworks β pure HTML, CSS, Vanilla JS + +**Play:** [jass-scoreboard/index.html](jass-scoreboard/index.html) + ### π GeoTriad - Geography Quiz Game A fun, educational geography quiz game for kids around 10 years old! diff --git a/jass-scoreboard/README.md b/jass-scoreboard/README.md new file mode 100644 index 0000000..87e9b6b --- /dev/null +++ b/jass-scoreboard/README.md @@ -0,0 +1,74 @@ +# Jass Scoreboard β Schiefertafel Z/Z + +A browser-based digital Jass scoreboard that replicates the look and feel of a traditional Swiss chalk slate board (Jasstafeln) with the classic Z/Z layout. + +## Features + +- π― **Z/Z Board Layout** β Visual division of two mirrored team areas with red chalk guide lines +- π **Tally Mark Display** β Points shown as chalk tally marks (`||||\ ||||\ |`) instead of numbers +- π’ **Numeric Totals** β Running totals displayed at the bottom of each panel +- π **Win Detection** β Animated win overlay when a team exceeds the target score +- β© **Undo** β Remove the last recorded entry +- π **Reset** β Clear all scores while keeping team names and target score +- β **Flip Board** β Swap panel positions for players on opposite sides of the table +- πΎ **Persistence** β Game state saved to `localStorage` and restored on page reload +- π± **Responsive** β Works on mobile (320px+), tablet, and desktop + +## Usage + +1. Open `index.html` in any modern browser (requires ES module support) +2. Edit team names by clicking on the name fields at the top +3. Set your target score (default: 2500) +4. Select a team, enter points (1β500), and click **Eintragen** +5. Use **β© RΓΌckgΓ€ngig** to undo the last entry +6. Use **π Neu** to reset the game +7. Use **β** to flip the board orientation + +## Project Structure + +``` +jass-scoreboard/ +βββ index.html # Root HTML structure +βββ css/ +β βββ styles.css # Chalk board styling, Z/Z grid, animations +βββ js/ +β βββ app.js # Application bootstrap +β βββ state.js # Global state definition and mutations +β βββ storage.js # localStorage persistence +β βββ scoring.js # Score entry, totals, win check +β βββ renderer.js # Board, tallies, totals, win state rendering +β βββ ui.js # Event binding and input validation +βββ assets/ +β βββ textures/ # (placeholder for optional slate texture) +β βββ fonts/ # (placeholder for optional chalk fonts) +βββ README.md +``` + +## Technologies + +- HTML5 +- CSS3 +- Vanilla JavaScript (ES6 modules) + +No external frameworks or libraries are used. + +## Game Rules + +- Two teams record scores round by round +- A team wins when its total **exceeds** the target score +- Default target: **2500 points** +- Valid point entries: integers from 1 to 500 +- After a win, score entry is disabled but undo remains available + +## Tally Mark Rendering + +Points are displayed as tally marks: + +| Points | Display | +|--------|-----------------------| +| 3 | `\| \| \|` | +| 5 | `\|\|\|\|\ ` | +| 7 | `\|\|\|\|\ \| \|` | +| 10 | `\|\|\|\|\ \|\|\|\|\ ` | + +Groups of 5 are rendered as `||||\ ` and remainders as `| `. diff --git a/jass-scoreboard/css/styles.css b/jass-scoreboard/css/styles.css new file mode 100644 index 0000000..a57d9b5 --- /dev/null +++ b/jass-scoreboard/css/styles.css @@ -0,0 +1,567 @@ +/* styles.css β Jass Scoreboard chalk slate styling */ + +/* βββ Reset & Base ββββββββββββββββββββββββββββββββββββββββββββββ */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --slate-bg: #1a2a1a; + --slate-surface: #1e321e; + --chalk-white: #f0ede0; + --chalk-dim: #c8c4b0; + --chalk-red: #e05050; + --panel-border: rgba(255, 255, 255, 0.12); + --input-bg: #243224; + --input-border: rgba(240, 237, 224, 0.3); + --btn-primary: #4a7c59; + --btn-primary-h: #5a9c6e; + --btn-secondary: #3a5a4a; + --btn-danger: #7a3535; + --btn-danger-h: #9a4545; + --winner-glow: #ffd700; + --font-main: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + --radius: 6px; +} + +html, body { + height: 100%; +} + +body { + background-color: var(--slate-bg); + background-image: + radial-gradient(ellipse at 20% 30%, rgba(255,255,255,0.02) 0%, transparent 60%), + radial-gradient(ellipse at 80% 70%, rgba(255,255,255,0.015) 0%, transparent 60%); + color: var(--chalk-white); + font-family: var(--font-main); + min-height: 100vh; + min-width: 320px; + display: flex; + flex-direction: column; + align-items: center; +} + +/* βββ Screen-reader only utility βββββββββββββββββββββββββββββββ */ +.sr-only { + position: absolute; + width: 1px; height: 1px; + padding: 0; margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + white-space: nowrap; + border: 0; +} + +/* βββ App wrapper βββββββββββββββββββββββββββββββββββββββββββββββ */ +.app-wrapper { + width: 100%; + max-width: 760px; + padding: 12px 12px 24px; + display: flex; + flex-direction: column; + gap: 12px; +} + +/* βββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββ */ +.app-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 8px 4px; + border-bottom: 2px solid var(--chalk-red); +} + +.header-title { + font-size: 1.6rem; + font-weight: 700; + letter-spacing: 0.08em; + color: var(--chalk-white); + text-shadow: 0 0 12px rgba(240,237,224,0.35); +} + +.header-settings { + display: flex; + align-items: center; + gap: 8px; +} + +.setting-label { + font-size: 0.85rem; + color: var(--chalk-dim); +} + +.setting-input { + width: 80px; + padding: 4px 8px; + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: var(--radius); + color: var(--chalk-white); + font-size: 0.9rem; + text-align: center; +} + +.setting-input:focus { + outline: 2px solid var(--chalk-dim); + outline-offset: 1px; +} + +/* βββ Name editors ββββββββββββββββββββββββββββββββββββββββββββββ */ +.name-editors { + display: flex; + align-items: center; + gap: 8px; +} + +.name-editor-group { + flex: 1; +} + +.team-name-input { + width: 100%; + padding: 6px 10px; + background: transparent; + border: none; + border-bottom: 1px dashed rgba(240,237,224,0.3); + color: var(--chalk-white); + font-size: 1rem; + font-weight: 600; + text-align: center; + letter-spacing: 0.04em; + transition: border-color 0.2s; +} + +.team-name-input:focus { + outline: none; + border-bottom-color: var(--chalk-white); +} + +.name-editor-divider { + color: var(--chalk-dim); + font-size: 0.8rem; + flex-shrink: 0; +} + +/* βββ Board (Z/Z grid β two stacked panels) ββββββββββββββββββββ */ +.board { + display: grid; + grid-template-rows: 1fr auto 1fr; + min-height: 420px; + background: var(--slate-surface); + border: 1px solid var(--panel-border); + border-radius: var(--radius); + overflow: hidden; + box-shadow: + inset 0 0 60px rgba(0,0,0,0.35), + 0 4px 20px rgba(0,0,0,0.5); +} + +/* βββ Board panels ββββββββββββββββββββββββββββββββββββββββββββββ */ +.board-panel { + padding: 16px 14px 14px; + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; + transition: box-shadow 0.4s ease; +} + +/* Bottom panel rotated 180Β° so the opposing player can read it */ +.panel-bottom { + transform: rotate(180deg); +} + +/* Horizontal red chalk line between the two Z panels */ +.board-h-divider { + height: 3px; + background: linear-gradient( + to right, + transparent 0%, + rgba(224, 80, 80, 0.7) 15%, + rgba(224, 80, 80, 0.7) 85%, + transparent 100% + ); +} + +/* Winner highlight */ +.winner-panel { + box-shadow: + inset 0 0 40px rgba(255, 215, 0, 0.18), + inset 0 0 10px rgba(255, 215, 0, 0.08); + animation: panelPulse 1.6s ease-in-out infinite alternate; +} + +@keyframes panelPulse { + from { box-shadow: inset 0 0 40px rgba(255,215,0,0.12), inset 0 0 10px rgba(255,215,0,0.06); } + to { box-shadow: inset 0 0 60px rgba(255,215,0,0.28), inset 0 0 20px rgba(255,215,0,0.14); } +} + +/* βββ Chalk text ββββββββββββββββββββββββββββββββββββββββββββββββ */ +.chalk-text { + color: var(--chalk-white); + text-rendering: geometricPrecision; +} + +/* βββ Team name inside panel βββββββββββββββββββββββββββββββββββ */ +.panel-team-name { + font-size: 1.1rem; + font-weight: 700; + letter-spacing: 0.06em; + text-align: center; + padding-bottom: 6px; + border-bottom: 1px solid rgba(224, 80, 80, 0.45); + text-shadow: 0 0 8px rgba(240,237,224,0.3); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* βββ Z SVG area (filled by renderer.js) βββββββββββββββββββββββ */ +.z-svg-area { + flex: 1; + min-height: 110px; + overflow: hidden; +} + +.z-svg-area svg { + display: block; + width: 100%; + height: 100%; +} + +/* βββ Total row βββββββββββββββββββββββββββββββββββββββββββββββββ */ +.panel-total-row { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 8px; + border-top: 1px solid rgba(224, 80, 80, 0.45); +} + +.total-label { + font-size: 0.8rem; + color: var(--chalk-dim); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.total-value { + font-size: 1.4rem; + font-weight: 700; + letter-spacing: 0.04em; + text-shadow: 0 0 10px rgba(240,237,224,0.4); +} + +/* βββ Z wrapper β contains SVG guide lines + tally entries βββββ */ +.z-wrapper { + position: relative; + flex: 1; + min-height: 110px; + overflow-y: auto; +} + +.z-lines { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; +} +/* βββ Score input area ββββββββββββββββββββββββββββββββββββββββββ */ +.score-input-area { + background: var(--input-bg); + border: 1px solid var(--panel-border); + border-radius: var(--radius); + padding: 14px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.score-input-area.disabled { + opacity: 0.55; + pointer-events: none; +} + +/* βββ Team toggle row βββββββββββββββββββββββββββββββββββββββββββ */ +.team-toggle-row { + display: flex; + gap: 8px; +} + +.btn-team { + flex: 1; + padding: 10px 8px; + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: var(--radius); + color: var(--chalk-dim); + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: background 0.18s, color 0.18s, border-color 0.18s; + letter-spacing: 0.04em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.btn-team.is-active { + background: var(--btn-primary); + color: #fff; + border-color: var(--btn-primary-h); +} + +.btn-team:hover:not(.is-active) { + background: rgba(255,255,255,0.06); + color: var(--chalk-white); +} + +/* βββ Mark buttons (+100 / +50 / +20) ββββββββββββββββββββββββββ */ +.mark-row { + display: flex; + gap: 8px; +} + +.btn-mark { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + padding: 12px 6px; + background: var(--slate-surface); + border: 1px solid var(--panel-border); + border-radius: var(--radius); + cursor: pointer; + transition: background 0.18s, transform 0.1s, border-color 0.18s; +} + +.btn-mark:hover { + background: rgba(255,255,255,0.07); + border-color: rgba(240,237,224,0.3); +} + +.btn-mark:active { + transform: scale(0.95); + background: rgba(255,255,255,0.12); +} + +.btn-mark:focus-visible { + outline: 2px solid var(--chalk-white); + outline-offset: 2px; +} + +.mark-pts { + font-size: 1.3rem; + font-weight: 700; + color: var(--chalk-white); + text-shadow: 0 0 8px rgba(240,237,224,0.3); + letter-spacing: 0.04em; +} + +.mark-lbl { + font-size: 0.7rem; + color: var(--chalk-dim); + text-transform: uppercase; + letter-spacing: 0.07em; +} + +/* βββ Buttons βββββββββββββββββββββββββββββββββββββββββββββββββββ */ +.btn { + padding: 10px 18px; + border: none; + border-radius: var(--radius); + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: background 0.18s, transform 0.1s; + white-space: nowrap; +} + +.btn:active { + transform: scale(0.97); +} + +.btn:focus-visible { + outline: 2px solid var(--chalk-white); + outline-offset: 2px; +} + +.btn-primary { + background: var(--btn-primary); + color: #fff; + flex-shrink: 0; +} + +.btn-primary:hover { + background: var(--btn-primary-h); +} + +.btn-secondary { + background: var(--btn-secondary); + color: var(--chalk-white); +} + +.btn-secondary:hover { + background: #4a7a6a; +} + +.btn-danger { + background: var(--btn-danger); + color: var(--chalk-white); +} + +.btn-danger:hover { + background: var(--btn-danger-h); +} + +.btn-icon { + background: transparent; + border: 1px solid var(--input-border); + color: var(--chalk-white); + padding: 6px 12px; + font-size: 1.1rem; +} + +.btn-icon:hover { + background: rgba(255,255,255,0.08); +} + +/* βββ Action row ββββββββββββββββββββββββββββββββββββββββββββββββ */ +.action-row { + display: flex; + gap: 8px; + justify-content: flex-end; + flex-wrap: wrap; +} + +/* βββ Error message βββββββββββββββββββββββββββββββββββββββββββββ */ +.error-msg { + min-height: 1.2em; + font-size: 0.85rem; + color: #ff8080; + opacity: 0; + transition: opacity 0.25s; + text-align: center; +} + +.error-msg.visible { + opacity: 1; +} + +/* βββ Win overlay βββββββββββββββββββββββββββββββββββββββββββββββ */ +#win-overlay { + position: fixed; + inset: 0; + background: rgba(10, 25, 10, 0.88); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + opacity: 0; + pointer-events: none; + transition: opacity 0.4s ease; + backdrop-filter: blur(3px); +} + +#win-overlay.active { + opacity: 1; + pointer-events: all; +} + +.win-content { + text-align: center; + animation: winPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; +} + +@keyframes winPop { + from { transform: scale(0.5); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +.win-icon { + font-size: 4rem; + margin-bottom: 12px; + animation: winSpin 0.8s ease-out; +} + +@keyframes winSpin { + from { transform: rotate(-20deg) scale(0.6); } + to { transform: rotate(0deg) scale(1); } +} + +#win-overlay h2 { + font-size: 2.8rem; + color: var(--winner-glow); + text-shadow: 0 0 24px rgba(255,215,0,0.7); + margin-bottom: 10px; + letter-spacing: 0.1em; +} + +.win-team { + font-size: 1.8rem; + color: var(--chalk-white); + text-shadow: 0 0 16px rgba(240,237,224,0.6); + margin-bottom: 16px; +} + +.win-hint { + font-size: 0.8rem; + color: var(--chalk-dim); + opacity: 0.7; + margin-top: 18px; +} + +/* βββ Chalk dust particles ββββββββββββββββββββββββββββββββββββββ */ +.chalk-particles { + position: absolute; + inset: 0; + pointer-events: none; + overflow: hidden; +} + +.chalk-particles span { + position: absolute; + display: block; + width: 6px; + height: 6px; + background: rgba(240, 237, 224, 0.6); + border-radius: 50%; + animation: float 3s ease-in-out infinite; +} + +.chalk-particles span:nth-child(1) { left: 10%; top: 20%; animation-delay: 0s; animation-duration: 2.8s; width: 4px; height: 4px; } +.chalk-particles span:nth-child(2) { left: 25%; top: 60%; animation-delay: 0.4s; animation-duration: 3.2s; } +.chalk-particles span:nth-child(3) { left: 45%; top: 15%; animation-delay: 0.8s; animation-duration: 2.5s; width: 8px; height: 8px; } +.chalk-particles span:nth-child(4) { left: 65%; top: 70%; animation-delay: 1.2s; animation-duration: 3.6s; width: 5px; height: 5px; } +.chalk-particles span:nth-child(5) { left: 80%; top: 30%; animation-delay: 0.2s; animation-duration: 2.9s; } +.chalk-particles span:nth-child(6) { left: 90%; top: 80%; animation-delay: 1.6s; animation-duration: 3.4s; width: 3px; height: 3px; } +.chalk-particles span:nth-child(7) { left: 55%; top: 45%; animation-delay: 0.6s; animation-duration: 2.6s; width: 7px; height: 7px; } +.chalk-particles span:nth-child(8) { left: 35%; top: 85%; animation-delay: 1.0s; animation-duration: 3.0s; } +.chalk-particles span:nth-child(9) { left: 72%; top: 10%; animation-delay: 1.4s; animation-duration: 2.7s; width: 5px; height: 5px; } + +@keyframes float { + 0% { transform: translateY(0) rotate(0deg); opacity: 0.8; } + 50% { transform: translateY(-30px) rotate(180deg); opacity: 0.4; } + 100% { transform: translateY(-60px) rotate(360deg); opacity: 0; } +} + +/* βββ Responsive ββββββββββββββββββββββββββββββββββββββββββββββββ */ +@media (max-width: 480px) { + .app-wrapper { padding: 8px 8px 20px; } + .header-title { font-size: 1.3rem; } + .panel-team-name { font-size: 0.95rem; } + .total-value { font-size: 1.15rem; } + .mark-pts { font-size: 1.1rem; } + #win-overlay h2 { font-size: 2rem; } + .win-team { font-size: 1.3rem; } +} + +@media (min-width: 600px) { + .board { min-height: 480px; } +} diff --git a/jass-scoreboard/index.html b/jass-scoreboard/index.html new file mode 100644 index 0000000..15f27ec --- /dev/null +++ b/jass-scoreboard/index.html @@ -0,0 +1,118 @@ + + +
+ + +Tippe zum Schliessen
+