digitle is a number puzzle game, best known from the UK game show Countdown.
442 |
Try to reach the target number by combining the six given numbers and the four basic arithmetic operations. Click on available numbers to insert them into an expression, or type them on your keyboard.
443 |
444 |
445 |
446 | 1
447 | +
448 | 2
449 | ×
450 | 3
451 |
452 |
453 | =
454 | 7
455 |
456 |
457 |
Order of operations matters. You can now use this 7 again later.
458 |
459 |
460 | 7
461 | -
462 | 9
463 | +
464 | 10
465 |
466 |
467 | =
468 | 8
469 |
470 |
471 |
Negative numbers aren't allowed. A hidden −2 is created here.
472 |
473 |
474 | 7
475 | +
476 | 10
477 | -
478 | 9
479 |
480 |
481 | =
482 | 8
483 |
484 |
485 |
Rearranging can help.
486 |
487 |
488 | 8
489 | ÷
490 | 3
491 |
492 |
493 | =
494 | 2⅔
495 |
496 |
497 |
Fractions aren't allowed, either.
498 |
499 |
You earn ⭐ for getting within 10 of the target, ⭐⭐ for getting within 5, and ⭐⭐⭐ for reaching it exactly. Note that in rare cases, a puzzle may not be exactly solvable.
500 |
Made by Eevee (@eevee). Hope you enjoy! Source code on GitHub, but also in your browser; it's only two files.
501 |
502 |
503 |
504 |
505 |
options
506 |
507 |
508 |
509 |
510 |
549 |
550 |
554 |
555 |
556 |
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | // thanks, https://stackoverflow.com/a/47593316/17875
2 | // string hash function
3 | function xmur3(str) {
4 | for(var i = 0, h = 1779033703 ^ str.length; i < str.length; i++) {
5 | h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
6 | h = h << 13 | h >>> 19;
7 | } return function() {
8 | h = Math.imul(h ^ (h >>> 16), 2246822507);
9 | h = Math.imul(h ^ (h >>> 13), 3266489909);
10 | return (h ^= h >>> 16) >>> 0;
11 | }
12 | }
13 | // simple PRNG, seeded by an integer
14 | function mulberry32(a) {
15 | return function() {
16 | let t = a += 0x6D2B79F5;
17 | t = Math.imul(t ^ t >>> 15, t | 1);
18 | t ^= t + Math.imul(t ^ t >>> 7, t | 61);
19 | return ((t ^ t >>> 14) >>> 0) / 4294967296;
20 | }
21 | }
22 |
23 | export class RNG {
24 | constructor(seed) {
25 | if (seed === undefined) {
26 | seed = Date.now();
27 | }
28 | this.seed = seed;
29 |
30 | if (typeof seed !== 'number') {
31 | seed = xmur3(String(seed))();
32 | }
33 | this.faucet = mulberry32(seed);
34 | }
35 |
36 | // This is the Python random module interface, which is the pinnacle of RNG interfaces
37 | random() {
38 | return this.faucet();
39 | }
40 |
41 | randrange(a, b) {
42 | if (b === undefined) {
43 | b = a;
44 | a = 0;
45 | }
46 | return a + Math.floor((b - a) * this.random());
47 | }
48 |
49 | choice(seq) {
50 | return seq[Math.floor(this.random() * seq.length)];
51 | }
52 |
53 | sample(seq, k) {
54 | let pool = Array.from(seq);
55 | let n = pool.length;
56 | let ret = [];
57 | for (let i = 0; i < k; i++) {
58 | let j = this.randrange(n - i);
59 | ret.push(pool[j]);
60 | pool[j] = pool[n - i - 1];
61 | }
62 | return ret;
63 | }
64 | }
65 |
66 |
67 | function _mk(el, children) {
68 | if (children.length > 0) {
69 | if (!(children[0] instanceof Node) && children[0] !== undefined && typeof(children[0]) !== "string" && typeof(children[0]) !== "number") {
70 | let [attrs] = children.splice(0, 1);
71 | for (let [key, value] of Object.entries(attrs)) {
72 | el.setAttribute(key, value);
73 | }
74 | }
75 | el.append(...children);
76 | }
77 | return el;
78 | }
79 |
80 | export function mk(tag_selector, ...children) {
81 | let [tag, ...classes] = tag_selector.split('.');
82 | let el = document.createElement(tag);
83 | if (classes.length > 0) {
84 | el.classList = classes.join(' ');
85 | }
86 | return _mk(el, children);
87 | }
88 |
89 |
90 | const BIG_NUMBERS = [25, 50, 75, 100];
91 | const SMALL_NUMBERS = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10];
92 | export class Game {
93 | constructor(rng) {
94 | this.rng = rng ?? new RNG;
95 | this.target = this.rng.randrange(100, 1000);
96 | // How many bigguns?
97 | let big;
98 | let n = this.rng.random();
99 | if (n < 0.5) {
100 | big = 1;
101 | }
102 | else if (n < 0.8) {
103 | big = 2;
104 | }
105 | else if (n < 0.9) {
106 | big = 3;
107 | }
108 | else if (n < 0.95) {
109 | big = 4;
110 | }
111 | else {
112 | big = 0;
113 | }
114 |
115 | this.numbers = [];
116 | this.numbers.push(...this.rng.sample(BIG_NUMBERS, big));
117 | this.numbers.push(...this.rng.sample(SMALL_NUMBERS, 6 - big));
118 |
119 | this.used = [];
120 | this.reset();
121 | }
122 |
123 | reset() {
124 | this.numbers.splice(6);
125 | this.used = [false, false, false, false, false, false];
126 | this.win = null;
127 | }
128 |
129 | check_for_win() {
130 | let closest = null;
131 | let closest_off = null;
132 | for (let [i, n] of this.numbers.entries()) {
133 | if (this.used[i])
134 | continue;
135 |
136 | let off = Math.abs(n - this.target);
137 | if (closest === null || off < closest_off) {
138 | closest = i;
139 | closest_off = off;
140 | }
141 | }
142 |
143 | if (closest_off > 10) {
144 | this.win = null;
145 | return;
146 | }
147 |
148 | this.win = {
149 | closest: this.numbers[closest],
150 | index: closest,
151 | off: closest_off,
152 | stars: 1,
153 | };
154 | if (closest_off === 0) {
155 | this.win.stars = 3;
156 | }
157 | else if (closest_off <= 5) {
158 | this.win.stars = 2;
159 | }
160 |
161 | return this.win;
162 | }
163 | }
164 |
165 |
166 | const OPERATORS = {
167 | '+': {
168 | text: '+',
169 | emoji: '➕',
170 | evaluate: (a, b) => a + b,
171 | },
172 | '-': {
173 | text: '−',
174 | emoji: '➖',
175 | evaluate: (a, b) => a - b,
176 | },
177 | '*': {
178 | text: '×',
179 | emoji: '✖️',
180 | evaluate: (a, b) => a * b,
181 | },
182 | '/': {
183 | text: '÷',
184 | emoji: '➗',
185 | evaluate: (a, b) => a / b,
186 | },
187 | };
188 | class IntegerError extends RangeError {}
189 | class Expression {
190 | constructor(game, parent_el) {
191 | this.game = game;
192 | this.expn_el = mk('div.-expn');
193 | this.result_el = mk('div.-eq');
194 | parent_el.append(this.expn_el, this.result_el);
195 |
196 | this.parts = []; // alternates between numbers and ops
197 | this.add_number();
198 |
199 | this.error = null;
200 | }
201 |
202 | add_number() {
203 | let element = mk('span.num.-pending');
204 | this.expn_el.append(element);
205 | this.pending_part = {
206 | value: 0,
207 | element,
208 | };
209 | this.parts.push(this.pending_part);
210 | }
211 |
212 | set_pending_number(n) {
213 | if (! this.pending_part)
214 | return;
215 |
216 | this.pending_part.value = n;
217 | this.pending_part.element.textContent = n ? String(n) : NBSP;
218 | }
219 |
220 | evaluate() {
221 | let parts = this.parts.map(part => part.value);
222 |
223 | // Perform multiplication and division
224 | let i = 1;
225 | while (i < parts.length) {
226 | if (parts[i] === '*') {
227 | parts.splice(i - 1, 3, parts[i - 1] * parts[i + 1]);
228 | }
229 | else if (parts[i] === '/') {
230 | let result = parts[i - 1] / parts[i + 1];
231 | if (result !== Math.floor(result)) {
232 | throw new IntegerError(`Fractions are not allowed`);
233 | }
234 | parts.splice(i - 1, 3, result);
235 | }
236 | else {
237 | i += 2;
238 | }
239 | }
240 | // Perform addition and subtraction
241 | i = 1;
242 | while (i < parts.length) {
243 | if (parts[i] === '+') {
244 | parts.splice(i - 1, 3, parts[i - 1] + parts[i + 1]);
245 | }
246 | else if (parts[i] === '-') {
247 | let result = parts[i - 1] - parts[i + 1];
248 | if (result < 0) {
249 | throw new IntegerError(`Negative values are not allowed`);
250 | }
251 | parts.splice(i - 1, 3, result);
252 | }
253 | else {
254 | i += 2;
255 | }
256 | }
257 |
258 | if (parts.length !== 1) {
259 | console.error("Didn't end up with 1 part left", parts);
260 | }
261 |
262 | return parts[0];
263 | }
264 |
265 | commit_number(index = null) {
266 | this.uncommit_number();
267 |
268 | let {value, element} = this.pending_part;
269 | element.classList.remove('-pending');
270 |
271 | // Double-check that the index matches our number
272 | if (index !== null && this.game.numbers[index] !== value) {
273 | index = null;
274 | }
275 |
276 | // Find the first matching number
277 | if (index === null) {
278 | for (let [j, n] of this.game.numbers.entries()) {
279 | if (n === value && ! this.game.used[j] &&
280 | ! this.parts.some(part => part.index === j))
281 | {
282 | index = j;
283 | break;
284 | }
285 | }
286 | }
287 |
288 | if (index !== null) {
289 | if (index >= 6) {
290 | element.classList.add('intermed');
291 | }
292 | this.pending_part.index = index;
293 | return true;
294 | }
295 | else {
296 | element.classList.add('-error');
297 | this.error = `No ${value} is available`;
298 | return false;
299 | }
300 | }
301 |
302 | uncommit_number() {
303 | this.pending_part.index = null;
304 | let element = this.pending_part.element;
305 | element.classList.remove('-error', 'intermed', 'used');
306 | element.classList.add('-pending');
307 | this.error = null;
308 | }
309 |
310 | is_empty() {
311 | return this.parts.length === 1 && this.pending_part.value === 0;
312 | }
313 |
314 | // User input API
315 |
316 | append_digit(digit) {
317 | this.set_pending_number(this.pending_part.value * 10 + digit);
318 | this.uncommit_number();
319 | }
320 |
321 | set_number_by_index(index) {
322 | this.set_pending_number(this.game.numbers[index]);
323 | this.commit_number(index);
324 | }
325 |
326 | add_operator(op) {
327 | let opdef = OPERATORS[op];
328 | if (! opdef)
329 | return;
330 |
331 | if (this.pending_part.value === 0) {
332 | // No number; try to prefill the result of the last expression
333 | if (this.game.numbers.length > 6) {
334 | this.set_pending_number(this.game.numbers[this.game.numbers.length - 1]);
335 | }
336 | else {
337 | return;
338 | }
339 | }
340 |
341 | if (! this.commit_number())
342 | return;
343 |
344 | let element = mk('span.op', opdef.text);
345 | this.parts.push({ value: op, element });
346 | this.expn_el.append(element);
347 |
348 | this.add_number();
349 | return true;
350 | }
351 |
352 | backspace() {
353 | if (this.pending_part.value === 0) {
354 | // Erase the previous operator, if any (if none, do nothing)
355 | if (this.parts.length > 1) {
356 | this.parts.pop().element.remove(); // number
357 | this.parts.pop().element.remove(); // operator
358 | this.pending_part = this.parts[this.parts.length - 1];
359 | // Don't uncommit the number yet, for consistency with click entry
360 | }
361 | }
362 | else {
363 | // Delete the last digit of the current number
364 | this.set_pending_number(Math.floor(this.pending_part.value / 10));
365 | this.uncommit_number();
366 | }
367 | }
368 |
369 | commit() {
370 | if (this.pending_part.value === 0) {
371 | // Can't be done with no number
372 | return;
373 | }
374 | if (this.parts.length < 3) {
375 | // Need to have an expression to evaluate
376 | return;
377 | }
378 |
379 | if (! this.commit_number())
380 | return;
381 |
382 | let result;
383 | try {
384 | result = this.evaluate();
385 | }
386 | catch (e) {
387 | if (e instanceof IntegerError) {
388 | this.uncommit_number();
389 | this.error = e.message;
390 | return;
391 | }
392 | throw e;
393 | }
394 |
395 | let element = mk('span.num.intermed', result);
396 | this.result_el.append(
397 | mk('span.op', "="),
398 | element,
399 | );
400 |
401 | let used = [];
402 | for (let [i, part] of this.parts.entries()) {
403 | if (i % 2 === 0) {
404 | used.push(part.index);
405 | }
406 | }
407 |
408 | return {
409 | value: result,
410 | used,
411 | element,
412 | };
413 | }
414 |
415 | uncommit() {
416 | this.result_el.textContent = '';
417 | }
418 | }
419 |
420 | const NBSP = "\xa0";
421 | const SEED_POOL = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
422 | const SEED_LENGTH = 8;
423 |
424 | export class UI {
425 | constructor(root) {
426 | this.root = root;
427 |
428 | this.target_el = root.querySelector('#board .num.target');
429 | this.givens_el = root.querySelector('#given');
430 | this.expns_el = root.querySelector('#inputs');
431 | this.error_el = root.querySelector('#error');
432 |
433 | this.number_els = [];
434 | for (let i = 0; i < 6; i++) {
435 | let el = mk('li.num', {'data-index': i}, "?");
436 | this.number_els.push(el);
437 | this.givens_el.append(el);
438 | }
439 |
440 | this.expressions = [];
441 | this.current_expn = null;
442 |
443 | // Handle clicks on available numbers
444 | this.root.addEventListener('click', ev => {
445 | let num = ev.target.closest('.num[data-index]');
446 | if (! num)
447 | return;
448 |
449 | this.input_number(parseInt(num.getAttribute('data-index')));
450 | });
451 |
452 | this.options = {
453 | hard_mode: true,
454 | pool: 'normal',
455 | selection: 'auto',
456 | base: 10,
457 | };
458 |
459 | // Handle keypresses
460 | // TODO make it clear when we have focus?
461 | document.body.addEventListener('keydown', ev => {
462 | if ('0123456789'.indexOf(ev.key) >= 0) {
463 | this.input_digit(parseInt(ev.key, 10));
464 | }
465 | else if (ev.key === '+' || ev.key === '-' || ev.key === '*' || ev.key === '/') {
466 | // TODO would be nice to be able to do this and automatically bring down the result from the previous line
467 | this.input_operator(ev.key);
468 | }
469 | else if (ev.key === ':') {
470 | // Firefox nicety
471 | this.input_operator('/');
472 | }
473 | else if (ev.key === '=' || ev.key === 'Enter' || ev.key === 'Return') {
474 | this.input_done();
475 | }
476 | else if (ev.key === 'Backspace') {
477 | this.input_backspace();
478 | }
479 | else {
480 | return;
481 | }
482 |
483 | ev.preventDefault();
484 | });
485 |
486 | // Wire up the keyboard
487 | this.root.querySelector('#keyboard').addEventListener('click', ev => {
488 | let button = ev.target;
489 | if (button.tagName !== 'BUTTON')
490 | return;
491 |
492 | let type = button.getAttribute('data-type');
493 | if (type === 'digit') {
494 | this.input_digit(parseInt(button.getAttribute('data-digit'), 10));
495 | }
496 | else if (type === 'operator') {
497 | this.input_operator(button.getAttribute('data-op'));
498 | }
499 | else if (type === 'erase') {
500 | this.input_backspace();
501 | }
502 | else if (type === 'done') {
503 | this.input_done();
504 | }
505 | });
506 |
507 | this.root.querySelector('#button-reroll').addEventListener('click', () => {
508 | this.reroll();
509 | });
510 | this.root.querySelector('#button-reset').addEventListener('click', () => {
511 | this.reset();
512 | });
513 | this.root.querySelector('#button-copy-link').addEventListener('click', ev => {
514 | this.copy_link().then(() => {
515 | this.confirm_copy(ev);
516 | });
517 | });
518 |
519 | this.mode_button = this.root.querySelector('#button-mode');
520 | this.mode_button.addEventListener('click', () => {
521 | if (this.daily_mode) {
522 | this.switch_to_random_mode();
523 | }
524 | else {
525 | this.switch_to_daily_mode();
526 | }
527 | });
528 |
529 | this.root.querySelector('#button-copy-results').addEventListener('click', ev => {
530 | this.copy_results().then(() => {
531 | this.confirm_copy(ev);
532 | });
533 | });
534 |
535 | // Set up tabs (which are outside the root oops)
536 | this.root.querySelector('#button-show-about').addEventListener('click', () => {
537 | this.switch_to_tab('main-about');
538 | });
539 | document.querySelector('#button-close-about').addEventListener('click', () => {
540 | this.switch_to_tab('main-game');
541 | });
542 | this.root.querySelector('#button-settings').addEventListener('click', () => {
543 | this.switch_to_tab('main-settings');
544 | });
545 | document.querySelector('#button-close-settings').addEventListener('click', () => {
546 | this.switch_to_tab('main-game');
547 | });
548 |
549 | // Figure out what game we're playing. If we have a seed in the URL, read that.
550 | let seed;
551 | if (location.hash) {
552 | let params = new URLSearchParams(location.hash.substring(1));
553 | seed = params.get('seed');
554 | if (seed) {
555 | let m = seed.match(/^(\d{4}-\d{2}-\d{2})(?:[/](\d+))?$/);
556 | if (m) {
557 | this.daily_mode = true;
558 | // FIXME come on
559 | this.mode_button.textContent = "📆";
560 | this.mode_button.setAttribute('title', "Daily");
561 | this.daily_date = m[1];
562 | this.daily_number = m[2] ? parseInt(m[2], 10) : 1;
563 | console.log(seed, m, this.daily_date, this.daily_number);
564 | }
565 | else {
566 | this.daily_mode = false;
567 | this.mode_button.textContent = "🎲";
568 | this.mode_button.setAttribute('title', "Random");
569 | }
570 | this.set_game(new Game(new RNG(seed)));
571 | }
572 | history.replaceState({}, document.title, location.origin + location.pathname);
573 | }
574 |
575 | // Otherwise, default to today's game
576 | if (! seed) {
577 | this.switch_to_daily_mode();
578 | }
579 | }
580 |
581 | switch_to_tab(id) {
582 | for (let el of document.querySelectorAll('main')) {
583 | el.setAttribute('hidden', '');
584 | }
585 | document.querySelector('#' + id).removeAttribute('hidden');
586 | }
587 |
588 | get_datestamp() {
589 | return new Date().toISOString().substring(0, 10); // yyyy-mm-dd
590 | }
591 |
592 | switch_to_daily_mode() {
593 | this.daily_mode = true;
594 | this.mode_button.textContent = "📆";
595 | this.mode_button.setAttribute('title', "Daily");
596 |
597 | this.daily_date = this.get_datestamp();
598 | // FIXME switching back should remember what number you're on
599 | this.daily_number = 1;
600 | this.generate_game();
601 | }
602 |
603 | switch_to_random_mode() {
604 | this.daily_mode = false;
605 | this.mode_button.textContent = "🎲";
606 | this.mode_button.setAttribute('title', "Random");
607 |
608 | this.generate_game();
609 | }
610 |
611 | reroll() {
612 | if (this.daily_mode) {
613 | let stamp = this.get_datestamp();
614 | if (stamp === this.daily_date) {
615 | this.daily_number += 1;
616 | }
617 | else {
618 | this.daily_date = stamp;
619 | this.daily_number = 1;
620 | }
621 | }
622 |
623 | this.generate_game();
624 | }
625 |
626 | generate_game() {
627 | let seed;
628 | if (this.daily_mode) {
629 | seed = this.daily_date;
630 | if (this.daily_number > 1) {
631 | seed += `/${this.daily_number}`;
632 | }
633 | }
634 | else {
635 | // Generate a random seed from [a-zA-Z0-9], eight chars long
636 | let chars = [];
637 | for (let i = 0; i < SEED_LENGTH; i++) {
638 | chars.push(SEED_POOL[Math.floor(Math.random() * SEED_POOL.length)]);
639 | }
640 | seed = chars.join('');
641 | }
642 |
643 | this.set_game(new Game(new RNG(seed)));
644 | }
645 |
646 | set_game(game) {
647 | this.game = game;
648 | this.target_el.textContent = this.game.target;
649 | for (let [i, n] of this.game.numbers.entries()) {
650 | this.number_els[i].textContent = n;
651 | }
652 |
653 | let p = this.root.querySelector('#puzzle-desc p');
654 | if (this.daily_mode) {
655 | let bits = ["Daily puzzle"];
656 | if (this.daily_number > 1) {
657 | bits.push(" #", this.daily_number);
658 | }
659 | bits.push(" for ", this.daily_date);
660 | p.textContent = bits.join("");
661 | }
662 | else {
663 | p.textContent = `Random puzzle ${this.game.rng.seed}`;
664 | }
665 |
666 | this.reset();
667 | }
668 |
669 | reset() {
670 | this.game.reset();
671 |
672 | this.root.classList.remove('won');
673 | this.error_el.textContent = '';
674 | this.expns_el.textContent = '';
675 | this.expressions = [];
676 | this.add_new_expression();
677 | this.number_els.splice(6);
678 | for (let el of this.givens_el.querySelectorAll('.num.used')) {
679 | el.classList.remove('used');
680 | }
681 | this.update_ui();
682 | }
683 |
684 | add_new_expression() {
685 | this.current_expn = new Expression(this.game, this.expns_el);
686 | this.expressions.push(this.current_expn);
687 | }
688 |
689 | update_ui() {
690 | // Scroll the expression list to the bottom
691 | this.expns_el.parentNode.scrollTo(0, this.expns_el.parentNode.scrollHeight);
692 |
693 | // Update the error + win elements
694 | if (this.current_expn && this.current_expn.error) {
695 | this.error_el.textContent = this.current_expn.error;
696 | this.root.classList.remove('won');
697 | }
698 | else {
699 | // If all is well, check for a win
700 | let win = this.game.check_for_win();
701 | this.root.classList.toggle('won', !!win);
702 | if (win) {
703 | let element = this.root.querySelector('#win-message');
704 | if (win.stars === 3) {
705 | element.textContent = `Bang on! ⭐⭐⭐`;
706 | }
707 | else {
708 | element.textContent = `${win.off} away! ` + "⭐".repeat(win.stars);
709 | }
710 | }
711 | }
712 | }
713 |
714 | input_digit(digit) {
715 | if (! this.current_expn)
716 | return;
717 | this.current_expn.append_digit(digit);
718 | this.update_ui();
719 | }
720 |
721 | input_number(index) {
722 | if (! this.current_expn)
723 | return;
724 | this.current_expn.set_number_by_index(index);
725 | this.update_ui();
726 | }
727 |
728 | input_operator(op) {
729 | if (! this.current_expn)
730 | return;
731 | this.current_expn.add_operator(op);
732 | this.update_ui();
733 | }
734 |
735 | input_backspace() {
736 | if (! this.current_expn || this.current_expn.is_empty()) {
737 | if (this.expressions.length > 1 ||
738 | (! this.current_expn && this.expressions.length >= 1))
739 | {
740 | // Delete the current one's DOM
741 | if (this.current_expn) {
742 | this.current_expn.expn_el.remove();
743 | this.current_expn.result_el.remove();
744 | this.expressions.pop();
745 | }
746 |
747 | // Scrap it and make the previous one 'current'
748 | this.current_expn = this.expressions[this.expressions.length - 1];
749 |
750 | // Uncommit the previous one
751 | this.current_expn.uncommit();
752 | // Mark its numbers as no longer used
753 | for (let [i, part] of this.current_expn.parts.entries()) {
754 | if (i % 2 === 0) {
755 | this.game.used[part.index] = false;
756 | this.number_els[part.index].classList.remove('used');
757 | }
758 | }
759 | // Remove everything
760 | this.game.numbers.pop();
761 | this.game.used.pop();
762 | this.number_els.pop();
763 | }
764 | }
765 | else {
766 | this.current_expn.backspace();
767 | }
768 |
769 | this.update_ui();
770 | }
771 |
772 | input_done() {
773 | if (! this.current_expn)
774 | return;
775 |
776 | let result = this.current_expn.commit();
777 | if (result) {
778 | this.game.numbers.push(result.value);
779 | this.game.used.push(false);
780 |
781 | this.number_els.push(result.element);
782 | result.element.setAttribute('data-index', this.game.numbers.length - 1);
783 |
784 | let off = Math.abs(result.value - this.game.target);
785 | if (off === 0) {
786 | result.element.classList.add('win3');
787 | }
788 | else if (off <= 5) {
789 | result.element.classList.add('win2');
790 | }
791 | else if (off <= 10) {
792 | result.element.classList.add('win1');
793 | }
794 |
795 | for (let index of result.used) {
796 | this.game.used[index] = true;
797 | this.number_els[index].classList.add('used');
798 | }
799 |
800 | // Add a new expression only if at least 2 unused numbers remain
801 | if (this.game.used.filter(x => ! x).length >= 2) {
802 | this.add_new_expression();
803 | }
804 | else {
805 | this.current_expn = null;
806 | }
807 | }
808 | this.update_ui();
809 | }
810 |
811 | copy_link() {
812 | // Rebuild this without any query or fragment
813 | let base_url = location.origin + location.pathname;
814 |
815 | // Construct the fragment
816 | let params = new URLSearchParams([['seed', this.game.rng.seed]]);
817 |
818 | return navigator.clipboard.writeText(base_url + '#' + params.toString());
819 | }
820 |
821 | copy_results() {
822 | if (! this.game.win)
823 | return;
824 |
825 | let text = [];
826 |
827 | if (this.daily_mode) {
828 | text.push(`daily digitle ${this.daily_date}`);
829 | if (this.daily_number > 1) {
830 | text.push(' #', this.daily_number);
831 | }
832 | }
833 | else {
834 | text.push(`random digitle `);
835 | text.push(this.game.rng.seed);
836 | }
837 | text.push("\n");
838 |
839 | for (let expn of this.expressions) {
840 | if (expn === this.current_expn)
841 | // Still pending, ignore it
842 | continue;
843 |
844 | for (let [i, part] of expn.parts.entries()) {
845 | if (i % 2 === 0) {
846 | if (part.index < 6) {
847 | text.push("🟦");
848 | }
849 | else {
850 | text.push("🟨");
851 | }
852 | }
853 | else {
854 | text.push(OPERATORS[part.value].emoji);
855 | }
856 | }
857 | text.push("\n");
858 | }
859 |
860 | // TODO indicate all numbers used? hm
861 | text.push("⭐".repeat(this.game.win.stars));
862 | text.push(" ");
863 | if (this.game.win.off === 0) {
864 | text.push("perfect!");
865 | }
866 | else {
867 | text.push(`${this.game.win.off} away`);
868 | }
869 |
870 | return navigator.clipboard.writeText(text.join(""));
871 | }
872 |
873 | confirm_copy(ev) {
874 | let clipboard = mk('div.clipboard-confirm', "📋");
875 | clipboard.style.left = `${ev.clientX}px`;
876 | clipboard.style.top = `${ev.clientY}px`;
877 | document.body.append(clipboard);
878 | setTimeout(() => clipboard.remove(), 1000);
879 | }
880 | }
881 |
882 | // TODO:
883 | // - figure out a way to accept a partial answer
884 | // - obviously if you hit the target, you win immediately (unless on "hard mode" where you must use every number)
885 | // - if you run out of numbers and your last one is within X, accept that? (eh but, you might want to backspace and try again)
886 | // - default to daily, but add random twiddles
887 | // - hard mode: must use every number
888 | // - big number pool: normal (25s), hard (12, 37, 62, 87), awkward (gross primes: 17, 43, 71, 89), chaos (anything from 11-100)
889 | // - force count of big numbers
890 | // - do it in other bases??
891 | // - store prefs
892 | // - remember streak, score?
893 | // - fragment trick
894 | // - limit intermediate results to 5 digits
895 | // - hide last expression and ignore input when only one number is left
896 | // - give this an embed and a favicon i guess
897 |
--------------------------------------------------------------------------------