From de33b5038cbbc50526eb0fd5b6b097c5e59816ea Mon Sep 17 00:00:00 2001 From: notune Date: Tue, 25 Nov 2025 22:48:45 +0100 Subject: [PATCH 1/4] simplify odds generator --- layouts/shortcodes/odds-selector.html | 47 +++++--------------- themes/leela/assets/js/additional/odds.js | 54 ++++++++--------------- 2 files changed, 31 insertions(+), 70 deletions(-) diff --git a/layouts/shortcodes/odds-selector.html b/layouts/shortcodes/odds-selector.html index 7131744..a3946c5 100644 --- a/layouts/shortcodes/odds-selector.html +++ b/layouts/shortcodes/odds-selector.html @@ -18,7 +18,7 @@

Game Configuration

@@ -106,41 +106,18 @@

Rooks

- - + +
+
+ + - -
-
-
-
-
Target Bot
-
LeelaQueenOdds
-
-
- - - Open Lichess - -
-
- - -
-
- FEN - rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -
- -
+ +
diff --git a/themes/leela/assets/js/additional/odds.js b/themes/leela/assets/js/additional/odds.js index e20e07a..8ad35d8 100644 --- a/themes/leela/assets/js/additional/odds.js +++ b/themes/leela/assets/js/additional/odds.js @@ -37,18 +37,11 @@ document.addEventListener("DOMContentLoaded", function() { // --- DOM Elements --- const generateBtn = document.getElementById('generateBtn'); - const resultCard = document.getElementById('resultCard'); const errorMessage = document.getElementById('errorMessage'); const frcToggle = document.getElementById('frc-toggle'); const frcInputContainer = document.getElementById('frc-input-container'); const frcIdInput = document.getElementById('frc-id'); const copyBtn = document.getElementById('copyBtn'); - const openLink = document.getElementById('openLink'); - const botValue = document.getElementById('botValue'); - const fenValue = document.getElementById('fenValue'); - const linkValue = document.getElementById('linkValue'); - const frcIdResultContainer = document.getElementById('frcIdResultContainer'); - const frcIdValue = document.getElementById('frcIdValue'); // Select labels based on their 'for' attribute since they don't have IDs in the HTML const knight_q_label = document.querySelector('label[for="knight_q"]'); @@ -89,17 +82,23 @@ document.addEventListener("DOMContentLoaded", function() { // Initialize visibility based on default state updateFRCToggle(); - generateBtn.addEventListener('click', generateLink); - copyBtn.addEventListener('click', copyLink); + generateBtn.addEventListener('click', function() { + if (calculateAndSetUrl()) { + window.open(url, '_blank'); + } + }); + + copyBtn.addEventListener('click', function() { + if (calculateAndSetUrl()) { + copyLink(); + } + }); - function generateLink() { + function calculateAndSetUrl() { const isFRC = frcToggle.checked; let frcID; - frcIdResultContainer.classList.add('hidden'); - errorMessage.parentNode.classList.add('hidden'); - resultCard.classList.remove('active'); if (isFRC) { const isFrcIdUserSpecified = frcIdInput.value !== ''; @@ -110,7 +109,7 @@ document.addEventListener("DOMContentLoaded", function() { if (frcID === 518) { errorMessage.textContent = 'Standard Chess (ID 518) is not allowed in FRC mode.'; errorMessage.parentNode.classList.remove('hidden'); - return; + return false; } } else { // User did not specify an ID, so randomize it @@ -126,7 +125,7 @@ document.addEventListener("DOMContentLoaded", function() { console.log('Invalid FRC ID detected:', frcID); errorMessage.textContent = 'Invalid FRC ID. Please enter a number between 0 and 959.'; errorMessage.parentNode.classList.remove('hidden'); - return; + return false; } const playerColor = document.querySelector('input[name="color"]:checked').value; @@ -160,7 +159,7 @@ document.addEventListener("DOMContentLoaded", function() { console.log('No pieces selected for removal.'); errorMessage.textContent = 'Please select at least one piece to remove.'; errorMessage.parentNode.classList.remove('hidden'); - return; + return false; } if (!isValidOdds(removedPieces, isFRC)) { @@ -184,7 +183,7 @@ document.addEventListener("DOMContentLoaded", function() { console.log('Invalid piece selection:', removedPieces); errorMessage.textContent = `Invalid Piece Selection: ${guidance}`; errorMessage.parentNode.classList.remove('hidden'); - return; + return false; } // Determine bot user @@ -204,19 +203,8 @@ document.addEventListener("DOMContentLoaded", function() { const encodedFen = fen.replace(/ /g, '_'); url = `https://lichess.org/?user=${botUser}&fen=${encodedFen}#friend`; - - - if (isFRC) { - frcIdValue.textContent = frcID; - frcIdResultContainer.classList.remove('hidden'); - } - - botValue.textContent = botUser; - fenValue.textContent = fen; - openLink.setAttribute('href', url); - - // Show result card - resultCard.classList.add('active'); + + return true; } @@ -443,7 +431,7 @@ document.addEventListener("DOMContentLoaded", function() { navigator.clipboard.writeText(url).then(() => { // Visual feedback for successful copy const originalText = copyBtn.innerHTML; - copyBtn.innerHTML = `Copied!`; + copyBtn.innerHTML = ``; copyBtn.style.backgroundColor = 'var(--color-success)'; setTimeout(() => { @@ -468,8 +456,4 @@ document.addEventListener("DOMContentLoaded", function() { } }); }); - - // Initialize with hidden result card - resultCard.classList.remove('active'); - errorMessage.parentNode.classList.add('hidden'); }); From c894890d8c0c25f3f080d7baaa0358b264051fba Mon Sep 17 00:00:00 2001 From: notune Date: Wed, 26 Nov 2025 11:53:40 +0100 Subject: [PATCH 2/4] disabled button for invalid config + guidance below button --- layouts/shortcodes/odds-selector.html | 19 +- themes/leela/assets/css/additional/odds.css | 34 ++- themes/leela/assets/js/additional/odds.js | 312 ++++++++++++-------- 3 files changed, 221 insertions(+), 144 deletions(-) diff --git a/layouts/shortcodes/odds-selector.html b/layouts/shortcodes/odds-selector.html index a3946c5..27db5a9 100644 --- a/layouts/shortcodes/odds-selector.html +++ b/layouts/shortcodes/odds-selector.html @@ -1,7 +1,7 @@ {{ .Page.Scratch.Add "requiredCSS" (slice "odds.css") }} {{ .Page.Scratch.Add "requiredJS" (slice "odds.js") }} -
+
@@ -110,23 +110,18 @@

Rooks

- -
- - +
-
\ No newline at end of file +
diff --git a/themes/leela/assets/css/additional/odds.css b/themes/leela/assets/css/additional/odds.css index 4868028..7ff37be 100644 --- a/themes/leela/assets/css/additional/odds.css +++ b/themes/leela/assets/css/additional/odds.css @@ -347,12 +347,28 @@ width: 60px; } -.detail-code { - color: var(--color-text-secondary); - word-break: break-all; -} - -/* --- Helpers --- */ -.hidden { - display: none; -} +.detail-code { + color: var(--color-text-secondary); + word-break: break-all; +} + +.odds-selector .odds-btn { + transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease; +} + +.odds-selector .odds-btn:disabled, +.odds-selector .odds-btn.odds-btn-disabled { + background-color: #d1d5db; + border-color: #d1d5db; + color: #4b5563; + cursor: not-allowed; +} + +.odds-selector .config-hint { + color: #000; +} + +/* --- Helpers --- */ +.hidden { + display: none; +} diff --git a/themes/leela/assets/js/additional/odds.js b/themes/leela/assets/js/additional/odds.js index 8ad35d8..d3cc1eb 100644 --- a/themes/leela/assets/js/additional/odds.js +++ b/themes/leela/assets/js/additional/odds.js @@ -36,17 +36,19 @@ document.addEventListener("DOMContentLoaded", function() { // --- DOM Elements --- - const generateBtn = document.getElementById('generateBtn'); - const errorMessage = document.getElementById('errorMessage'); - const frcToggle = document.getElementById('frc-toggle'); - const frcInputContainer = document.getElementById('frc-input-container'); - const frcIdInput = document.getElementById('frc-id'); - const copyBtn = document.getElementById('copyBtn'); - - // Select labels based on their 'for' attribute since they don't have IDs in the HTML - const knight_q_label = document.querySelector('label[for="knight_q"]'); - const knight_k_label = document.querySelector('label[for="knight_k"]'); - const bishop_q_label = document.querySelector('label[for="bishop_q"]'); + const generateBtn = document.getElementById('generateBtn'); + const configHint = document.getElementById('configHint'); + const frcToggle = document.getElementById('frc-toggle'); + const frcInputContainer = document.getElementById('frc-input-container'); + const frcIdInput = document.getElementById('frc-id'); + const copyBtn = document.getElementById('copyBtn'); + const pieceCheckboxIds = ['queen', 'knight_q', 'knight_k', 'bishop_q', 'bishop_k', 'rook_q', 'rook_k']; + const defaultHint = 'Complete the configuration to generate a challenge.'; + + // Select labels based on their 'for' attribute since they don't have IDs in the HTML + const knight_q_label = document.querySelector('label[for="knight_q"]'); + const knight_k_label = document.querySelector('label[for="knight_k"]'); + const bishop_q_label = document.querySelector('label[for="bishop_q"]'); const bishop_k_label = document.querySelector('label[for="bishop_k"]'); const rook_q_label = document.querySelector('label[for="rook_q"]'); const rook_k_label = document.querySelector('label[for="rook_k"]'); @@ -77,15 +79,19 @@ document.addEventListener("DOMContentLoaded", function() { } } - frcToggle.addEventListener('change', updateFRCToggle); - - // Initialize visibility based on default state - updateFRCToggle(); - - generateBtn.addEventListener('click', function() { - if (calculateAndSetUrl()) { - window.open(url, '_blank'); - } + frcToggle.addEventListener('change', () => { + updateFRCToggle(); + updateActionState(); + }); + + // Initialize visibility based on default state + updateFRCToggle(); + updateActionState(); + + generateBtn.addEventListener('click', function() { + if (calculateAndSetUrl()) { + window.open(url, '_blank'); + } }); copyBtn.addEventListener('click', function() { @@ -94,60 +100,73 @@ document.addEventListener("DOMContentLoaded", function() { } }); - function calculateAndSetUrl() { - const isFRC = frcToggle.checked; - let frcID; - - errorMessage.parentNode.classList.add('hidden'); - - if (isFRC) { - const isFrcIdUserSpecified = frcIdInput.value !== ''; - - if (isFrcIdUserSpecified) { - frcID = parseInt(frcIdInput.value, 10); - - if (frcID === 518) { - errorMessage.textContent = 'Standard Chess (ID 518) is not allowed in FRC mode.'; - errorMessage.parentNode.classList.remove('hidden'); - return false; - } - } else { - // User did not specify an ID, so randomize it - // Keep generating until we get a number that isn't 518 (Standard Position) - do { - frcID = Math.floor(Math.random() * 960); - } while (frcID === 518); - } - } - - - if (isFRC && !isValidID(frcID)) { - console.log('Invalid FRC ID detected:', frcID); - errorMessage.textContent = 'Invalid FRC ID. Please enter a number between 0 and 959.'; - errorMessage.parentNode.classList.remove('hidden'); - return false; - } - - const playerColor = document.querySelector('input[name="color"]:checked').value; - const initialArrangement = isFRC ? decode(frcID) : ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R']; - const files = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']; - - - // Map checkboxes to their piece definitions in the initial arrangement - const pieceDefinitions = definePieces(initialArrangement, playerColor, isFRC); - - let workingArrangement = [...initialArrangement]; - let removedPieces = { Q: 0, N: 0, R: 0, B: 0 }; - let removedRookFiles = []; - - let pieceCheckboxes = ['queen', 'knight_q', 'knight_k', 'bishop_q', 'bishop_k', 'rook_q', 'rook_k']; - - for (const id of pieceCheckboxes) { - if (document.getElementById(id).checked) { - const def = pieceDefinitions[id]; - if (def && workingArrangement[def.index] === def.piece) { - workingArrangement[def.index] = null; - removedPieces[def.piece]++; + pieceCheckboxIds.forEach(id => { + const checkbox = document.getElementById(id); + if (checkbox) { + checkbox.addEventListener('change', updateActionState); + } + }); + + frcIdInput.addEventListener('input', updateActionState); + + const colorRadios = document.querySelectorAll('input[name="color"]'); + + colorRadios.forEach(radio => { + radio.addEventListener('change', function() { + // Remove 'selected' class from all radio cards + document.querySelectorAll('.radio-card').forEach(card => { + card.classList.remove('selected'); + }); + + // Add 'selected' class to the parent card of the checked radio + if (this.checked) { + this.closest('.radio-card').classList.add('selected'); + } + updateActionState(); + }); + }); + + function calculateAndSetUrl() { + const validation = validateConfiguration(); + setConfigHint(validation.valid, validation.message || defaultHint); + if (!validation.valid) { + return false; + } + + const isFRC = frcToggle.checked; + let frcID = validation.frcID; + + if (isFRC) { + const isFrcIdUserSpecified = frcIdInput.value !== ''; + + if (!isFrcIdUserSpecified) { + // User did not specify an ID, so randomize it + // Keep generating until we get a number that isn't 518 (Standard Position) + do { + frcID = Math.floor(Math.random() * 960); + } while (frcID === 518); + } + } + + + const playerColor = document.querySelector('input[name="color"]:checked').value; + const initialArrangement = isFRC ? decode(frcID) : ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R']; + const files = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']; + + + // Map checkboxes to their piece definitions in the initial arrangement + const pieceDefinitions = definePieces(initialArrangement, playerColor, isFRC); + + let workingArrangement = [...initialArrangement]; + let removedPieces = { Q: 0, N: 0, R: 0, B: 0 }; + let removedRookFiles = []; + + for (const id of pieceCheckboxIds) { + if (document.getElementById(id).checked) { + const def = pieceDefinitions[id]; + if (def && workingArrangement[def.index] === def.piece) { + workingArrangement[def.index] = null; + removedPieces[def.piece]++; if (def.piece === 'R') { removedRookFiles.push(files[def.index]); } @@ -155,39 +174,8 @@ document.addEventListener("DOMContentLoaded", function() { } } - if (Object.values(removedPieces).reduce((a, b) => a + b, 0) === 0) { - console.log('No pieces selected for removal.'); - errorMessage.textContent = 'Please select at least one piece to remove.'; - errorMessage.parentNode.classList.remove('hidden'); - return false; - } - - if (!isValidOdds(removedPieces, isFRC)) { - const { Q, N, R, B } = removedPieces; - let guidance = "This combination of pieces is not supported."; - if (Q === 0 && N === 1 && R === 0 && B === 1) { - guidance = "For Bishop+Knight odds, select one bishop and one knight from opposite sides of the board"; - } - if (Q === 0 && N === 1 && R === 1 && B === 0) { - guidance = "For Rook+Knight odds, select queen-side rook and king-side knight"; - } - if (Q === 1 && N === 0 && R === 1 && B === 0) { - guidance = "For Queen+Rook odds, select queen-side rook only"; - } - if (Q === 0 && N === 0 && R === 1 && B === 1) { - guidance = "Rook+Bishop odds is not supported"; - } - if (Q === 1 && N === 2 && R === 2 && B === 2) { - guidance = "At least give the bot a fighting chance!"; - } - console.log('Invalid piece selection:', removedPieces); - errorMessage.textContent = `Invalid Piece Selection: ${guidance}`; - errorMessage.parentNode.classList.remove('hidden'); - return false; - } - - // Determine bot user - const botUser = getBotUser(removedPieces, isFRC); + // Determine bot user + const botUser = getBotUser(removedPieces, isFRC); // Build FEN const fenRank = arrangementToFenRank(workingArrangement); @@ -441,19 +429,97 @@ document.addEventListener("DOMContentLoaded", function() { }); } - const colorRadios = document.querySelectorAll('input[name="color"]'); - - colorRadios.forEach(radio => { - radio.addEventListener('change', function() { - // Remove 'selected' class from all radio cards - document.querySelectorAll('.radio-card').forEach(card => { - card.classList.remove('selected'); - }); - - // Add 'selected' class to the parent card of the checked radio - if (this.checked) { - this.closest('.radio-card').classList.add('selected'); - } - }); - }); -}); + function validateConfiguration() { + const isFRC = frcToggle.checked; + const frcValue = frcIdInput.value.trim(); + let frcID = null; + + if (isFRC && frcValue !== '') { + if (!/^\d+$/.test(frcValue)) { + return { valid: false, message: 'Enter a whole number between 0 and 959 for the FRC ID.' }; + } + + frcID = parseInt(frcValue, 10); + + if (!isValidID(frcID)) { + return { valid: false, message: 'Enter a number between 0 and 959 for the FRC ID.' }; + } + + if (frcID === 518) { + return { valid: false, message: 'Standard Chess (ID 518) is not allowed in FRC mode.' }; + } + } + + const removedPieces = getRemovedPieces(); + const totalRemoved = Object.values(removedPieces).reduce((a, b) => a + b, 0); + if (totalRemoved === 0) { + return { valid: false, message: 'Select at least one piece to remove.' }; + } + + if (!isValidOdds(removedPieces, isFRC)) { + return { valid: false, message: buildInvalidSelectionGuidance(removedPieces) }; + } + + return { valid: true, message: '', frcID, isFRC }; + } + + function getRemovedPieces() { + const removedPieces = { Q: 0, N: 0, R: 0, B: 0 }; + pieceCheckboxIds.forEach(id => { + if (document.getElementById(id).checked) { + if (id.startsWith('knight')) removedPieces.N++; + if (id.startsWith('rook')) removedPieces.R++; + if (id.startsWith('bishop')) removedPieces.B++; + if (id === 'queen') removedPieces.Q++; + } + }); + return removedPieces; + } + + function buildInvalidSelectionGuidance(removedPieces) { + const { Q, N, R, B } = removedPieces; + if (Q === 0 && N === 1 && R === 0 && B === 1) { + return 'For Bishop+Knight odds, select one bishop and one knight from opposite sides of the board.'; + } + if (Q === 0 && N === 1 && R === 1 && B === 0) { + return 'For Rook+Knight odds, select queen-side rook and king-side knight.'; + } + if (Q === 1 && N === 0 && R === 1 && B === 0) { + return 'For Queen+Rook odds, select queen-side rook only.'; + } + if (Q === 0 && N === 0 && R === 1 && B === 1) { + return 'Rook+Bishop odds are not supported.'; + } + if (Q === 1 && N === 2 && R === 2 && B === 2) { + return 'At least give the bot a fighting chance!'; + } + return 'This combination of pieces is not supported.'; + } + + function setButtonDisabledState(isDisabled) { + [generateBtn, copyBtn].forEach(button => { + if (!button) return; + button.disabled = isDisabled; + button.classList.toggle('odds-btn-disabled', isDisabled); + }); + } + + function setConfigHint(isValid, message) { + setButtonDisabledState(!isValid); + if (!configHint) return; + if (isValid) { + configHint.classList.add('hidden'); + configHint.textContent = ''; + return; + } + + configHint.classList.remove('hidden'); + configHint.textContent = message; + } + + function updateActionState() { + const validation = validateConfiguration(); + const message = validation.message || defaultHint; + setConfigHint(validation.valid, message); + } +}); From 8e2eef9ca83af57d78b2cc3d82d003acea304035 Mon Sep 17 00:00:00 2001 From: notune Date: Wed, 26 Nov 2025 12:19:55 +0100 Subject: [PATCH 3/4] add checbox to toggle random frc value & update generated value in textbox after challenge click --- layouts/shortcodes/odds-selector.html | 22 ++++-- themes/leela/assets/css/additional/odds.css | 85 +++++++++++++++------ themes/leela/assets/js/additional/odds.js | 41 ++++++++-- 3 files changed, 107 insertions(+), 41 deletions(-) diff --git a/layouts/shortcodes/odds-selector.html b/layouts/shortcodes/odds-selector.html index 27db5a9..77bf5d3 100644 --- a/layouts/shortcodes/odds-selector.html +++ b/layouts/shortcodes/odds-selector.html @@ -15,14 +15,20 @@

Game Configuration

- -
+ +
diff --git a/themes/leela/assets/css/additional/odds.css b/themes/leela/assets/css/additional/odds.css index 7ff37be..6184ac2 100644 --- a/themes/leela/assets/css/additional/odds.css +++ b/themes/leela/assets/css/additional/odds.css @@ -52,31 +52,66 @@ border-left: 2px solid var(--color-border-primary); } -.input-group { - display: flex; - align-items: center; - gap: 1rem; -} - -#frc-id { - padding: 0.5rem; - border: 1px solid var(--color-border-primary); - background-color: var(--color-bg-secondary); - color: var(--color-text-primary); - border-radius: 0.375rem; - width: 100px; -} - -#frc-id:focus { - border-color: var(--color-border-active); - outline: none; -} - -.helper-text { - display: block; - margin-top: 0.5rem; - font-size: 0.75rem; - font-style: italic; +.input-group { + display: flex; + align-items: center; + gap: 1rem; +} + +.frc-input-group { + flex-wrap: wrap; +} + +#frc-id { + padding: 0.5rem; + border: 1px solid var(--color-border-primary); + background-color: var(--color-bg-primary); + color: var(--color-text-primary); + border-radius: 0.375rem; + width: 100px; +} + +#frc-id[readonly] { + background-color: var(--color-bg-secondary); + color: var(--color-text-secondary); + cursor: not-allowed; +} + +#frc-id:focus { + border-color: var(--color-border-active); + outline: none; +} + +.frc-input-controls { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.frc-random-toggle { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.85rem; + font-weight: 600; + color: var(--color-text-primary); + cursor: pointer; + user-select: none; +} + +.frc-random-toggle input { + width: 1rem; + height: 1rem; + cursor: pointer; + accent-color: var(--color-accent); +} + +.helper-text { + display: block; + margin-top: 0.5rem; + font-size: 0.75rem; + font-style: italic; color: var(--color-text-tertiary); } diff --git a/themes/leela/assets/js/additional/odds.js b/themes/leela/assets/js/additional/odds.js index d3cc1eb..37d16c0 100644 --- a/themes/leela/assets/js/additional/odds.js +++ b/themes/leela/assets/js/additional/odds.js @@ -41,6 +41,7 @@ document.addEventListener("DOMContentLoaded", function() { const frcToggle = document.getElementById('frc-toggle'); const frcInputContainer = document.getElementById('frc-input-container'); const frcIdInput = document.getElementById('frc-id'); + const frcRandomCheckbox = document.getElementById('frc-random'); const copyBtn = document.getElementById('copyBtn'); const pieceCheckboxIds = ['queen', 'knight_q', 'knight_k', 'bishop_q', 'bishop_k', 'rook_q', 'rook_k']; const defaultHint = 'Complete the configuration to generate a challenge.'; @@ -57,9 +58,10 @@ document.addEventListener("DOMContentLoaded", function() { // Event Listeners - function updateFRCToggle() { - const isFRC = frcToggle.checked; - frcInputContainer.classList.toggle('hidden', !isFRC); + function updateFRCToggle() { + const isFRC = frcToggle.checked; + frcInputContainer.classList.toggle('hidden', !isFRC); + updateFrcInputState(); if (!isFRC) { // Standard Chess Labels @@ -109,6 +111,15 @@ document.addEventListener("DOMContentLoaded", function() { frcIdInput.addEventListener('input', updateActionState); + if (frcRandomCheckbox) { + frcRandomCheckbox.addEventListener('change', () => { + updateFrcInputState(); + updateActionState(); + }); + } + + updateFrcInputState(); + const colorRadios = document.querySelectorAll('input[name="color"]'); colorRadios.forEach(radio => { @@ -134,17 +145,20 @@ document.addEventListener("DOMContentLoaded", function() { } const isFRC = frcToggle.checked; + const shouldRandomize = isFRC && frcRandomCheckbox && frcRandomCheckbox.checked; let frcID = validation.frcID; if (isFRC) { - const isFrcIdUserSpecified = frcIdInput.value !== ''; - - if (!isFrcIdUserSpecified) { - // User did not specify an ID, so randomize it + if (shouldRandomize) { // Keep generating until we get a number that isn't 518 (Standard Position) do { frcID = Math.floor(Math.random() * 960); } while (frcID === 518); + if (frcIdInput) { + frcIdInput.value = frcID; + } + } else if (typeof frcID !== 'number') { + return false; } } @@ -432,9 +446,14 @@ document.addEventListener("DOMContentLoaded", function() { function validateConfiguration() { const isFRC = frcToggle.checked; const frcValue = frcIdInput.value.trim(); + const shouldRandomize = frcRandomCheckbox && frcRandomCheckbox.checked; let frcID = null; - if (isFRC && frcValue !== '') { + if (isFRC && !shouldRandomize) { + if (frcValue === '') { + return { valid: false, message: 'Enter a number between 0 and 959 for the FRC ID.' }; + } + if (!/^\d+$/.test(frcValue)) { return { valid: false, message: 'Enter a whole number between 0 and 959 for the FRC ID.' }; } @@ -522,4 +541,10 @@ document.addEventListener("DOMContentLoaded", function() { const message = validation.message || defaultHint; setConfigHint(validation.valid, message); } + + function updateFrcInputState() { + if (!frcIdInput || !frcRandomCheckbox) return; + const shouldRandomize = frcRandomCheckbox.checked; + frcIdInput.readOnly = shouldRandomize; + } }); From 4d7ca11f2c2356b68173b3a448abad06b6a59cb9 Mon Sep 17 00:00:00 2001 From: notune Date: Wed, 26 Nov 2025 14:11:10 +0100 Subject: [PATCH 4/4] allow to copy invalid config --- themes/leela/assets/js/additional/odds.js | 434 +++++++++++----------- 1 file changed, 214 insertions(+), 220 deletions(-) diff --git a/themes/leela/assets/js/additional/odds.js b/themes/leela/assets/js/additional/odds.js index 37d16c0..a706c48 100644 --- a/themes/leela/assets/js/additional/odds.js +++ b/themes/leela/assets/js/additional/odds.js @@ -36,20 +36,20 @@ document.addEventListener("DOMContentLoaded", function() { // --- DOM Elements --- - const generateBtn = document.getElementById('generateBtn'); - const configHint = document.getElementById('configHint'); - const frcToggle = document.getElementById('frc-toggle'); - const frcInputContainer = document.getElementById('frc-input-container'); - const frcIdInput = document.getElementById('frc-id'); - const frcRandomCheckbox = document.getElementById('frc-random'); - const copyBtn = document.getElementById('copyBtn'); - const pieceCheckboxIds = ['queen', 'knight_q', 'knight_k', 'bishop_q', 'bishop_k', 'rook_q', 'rook_k']; - const defaultHint = 'Complete the configuration to generate a challenge.'; - - // Select labels based on their 'for' attribute since they don't have IDs in the HTML - const knight_q_label = document.querySelector('label[for="knight_q"]'); - const knight_k_label = document.querySelector('label[for="knight_k"]'); - const bishop_q_label = document.querySelector('label[for="bishop_q"]'); + const generateBtn = document.getElementById('generateBtn'); + const configHint = document.getElementById('configHint'); + const frcToggle = document.getElementById('frc-toggle'); + const frcInputContainer = document.getElementById('frc-input-container'); + const frcIdInput = document.getElementById('frc-id'); + const frcRandomCheckbox = document.getElementById('frc-random'); + const copyBtn = document.getElementById('copyBtn'); + const pieceCheckboxIds = ['queen', 'knight_q', 'knight_k', 'bishop_q', 'bishop_k', 'rook_q', 'rook_k']; + const defaultHint = 'Complete the configuration to generate a challenge.'; + + // Select labels based on their 'for' attribute since they don't have IDs in the HTML + const knight_q_label = document.querySelector('label[for="knight_q"]'); + const knight_k_label = document.querySelector('label[for="knight_k"]'); + const bishop_q_label = document.querySelector('label[for="bishop_q"]'); const bishop_k_label = document.querySelector('label[for="bishop_k"]'); const rook_q_label = document.querySelector('label[for="rook_q"]'); const rook_k_label = document.querySelector('label[for="rook_k"]'); @@ -58,10 +58,10 @@ document.addEventListener("DOMContentLoaded", function() { // Event Listeners - function updateFRCToggle() { - const isFRC = frcToggle.checked; - frcInputContainer.classList.toggle('hidden', !isFRC); - updateFrcInputState(); + function updateFRCToggle() { + const isFRC = frcToggle.checked; + frcInputContainer.classList.toggle('hidden', !isFRC); + updateFrcInputState(); if (!isFRC) { // Standard Chess Labels @@ -81,106 +81,102 @@ document.addEventListener("DOMContentLoaded", function() { } } - frcToggle.addEventListener('change', () => { - updateFRCToggle(); - updateActionState(); - }); - - // Initialize visibility based on default state - updateFRCToggle(); - updateActionState(); - - generateBtn.addEventListener('click', function() { - if (calculateAndSetUrl()) { - window.open(url, '_blank'); - } + frcToggle.addEventListener('change', () => { + updateFRCToggle(); + updateActionState(); + }); + + // Initialize visibility based on default state + updateFRCToggle(); + updateActionState(); + + generateBtn.addEventListener('click', function() { + if (calculateAndSetUrl()) { + window.open(url, '_blank'); + } }); copyBtn.addEventListener('click', function() { - if (calculateAndSetUrl()) { - copyLink(); + calculateAndSetUrl() + copyLink(); + }); + + pieceCheckboxIds.forEach(id => { + const checkbox = document.getElementById(id); + if (checkbox) { + checkbox.addEventListener('change', updateActionState); } }); - pieceCheckboxIds.forEach(id => { - const checkbox = document.getElementById(id); - if (checkbox) { - checkbox.addEventListener('change', updateActionState); - } - }); - - frcIdInput.addEventListener('input', updateActionState); - - if (frcRandomCheckbox) { - frcRandomCheckbox.addEventListener('change', () => { - updateFrcInputState(); - updateActionState(); - }); - } - - updateFrcInputState(); - - const colorRadios = document.querySelectorAll('input[name="color"]'); - - colorRadios.forEach(radio => { - radio.addEventListener('change', function() { - // Remove 'selected' class from all radio cards - document.querySelectorAll('.radio-card').forEach(card => { - card.classList.remove('selected'); - }); - - // Add 'selected' class to the parent card of the checked radio - if (this.checked) { - this.closest('.radio-card').classList.add('selected'); - } - updateActionState(); - }); - }); - - function calculateAndSetUrl() { - const validation = validateConfiguration(); - setConfigHint(validation.valid, validation.message || defaultHint); - if (!validation.valid) { - return false; - } - - const isFRC = frcToggle.checked; - const shouldRandomize = isFRC && frcRandomCheckbox && frcRandomCheckbox.checked; - let frcID = validation.frcID; - - if (isFRC) { - if (shouldRandomize) { - // Keep generating until we get a number that isn't 518 (Standard Position) - do { - frcID = Math.floor(Math.random() * 960); - } while (frcID === 518); - if (frcIdInput) { - frcIdInput.value = frcID; - } - } else if (typeof frcID !== 'number') { - return false; - } - } - - - const playerColor = document.querySelector('input[name="color"]:checked').value; - const initialArrangement = isFRC ? decode(frcID) : ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R']; - const files = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']; - - - // Map checkboxes to their piece definitions in the initial arrangement - const pieceDefinitions = definePieces(initialArrangement, playerColor, isFRC); - - let workingArrangement = [...initialArrangement]; - let removedPieces = { Q: 0, N: 0, R: 0, B: 0 }; - let removedRookFiles = []; - - for (const id of pieceCheckboxIds) { - if (document.getElementById(id).checked) { - const def = pieceDefinitions[id]; - if (def && workingArrangement[def.index] === def.piece) { - workingArrangement[def.index] = null; - removedPieces[def.piece]++; + frcIdInput.addEventListener('input', updateActionState); + + if (frcRandomCheckbox) { + frcRandomCheckbox.addEventListener('change', () => { + updateFrcInputState(); + updateActionState(); + }); + } + + updateFrcInputState(); + + const colorRadios = document.querySelectorAll('input[name="color"]'); + + colorRadios.forEach(radio => { + radio.addEventListener('change', function() { + // Remove 'selected' class from all radio cards + document.querySelectorAll('.radio-card').forEach(card => { + card.classList.remove('selected'); + }); + + // Add 'selected' class to the parent card of the checked radio + if (this.checked) { + this.closest('.radio-card').classList.add('selected'); + } + updateActionState(); + }); + }); + + function calculateAndSetUrl() { + const validation = validateConfiguration(); + setConfigHint(validation.valid, validation.message || defaultHint); + + const isFRC = frcToggle.checked; + const shouldRandomize = isFRC && frcRandomCheckbox && frcRandomCheckbox.checked; + let frcID = validation.frcID; + + if (isFRC) { + if (shouldRandomize) { + // Keep generating until we get a number that isn't 518 (Standard Position) + do { + frcID = Math.floor(Math.random() * 960); + } while (frcID === 518); + if (frcIdInput) { + frcIdInput.value = frcID; + } + } else if (typeof frcID !== 'number') { + return false; + } + } + + + const playerColor = document.querySelector('input[name="color"]:checked').value; + const initialArrangement = isFRC ? decode(frcID) : ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R']; + const files = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']; + + + // Map checkboxes to their piece definitions in the initial arrangement + const pieceDefinitions = definePieces(initialArrangement, playerColor, isFRC); + + let workingArrangement = [...initialArrangement]; + let removedPieces = { Q: 0, N: 0, R: 0, B: 0 }; + let removedRookFiles = []; + + for (const id of pieceCheckboxIds) { + if (document.getElementById(id).checked) { + const def = pieceDefinitions[id]; + if (def && workingArrangement[def.index] === def.piece) { + workingArrangement[def.index] = null; + removedPieces[def.piece]++; if (def.piece === 'R') { removedRookFiles.push(files[def.index]); } @@ -188,8 +184,8 @@ document.addEventListener("DOMContentLoaded", function() { } } - // Determine bot user - const botUser = getBotUser(removedPieces, isFRC); + // Determine bot user + const botUser = getBotUser(removedPieces, isFRC); // Build FEN const fenRank = arrangementToFenRank(workingArrangement); @@ -206,7 +202,7 @@ document.addEventListener("DOMContentLoaded", function() { const encodedFen = fen.replace(/ /g, '_'); url = `https://lichess.org/?user=${botUser}&fen=${encodedFen}#friend`; - return true; + return validation.valid; } @@ -443,108 +439,106 @@ document.addEventListener("DOMContentLoaded", function() { }); } - function validateConfiguration() { - const isFRC = frcToggle.checked; - const frcValue = frcIdInput.value.trim(); - const shouldRandomize = frcRandomCheckbox && frcRandomCheckbox.checked; - let frcID = null; - - if (isFRC && !shouldRandomize) { - if (frcValue === '') { - return { valid: false, message: 'Enter a number between 0 and 959 for the FRC ID.' }; - } - - if (!/^\d+$/.test(frcValue)) { - return { valid: false, message: 'Enter a whole number between 0 and 959 for the FRC ID.' }; - } - - frcID = parseInt(frcValue, 10); - - if (!isValidID(frcID)) { - return { valid: false, message: 'Enter a number between 0 and 959 for the FRC ID.' }; - } - - if (frcID === 518) { - return { valid: false, message: 'Standard Chess (ID 518) is not allowed in FRC mode.' }; - } - } - - const removedPieces = getRemovedPieces(); - const totalRemoved = Object.values(removedPieces).reduce((a, b) => a + b, 0); - if (totalRemoved === 0) { - return { valid: false, message: 'Select at least one piece to remove.' }; - } - - if (!isValidOdds(removedPieces, isFRC)) { - return { valid: false, message: buildInvalidSelectionGuidance(removedPieces) }; - } - - return { valid: true, message: '', frcID, isFRC }; - } - - function getRemovedPieces() { - const removedPieces = { Q: 0, N: 0, R: 0, B: 0 }; - pieceCheckboxIds.forEach(id => { - if (document.getElementById(id).checked) { - if (id.startsWith('knight')) removedPieces.N++; - if (id.startsWith('rook')) removedPieces.R++; - if (id.startsWith('bishop')) removedPieces.B++; - if (id === 'queen') removedPieces.Q++; - } - }); - return removedPieces; - } - - function buildInvalidSelectionGuidance(removedPieces) { - const { Q, N, R, B } = removedPieces; - if (Q === 0 && N === 1 && R === 0 && B === 1) { - return 'For Bishop+Knight odds, select one bishop and one knight from opposite sides of the board.'; - } - if (Q === 0 && N === 1 && R === 1 && B === 0) { - return 'For Rook+Knight odds, select queen-side rook and king-side knight.'; - } - if (Q === 1 && N === 0 && R === 1 && B === 0) { - return 'For Queen+Rook odds, select queen-side rook only.'; - } - if (Q === 0 && N === 0 && R === 1 && B === 1) { - return 'Rook+Bishop odds are not supported.'; - } - if (Q === 1 && N === 2 && R === 2 && B === 2) { - return 'At least give the bot a fighting chance!'; - } - return 'This combination of pieces is not supported.'; - } - - function setButtonDisabledState(isDisabled) { - [generateBtn, copyBtn].forEach(button => { - if (!button) return; - button.disabled = isDisabled; - button.classList.toggle('odds-btn-disabled', isDisabled); - }); - } - - function setConfigHint(isValid, message) { - setButtonDisabledState(!isValid); - if (!configHint) return; - if (isValid) { - configHint.classList.add('hidden'); - configHint.textContent = ''; - return; - } - - configHint.classList.remove('hidden'); - configHint.textContent = message; - } - - function updateActionState() { - const validation = validateConfiguration(); - const message = validation.message || defaultHint; - setConfigHint(validation.valid, message); - } - - function updateFrcInputState() { - if (!frcIdInput || !frcRandomCheckbox) return; - const shouldRandomize = frcRandomCheckbox.checked; - frcIdInput.readOnly = shouldRandomize; - } -}); + function validateConfiguration() { + const isFRC = frcToggle.checked; + const frcValue = frcIdInput.value.trim(); + const shouldRandomize = frcRandomCheckbox && frcRandomCheckbox.checked; + let frcID = null; + + if (isFRC && !shouldRandomize) { + if (frcValue === '') { + return { valid: false, message: 'Enter a number between 0 and 959 for the FRC ID.' }; + } + + if (!/^\d+$/.test(frcValue)) { + return { valid: false, message: 'Enter a whole number between 0 and 959 for the FRC ID.' }; + } + + frcID = parseInt(frcValue, 10); + + if (!isValidID(frcID)) { + return { valid: false, message: 'Enter a number between 0 and 959 for the FRC ID.' }; + } + + if (frcID === 518) { + return { valid: false, message: 'Standard Chess (ID 518) is not allowed in FRC mode.' }; + } + } + + const removedPieces = getRemovedPieces(); + const totalRemoved = Object.values(removedPieces).reduce((a, b) => a + b, 0); + if (totalRemoved === 0) { + return { valid: false, message: 'Select at least one piece to remove.' }; + } + + if (!isValidOdds(removedPieces, isFRC)) { + return { valid: false, message: buildInvalidSelectionGuidance(removedPieces) }; + } + + return { valid: true, message: '', frcID, isFRC }; + } + + function getRemovedPieces() { + const removedPieces = { Q: 0, N: 0, R: 0, B: 0 }; + pieceCheckboxIds.forEach(id => { + if (document.getElementById(id).checked) { + if (id.startsWith('knight')) removedPieces.N++; + if (id.startsWith('rook')) removedPieces.R++; + if (id.startsWith('bishop')) removedPieces.B++; + if (id === 'queen') removedPieces.Q++; + } + }); + return removedPieces; + } + + function buildInvalidSelectionGuidance(removedPieces) { + const { Q, N, R, B } = removedPieces; + if (Q === 0 && N === 1 && R === 0 && B === 1) { + return 'For Bishop+Knight odds, select one bishop and one knight from opposite sides of the board.'; + } + if (Q === 0 && N === 1 && R === 1 && B === 0) { + return 'For Rook+Knight odds, select queen-side rook and king-side knight.'; + } + if (Q === 1 && N === 0 && R === 1 && B === 0) { + return 'For Queen+Rook odds, select queen-side rook only.'; + } + if (Q === 0 && N === 0 && R === 1 && B === 1) { + return 'Rook+Bishop odds are not supported.'; + } + if (Q === 1 && N === 2 && R === 2 && B === 2) { + return 'At least give the bot a fighting chance!'; + } + return 'This combination of pieces is not supported.'; + } + + function setButtonDisabledState(isDisabled) { + if (!generateBtn) return; + generateBtn.disabled = isDisabled; + generateBtn.classList.toggle('odds-btn-disabled', isDisabled); + } + + function setConfigHint(isValid, message) { + setButtonDisabledState(!isValid); + if (!configHint) return; + if (isValid) { + configHint.classList.add('hidden'); + configHint.textContent = ''; + return; + } + + configHint.classList.remove('hidden'); + configHint.textContent = message; + } + + function updateActionState() { + const validation = validateConfiguration(); + const message = validation.message || defaultHint; + setConfigHint(validation.valid, message); + } + + function updateFrcInputState() { + if (!frcIdInput || !frcRandomCheckbox) return; + const shouldRandomize = frcRandomCheckbox.checked; + frcIdInput.readOnly = shouldRandomize; + } +});