Skip to content

Instantly share code, notes, and snippets.

@tomhermans
Created February 17, 2026 12:55
Show Gist options
  • Select an option

  • Save tomhermans/0d0880816f3fa352d83256ecabc95afb to your computer and use it in GitHub Desktop.

Select an option

Save tomhermans/0d0880816f3fa352d83256ecabc95afb to your computer and use it in GitHub Desktop.
Countdown Numbers Game
<section class="wrap">
<aside>
<h1 class="pagename">Countdown Numbers Game</h1>
<div class="controls">
<div class="input-section">
<div class="input-group">
<label for="largeCount">Large Numbers:</label>
<input type="number" id="largeCount" min="0" max="4" value="2" />
<span>(0-4 numbers from: 25, 50, 75, 100)</span>
</div>
<div class="input-group">
<label for="smallCount">Small Numbers:</label>
<input
type="number"
id="smallCount"
min="2"
max="6"
value="4"
readonly
/>
<span>(automatically calculated to total 6)</span>
</div>
<div id="status" class="status" style="display: none"></div>
</div>
<div class="buttons">
<button class="btn btn--large" id="drawNumbers" onclick="drawNumbers()">
Draw Numbers
</button>
<button class="btn btn--large" id="setTarget" onclick="setTarget()">Set Target</button>
<button id="resetGame" class="btn btn--large reset-btn" onclick="resetGame()">
Reset Game
</button>
</div>
</div>
<div class="arrays-section">
<div class="array-display">
<div class="array-title">Large Numbers Available</div>
<div id="largeArray" class="array-content">[25, 50, 75, 100]</div>
</div>
<div class="array-display">
<div class="array-title">Small Numbers Available</div>
<div id="smallArray" class="array-content">
[1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10]
</div>
</div>
</div>
</aside>
<main>
<div class="random-display">
<div id="randomTarget">???</div>
</div>
<div id="pickedSection" class="picked-numbers">
<h3>Picked Numbers for Game</h3>
<div id="pickedDisplay" class="picked-display">
<div class="pickedNum"></div>
<div class="pickedNum"></div>
<div class="pickedNum"></div>
<div class="pickedNum"></div>
<div class="pickedNum"></div>
<div class="pickedNum"></div>
</div>
</div>
<div class="solver">
<button class="btn" id="getanswer" disabled>could it be done?</button>
<div id="solver-result"></div>
</div>
</main>
</section>
// Initialize the number stacks
let NumbersLarge = [25, 50, 75, 100];
let NumbersSmall = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10];
let PickedNumbers = [];
let randomTargetNum;
// Get DOM elements
const largeCountInput = document.getElementById('largeCount');
const smallCountInput = document.getElementById('smallCount');
const statusDiv = document.getElementById('status');
const largeArrayDiv = document.getElementById('largeArray');
const smallArrayDiv = document.getElementById('smallArray');
const pickedSection = document.getElementById('pickedSection');
const randomTarget = document.getElementById('randomTarget');
const pickedDisplay = document.getElementById('pickedDisplay');
const getanswerBtn = document.getElementById('getanswer');
const solverDiv = document.getElementById('solver-result');
// Update display of arrays
function updateArrayDisplay() {
largeArrayDiv.textContent = '[' + NumbersLarge.join(', ') + ']';
smallArrayDiv.textContent = '[' + NumbersSmall.join(', ') + ']';
}
// Show status message
function showStatus(message, isError = false) {
statusDiv.textContent = message;
statusDiv.className = isError ? 'status error' : 'status';
statusDiv.style.display = 'block';
}
// Hide status message
function hideStatus() {
// statusDiv.style.display = 'none';
}
// Update small count automatically when large count changes
largeCountInput.addEventListener('input', function() {
resetGame();
const LNumChoice = parseInt(this.value) || 0;
const SNumChoice = 6 - LNumChoice;
// Validate inputs
if (LNumChoice < 0 || LNumChoice > 4) {
showStatus('Large numbers must be between 0 and 4', true);
return;
}
if (SNumChoice < 0 || SNumChoice > 6) {
showStatus('Invalid combination - total must equal 6', true);
return;
}
smallCountInput.value = SNumChoice;
hideStatus();
});
function canSolverWork() {
getanswerBtn.disabled = true;
console.log('pickednums=', PickedNumbers);
// check randomTarget.innerText
console.log('target=', randomTargetNum);
if (PickedNumbers.length != 0 && randomTargetNum != 0) {
// console.log("success");
getanswerBtn.disabled = false;
} else {
console.log("not both true");
}
}
// Random draw function
function drawNumbers() {
resetGame();
const LNumChoice = parseInt(largeCountInput.value) || 0;
const SNumChoice = parseInt(smallCountInput.value) || 0;
// Validation
if (LNumChoice + SNumChoice !== 6) {
showStatus('Total numbers must equal 6!', true);
return;
}
if (LNumChoice > NumbersLarge.length) {
showStatus(`Only ${NumbersLarge.length} large numbers available!`, true);
return;
}
if (SNumChoice > NumbersSmall.length) {
showStatus(`Only ${NumbersSmall.length} small numbers available!`, true);
return;
}
// Clear previous picks
PickedNumbers = [];
// Draw large numbers
for (let i = 0; i < LNumChoice; i++) {
const randomIndex = Math.floor(Math.random() * NumbersLarge.length);
const pickedNumber = NumbersLarge.splice(randomIndex, 1)[0];
PickedNumbers.push(pickedNumber);
}
// Draw small numbers
for (let i = 0; i < SNumChoice; i++) {
const randomIndex = Math.floor(Math.random() * NumbersSmall.length);
const pickedNumber = NumbersSmall.splice(randomIndex, 1)[0];
PickedNumbers.push(pickedNumber);
}
// Sort picked numbers for display (large numbers first, then small numbers sorted)
PickedNumbers.sort((a, b) => {
if (a >= 25 && b >= 25) return a - b; // Both large
if (a >= 25 && b < 25) return -1; // a is large, b is small
if (a < 25 && b >= 25) return 1; // a is small, b is large
return a - b; // Both small
});
// Update displays
updateArrayDisplay();
// pickedDisplay.textContent = PickedNumbers.join(' ');
// pickedSection.style.display = 'block';
// Clear any existing content
pickedDisplay.innerHTML = '<div class="pickedNum">?</div><div class="pickedNum">?</div><div class="pickedNum">?</div><div class="pickedNum">?</div><div class="pickedNum">?</div><div class="pickedNum">?</div>';
// Loop through PickedNumbers and create a div for each
pickedDisplay.innerHTML = '';
PickedNumbers.forEach(number => {
const numberDiv = document.createElement('div');
numberDiv.className = 'pickedNum';
numberDiv.textContent = number;
pickedDisplay.appendChild(numberDiv);
});
pickedSection.style.display = 'block';
canSolverWork();
// showStatus(`Successfully drew ${LNumChoice} large and ${SNumChoice} small numbers!`);
// Log to console for debugging
console.log('Picked Numbers:', {PickedNumbers});
// console.log('Remaining Large:', {NumbersLarge});
// console.log('Remaining Small:', {NumbersSmall});
}
// Reset game function
function resetGame() {
// delete solver-result div + disable solver button
solverDiv.innerText = '';
getanswerBtn.disabled = true;
randomTargetNum = 0;
pickedDisplay.innerHTML = '<div class="pickedNum">?</div><div class="pickedNum">?</div><div class="pickedNum">?</div><div class="pickedNum">?</div><div class="pickedNum">?</div><div class="pickedNum">?</div>';
randomTarget.innerText = '---';
// Reset arrays to original state
NumbersLarge = [25, 50, 75, 100];
NumbersSmall = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10];
PickedNumbers = [];
// Reset inputs
// largeCountInput.value = 2;
// smallCountInput.value = 4;
// Update displays
updateArrayDisplay();
// pickedSection.style.display = 'none';
hideStatus();
canSolverWork();
console.log('Game reset!');
}
// Generate a random target number between 100 and 999 (inclusive)
function generateRandomTarget() {
// Math.random() gives 0 to 0.999...
// We want 100 to 999, so range is 900 numbers (999 - 100 + 1 = 900)
const min = 100;
const max = 999;
const range = max - min + 1; // 900
randomTargetNum = Math.floor(Math.random() * range) + min;
return randomTargetNum;
}
function setTarget() {
// console.log('random number');
randomTarget.innerText = generateRandomTarget();
canSolverWork();
}
// Initialize display
updateArrayDisplay();
// SOLVER
// SOLVER
document.getElementById('getanswer').addEventListener('click', (e) => {
// console.log('get results from solver for target: ', randomTargetNum);
// console.log('these numbers: ', PickedNumbers);
// console.log('print results in div with id #solver-result');
const result = solve_numbers(PickedNumbers.slice(), randomTargetNum, false);
solverDiv.innerHTML = result;
});
/* Solver lib code */
/* Javascript version of cntdn
*
* Countdown game solver
*
* James Stanley 2014
* https://incoherency.co.uk/countdown/cntdn.js
*/
function _recurse_solve_letters(letters, node, used_letter, cb, answer) {
if (node[0])
cb(answer, node[0]);
if (answer.length == letters.length)
return;
var done = {};
for (var i = 0; i < letters.length; i++) {
var c = letters.charAt(i);
if (used_letter[i] || done[c])
continue;
if (node[c]) {
used_letter[i] = true;
done[c] = true;
_recurse_solve_letters(letters, node[c], used_letter, cb, answer+c);
used_letter[i] = false;
}
}
}
function solve_letters(letters, cb) {
_recurse_solve_letters(letters, dictionary, {}, cb, '');
}
function sufficient_letters(word, letters) {
var count = {};
for (var i = 0; i < letters.length; i++) {
if (!count[letters.charAt(i)])
count[letters.charAt(i)] = 0;
count[letters.charAt(i)]++;
}
for (var i = 0; i < word.length; i++) {
if (!count[word.charAt(i)])
return false;
count[word.charAt(i)]--;
if (count[word.charAt(i)] < 0)
return false;
}
return true;
}
function word_in_dictionary(word) {
var node = dictionary;
var idx = 0;
while (idx < word.length) {
node = node[word.charAt(idx)];
idx++;
if (!node)
return false;
}
if (!node[0])
return false;
return true;
}
var bestdiff;
var bestvalsums;
var allresults = [];
var bestresult;
var OPS = {
"+": function(n1, n2) { if (n1 < 0 || n2 < 0) return false; return n1+n2; },
"-": function(n1, n2) { if (n2 >= n1) return false; return n1-n2; },
"_": function(n2, n1) { if (n2 >= n1) return false; return n1-n2; },
"*": function(n1, n2) { return n1*n2; },
"/": function(n1, n2) { if (n2 == 0 || n1%n2 != 0) return false; return n1/n2; },
"?": function(n2, n1) { if (n2 == 0 || n1%n2 != 0) return false; return n1/n2; },
};
var OPCOST = {
"+": 1,
"-": 1.05,
"_": 1.05,
"*": 1.2,
"/": 1.3,
"?": 1.3,
};
function _recurse_solve_numbers(numbers, searchedi, was_generated, target, levels, valsums, trickshot) {
levels--;
for (var i = 0; i < numbers.length-1; i++) {
var ni = numbers[i];
if (ni === false)
continue;
numbers[i] = false;
for (var j = i+1; j < numbers.length; j++) {
var nj = numbers[j];
if (nj === false)
continue;
if (i < searchedi && !was_generated[i] && !was_generated[j])
continue;
for (var o in OPS) {
var r = OPS[o](ni[0], nj[0]);
if (r === false)
continue;
if (o == '/' && nj[0] == 1)
continue;
if (o == '?' && ni[0] == 1)
continue;
if (o == '*' && (ni[0] == 1 || nj[0] == 1))
continue;
if (r == ni[0] || r == nj[0])
continue;
var op_cost = Math.abs(r);
while (op_cost % 10 == 0 && op_cost != 0)
op_cost /= 10;
if ((ni[0] == 10 || nj[0] == 10) && o == '*') // HACK: multiplication by 10 is cheap
op_cost = 1;
op_cost *= OPCOST[o];
var newvalsums = valsums + op_cost;
if (allresults.length == 0 || Math.abs(r-target) < Math.abs(allresults[0].answer[0]-target))
allresults = [];
if (allresults.length == 0 || Math.abs(r-target) <= Math.abs(allresults[0].answer[0]-target))
allresults.push(JSON.parse(JSON.stringify({valsums: valsums, answer: [r,o,ni,nj]})));
if ((Math.abs(r - target) < Math.abs(bestresult[0] - target))
|| (Math.abs(r - target) == Math.abs(bestresult[0] - target) && (trickshot || newvalsums < bestvalsums))) {
bestresult = [r,o,ni,nj];
bestvalsums = newvalsums;
}
numbers[j] = [r, o, ni, nj];
var old_was_gen = was_generated[j];
was_generated[j] = true;
if (levels > 0)
_recurse_solve_numbers(numbers, i+1, was_generated, target, levels, newvalsums, trickshot);
was_generated[j] = old_was_gen;
numbers[j] = nj;
}
}
numbers[i] = ni;
}
}
function tidyup_result(result) {
var mapping = {
"?": "/", "_": "-"
};
var swappable = {
"*": true, "+": true
};
if (result.length < 4)
return result;
for (var i = 2; i < result.length; i++) {
var child = result[i];
child = tidyup_result(child);
if (child[1] == result[1] && swappable[result[1]]) {
result.splice(i--, 1);
result = result.concat(child.slice(2));
} else {
result[i] = child;
}
}
if (result[1] in mapping) {
result[1] = mapping[result[1]];
var j = result[2];
result[2] = result[3];
result[3] = j;
} else if (swappable[result[1]]) {
childs = result.slice(2).sort(function(a,b) { return b[0] - a[0]; });
for (var i = 2; i < result.length; i++)
result[i] = childs[i-2];
}
return result;
}
function fullsize(array) {
if (array.constructor != Array)
return 0;
var l = 0;
for (var i = 0; i < array.length; i++)
l += fullsize(array[i]);
return l + array.length;
}
function serialise_result(result) {
var childparts = [];
for (var i = 2; i < result.length; i++) {
var child = result[i];
if (child.length >= 4)
childparts.push(serialise_result(child));
}
childparts = childparts.sort(function(a,b) { return fullsize(b) - fullsize(a); });
var parts = [];
for (var i = 0; i < childparts.length; i++) {
parts = parts.concat(childparts[i]);
}
var sliced = result.slice(2).map(function(l) { return l[0]; });
var thispart = [result[0], result[1]].concat(sliced);
return parts.concat([thispart]);
}
function stringify_result(serialised, target) {
var output = '';
serialised = serialised.slice(0);
for (var i = 0; i < serialised.length; i++) {
var x = serialised[i];
var args = x.slice(2);
output += args.join(' ' + x[1] + ' ') + ' = ' + x[0] + '\n';
}
var result = serialised[serialised.length-1][0];
if (result != target)
output += '(off by ' + (Math.abs(result - target)) + ')\n';
return output;
}
function _solve_numbers(numbers, target, trickshot) {
numbers = numbers.map(function(x) { return [x, false] });
var was_generated = [];
for (var i = 0; i < numbers.length; i++)
was_generated.push(false);
bestresult = [0, 0];
/* attempt to solve with dfs */
_recurse_solve_numbers(numbers, 0, was_generated, target, numbers.length, 0, trickshot);
return bestresult;
}
function solve_numbers(numbers, target, trickshot) {
numbers.sort();
bestresult = [numbers[0], numbers[0]];
/* see if one of these numbers is the answer; with trickshot you'd rather
* have an interesting answer that's close than an exact answer
*/
if (!trickshot) {
for (var i = 1; i < numbers.length; i++) {
if (Math.abs(numbers[i] - target) < Math.abs(bestresult[0] - target)) {
bestresult = [numbers[i], numbers[i]];
bestvalsums = numbers[i];
}
}
if (bestresult[0] == target)
return target + " = " + target;
}
//return stringify_result(serialise_result(tidyup_result(_solve_numbers(numbers, target, trickshot))), target);
allresults = [];
_solve_numbers(numbers, target, trickshot);
allresults.sort(function(a,b) {
return a.valsums - b.valsums;
});
var s = '';
var got = {};
for (var i = 0; i < allresults.length; i++) {
var this_str = stringify_result(serialise_result(tidyup_result(allresults[i].answer)), target);
if (!got[this_str]) {
got[this_str] = true;
const lines = this_str.trim().split('\n').filter(l => l.trim() !== '');
s += '<div class="solution">' + lines.map(l => '<span>' + l + '</span>').join('') + '</div>';
}
}
return s;
}
:root {
--surface1: #38669b;
--surface2: #1d4789;
--surface3: #2328c0;
--font-size-4: clamp(1.131rem, 1.131rem + 0.14vw, 1.414rem);
--font-size-5: clamp(1.600rem, 1.600rem + 0.20vw, 1.999rem);
--font-size-6: clamp(2.262rem, 2.262rem + 0.28vw, 2.827rem);
--font-size-7: clamp(3.198rem, 3.198rem + 0.40vw, 3.998rem);
--font-size-8: clamp(4.522rem, 4.522rem + 0.57vw, 5.653rem);
--font-size-9: clamp(6.394rem, 6.394rem + 0.80vw, 7.993rem);
}
body {
font-family: Arial, sans-serif;
// max-width: 800px;
margin: 0 auto;
// padding: 20px;
background-color: #f5f5f5;
}
.wrap {
display: flex;
}
aside {
width: 30%;
}
main {
// width: 70%;
padding: 3vw;
background: white;
}
// .container {
// background: white;
// padding: 30px;
// border-radius: 10px;
// box-shadow: 0 2px 10px rgba(0,0,0,0.1);
// }
.pagename {
color: #2c3e50;
text-align: center;
margin: 1rem 0;
}
.input-section {
background: #ecf0f1;
padding: 1vw 3vw;
border-radius: 8px;
// margin-bottom: 20px;
display: flex;
gap: 1rem;
}
.controls {
display: flex;
flex-direction: column;
> * {
flex-grow: 1;
justify-content: space-around;
}
}
.buttons,
.input-group {
display: flex;
flex-direction: column;
// align-items: left;
// margin-bottom: 15px;
gap: 10px;
span {
display: none;
font-size: 90%;
}
}
.buttons {
padding: 1rem;
}
label {
font-weight: bold;
min-width: 140px;
}
input[type="number"] {
padding: 8px;
border: 2px solid #bdc3c7;
border-radius: 8px;
// width: 80px;
font-size: 2rem;
font-weight: bold;
text-align: center;
}
input[type="number"]:focus {
border-color: #3498db;
outline: none;
}
.btn {
background: #3498db;
color: white;
border: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 1rem;
line-height: 1.5;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
}
.btn--large {
padding: 12px 24px;
font-size: 1.3rem;
}
button:hover {
background: #2980b9;
}
button:disabled {
background: #95a5a6;
cursor: not-allowed;
}
.status {
background: #e8f5e8;
border: 1px solid #27ae60;
color: #27ae60;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
}
.error {
background: #ffeaea;
border: 1px solid #e74c3c;
color: #e74c3c;
}
.arrays-section {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
margin-bottom: 20px;
}
.array-display {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
border: 1px solid #dee2e6;
}
.array-title {
font-weight: bold;
margin-bottom: 10px;
color: #495057;
}
.array-content {
font-family: 'Courier New', monospace;
background: white;
padding: 10px;
border-radius: 4px;
border: 1px solid #ced4da;
min-height: 40px;
}
.picked-numbers,
.solver {
// background: #fff3cd;
border: 2px solid #ccc;
padding: 10px;
border-radius: 8px;
margin-top: 20px;
text-align: center;
}
.picked-numbers h3 {
color: #856404;
margin-top: 0;
}
.random-display,
.picked-display {
font-family: sans-serif; // 'Courier New', monospace;
font-size: var(--font-size-6, 8vw);
font-weight: bold;
background: white;
// padding: 15px;
border-radius: 6px;
text-align: center;
// border: 1px solid #ffc107;
// justify-content: space-around;
gap: 10px;
}
.picked-display {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.pickedNum {
background: var(--surface2);
color: white;
// width: 120px;
// aspect-ratio: 2 / 3;
display: grid;
place-content: center;
padding: 2vh 1vw;
letter-spacing: -.2rem;
// min-width: 40px;
// min-height: 80px;
}
.random-display {
background: var(--surface3);
color: white;
letter-spacing: .5rem;
font-size: var(--font-size-8, 14vw);
text-align: center;
}
.reset-btn {
background: #6c757d;
// margin-left: 10px;
}
.reset-btn:hover {
background: #5a6268;
}
// adjustments
.wrap {
display: flex;
flex-direction: column; /* Mobile first: stack vertically */
}
aside {
width: 100%; /* Full width on mobile */
order: 1;
}
main {
// width: calc(100% - 4rem); /* Full width on mobile */
}
.pagename {
display: none;
}
@media (min-width: 508px) {
.pagename {
display: block;
}
.controls {
flex-direction: row;
}
}
/* Desktop styles */
@media (min-width: 768px) {
.wrap {
flex-direction: row; /* Side by side on desktop */
}
aside {
width: 200px; /* Fixed minimum width */
min-width: 200px; /* Ensure it doesn't shrink below 200px */
flex-shrink: 0; /* Prevent shrinking */
.controls,
.input-section {
flex-direction: column;
}
}
.picked-display {
grid-template-columns: repeat(6, 1fr);
}
.pickedNum {
aspect-ratio: 2 / 3;
padding: 0 1vw;
}
main {
flex: 1; /* Takes up remaining space */
// width: auto; /* Let flex handle the width */
min-height: 100vh;
order: 1;
}
}
#solver-result {
font-family: monospace;
outline: 1px dashed #999;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))
;
.solution {
padding: 1rem;
span {
display: block;
}
}
}
#solver-result:not:empty {
padding: 1rem 1rem 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment