Compare commits

..

34 Commits

Author SHA1 Message Date
catarrh cd8ba1c8aa bundle compact symbol font subsets, sw install hardening, back-exit history collapse, lang/ticker/theory fixes 2026-08-25 16:56:28 +03:00
catarrh 7f5d6f6bed readme update 2026-07-26 00:32:47 +03:00
catarrh b36f541ce3 readme update 2026-07-26 00:28:21 +03:00
catarrh 1a94116975 readme update 2026-07-25 20:50:26 +03:00
catarrh ecbf7f0f8a history population to handle back events 2026-07-24 08:30:08 +03:00
catarrh 41c38055a1 Merge pull request 'store theory files via storage api' (#3) from theory into main
Reviewed-on: #3
2026-07-19 00:10:06 +03:00
catarrh cb230638bd store theory files via storage api 2026-07-19 00:08:22 +03:00
catarrh 0571bc1741 Merge pull request 'theory pages edited' (#2) from theory into main
Reviewed-on: #2
2026-07-18 01:05:55 +03:00
catarrh fd4126b8af theory pages edited 2026-07-18 01:04:17 +03:00
catarrh 35b257dd61 Merge pull request 'theory pages added' (#1) from theory into main
Reviewed-on: #1
2026-07-18 00:51:11 +03:00
catarrh 159f4197ae theory pages added 2026-07-17 23:44:44 +03:00
catarrh 4bb7633353 right arrow instead of equals sign 2026-07-17 00:25:23 +03:00
catarrh 8cf76f6ea7 bump cache 2026-07-17 00:11:15 +03:00
catarrh cbd65ed7db theme switcher 2026-07-17 00:04:30 +03:00
catarrh d985a3d122 better mistakes list 2026-07-16 23:46:33 +03:00
catarrh 453be1178f polished numpad mode 2026-07-16 23:22:38 +03:00
catarrh 7ccc91ad01 added ternary (non-ballanced) and extended hebrew ranges 2026-07-16 22:30:09 +03:00
Трошин Антон Валерьевич 47be247e16 question prompt, monospace digits 2026-07-12 23:03:30 +03:00
Трошин Антон Валерьевич 8396c4410e more fonts/colors polishing 2026-07-12 22:46:15 +03:00
Трошин Антон Валерьевич 1083fa3784 borders in bright theme 2026-07-12 22:27:28 +03:00
Трошин Антон Валерьевич 9e360479ae png icon to fix small icon issue on android 2026-07-12 22:11:22 +03:00
Трошин Антон Валерьевич 818d4a3857 png icon to fix small icon issue on android 2026-07-12 22:10:45 +03:00
Трошин Антон Валерьевич e8a8682e27 png icon to fix small icon issue on android 2026-07-12 22:10:26 +03:00
Трошин Антон Валерьевич 6862907c8d use ponomar font in mistake list 2026-07-12 22:07:08 +03:00
Трошин Антон Валерьевич 9ba81825b7 more cosmetic improvements 2026-07-12 22:03:01 +03:00
Трошин Антон Валерьевич b7a5de9f26 and more cosmetic improvements 2026-07-12 22:00:03 +03:00
Трошин Антон Валерьевич 9881518e58 more cosmetic improvements 2026-07-12 21:55:11 +03:00
Трошин Антон Валерьевич 59a5d6953b more cosmetic improvements 2026-07-12 21:50:38 +03:00
Трошин Антон Валерьевич bd1f2153c1 cosmetic improvements 2026-07-12 11:57:54 +03:00
Трошин Антон Валерьевич 9f720e73ab base subscript moved to end of number, larger font for scroll text 2026-07-12 11:13:53 +03:00
Трошин Антон Валерьевич fc13a8832d bugfix: text scroll now appears faster 2026-07-12 10:34:06 +03:00
Трошин Антон Валерьевич 45c72fc4df nicer text scroll 2026-07-12 10:31:10 +03:00
Трошин Антон Валерьевич 983190d146 no frequent repetitions, choices are a bit less obviuos 2026-07-12 10:18:32 +03:00
Трошин Антон Валерьевич 45cd0ee838 non-positional systems comes first, new game starts with same options 2026-07-12 10:04:11 +03:00
31 changed files with 1068 additions and 145 deletions
Binary file not shown.
Binary file not shown.
+56 -2
View File
@@ -1,3 +1,57 @@
# numnum # NumNum
тренер систем счисления Numeral systems trainer PWA
**Demo:** [numnum.atroshin.ru](https://numnum.atroshin.ru)
## Features
- 10 numeral systems: Binary, Ternary, Balanced Ternary, Octal, Hexadecimal, Braille Decimal, Roman, Church Slavonic, Greek, Hebrew
- Two game modes: multiple choice and manual entry (numpad)
- Configurable number range, time limit (1s30s or no limit), question count (5100)
- Installable PWA with full offline support (theory content cached)
- Dark/light theme · Bilingual (English / Русский)
- In-app theory reference for each system
## Usage
Serve the directory with any HTTP server:
```
python3 -m http.server 8080
```
## Project structure
```
index.html — Entry point
app.js — Game logic and UI
numerals.js — Numeral system converters and choice generator
styles.css — Styles, dark/light theme
sw.js — Service worker (PWA caching)
manifest.json — PWA manifest
theory/ — Theory HTML files (10 systems × 2 languages)
```
## Supported systems
| System | Type | Range |
|---|---|---|
| Binary | Positional | 132768 |
| Ternary | Positional | 127 |
| Balanced Ternary | Positional | 2727 |
| Octal | Positional | 14096 |
| Hexadecimal | Positional | 165536 |
| Braille Decimal | Positional | 09999 |
| Roman | Non-positional | 13999 |
| Church Slavonic | Non-positional | 19999 |
| Greek | Non-positional | 19999 |
| Hebrew | Non-positional | 19999 |
## Development
Theory HTML files live in `theory/`. After editing them, bump the version in `sw.js` to refresh the cache on next install.
## License
MIT
+369 -78
View File
@@ -5,7 +5,8 @@ let LANG = localStorage.getItem('numnum-lang') || (navigator.language.startsWith
const STRINGS = { const STRINGS = {
en: { en: {
subtitle: 'Numeral systems trainer', subtitle: 'Numeral systems trainer',
startGame: 'Start Game', startGame: 'Start',
theory: 'Theory',
chooseSystem: 'Choose numeral system', chooseSystem: 'Choose numeral system',
positional: 'Positional', positional: 'Positional',
nonPositional: 'Non-Positional', nonPositional: 'Non-Positional',
@@ -14,14 +15,14 @@ const STRINGS = {
difficulty: 'Difficulty level', difficulty: 'Difficulty level',
answerMode: 'Answer Mode', answerMode: 'Answer Mode',
timeLimit: 'Time Limit', timeLimit: 'Time Limit',
numberOfRounds: 'Number of Rounds', numberOfRounds: 'Number of Questions',
play: 'Play!', play: 'Go!',
timeLeft: 'Time left', timeLeft: '\u23F1',
submit: 'Submit', submit: 'Submit',
next: 'Next', next: 'Next',
results: 'Results', results: 'Results',
mistakes: 'Mistakes', mistakes: 'Mistakes',
playAgain: 'Play Again', playAgain: 'Start again',
menu: 'Menu', menu: 'Menu',
choose: 'Choose', choose: 'Choose',
type: 'Type', type: 'Type',
@@ -33,8 +34,12 @@ const STRINGS = {
congratulations: 'Congratulations!', congratulations: 'Congratulations!',
pleaseSelect: 'Please select all options', pleaseSelect: 'Please select all options',
installApp: 'Install App', installApp: 'Install App',
theoryUnavailable: 'Content unavailable',
sec: 'sec',
chooseAnswer: 'Choose answer',
enterEquivalent: 'Enter equivalent',
systemNames: { systemNames: {
binary: 'Binary', octal: 'Octal', hex: 'Hexadecimal', binary: 'Binary', base3: 'Ternary', octal: 'Octal', hex: 'Hexadecimal',
ternary: 'Balanced Ternary', braille: 'Braille Decimal', ternary: 'Balanced Ternary', braille: 'Braille Decimal',
roman: 'Roman', slavonic: 'Church Slavonic', roman: 'Roman', slavonic: 'Church Slavonic',
greek: 'Greek', hebrew: 'Hebrew', greek: 'Greek', hebrew: 'Hebrew',
@@ -42,7 +47,8 @@ const STRINGS = {
}, },
ru: { ru: {
subtitle: '\u0422\u0440\u0435\u043D\u0430\u0436\u0435\u0440 \u0441\u0438\u0441\u0442\u0435\u043C \u0441\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u044F', subtitle: '\u0422\u0440\u0435\u043D\u0430\u0436\u0435\u0440 \u0441\u0438\u0441\u0442\u0435\u043C \u0441\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u044F',
startGame: '\u041D\u0430\u0447\u0430\u0442\u044C \u0438\u0433\u0440\u0443', startGame: 'Начать',
theory: '\u0422\u0435\u043E\u0440\u0438\u044F',
chooseSystem: '\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0438\u0441\u0442\u0435\u043C\u0443 \u0441\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u044F', chooseSystem: '\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0438\u0441\u0442\u0435\u043C\u0443 \u0441\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u044F',
positional: '\u041F\u043E\u0437\u0438\u0446\u0438\u043E\u043D\u043D\u044B\u0435', positional: '\u041F\u043E\u0437\u0438\u0446\u0438\u043E\u043D\u043D\u044B\u0435',
nonPositional: '\u041D\u0435\u043F\u043E\u0437\u0438\u0446\u0438\u043E\u043D\u043D\u044B\u0435', nonPositional: '\u041D\u0435\u043F\u043E\u0437\u0438\u0446\u0438\u043E\u043D\u043D\u044B\u0435',
@@ -50,15 +56,15 @@ const STRINGS = {
numberRange: '\u0414\u0438\u0430\u043F\u0430\u0437\u043E\u043D \u0447\u0438\u0441\u0435\u043B', numberRange: '\u0414\u0438\u0430\u043F\u0430\u0437\u043E\u043D \u0447\u0438\u0441\u0435\u043B',
difficulty: '\u0423\u0440\u043E\u0432\u0435\u043D\u044C \u0441\u043B\u043E\u0436\u043D\u043E\u0441\u0442\u0438', difficulty: '\u0423\u0440\u043E\u0432\u0435\u043D\u044C \u0441\u043B\u043E\u0436\u043D\u043E\u0441\u0442\u0438',
answerMode: '\u0420\u0435\u0436\u0438\u043C \u043E\u0442\u0432\u0435\u0442\u0430', answerMode: '\u0420\u0435\u0436\u0438\u043C \u043E\u0442\u0432\u0435\u0442\u0430',
timeLimit: '\u041E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435 \u043F\u043E \u0432\u0440\u0435\u043C\u0435\u043D\u0438', timeLimit: '\u0412\u0440\u0435\u043C\u044F \u043D\u0430 \u043E\u0442\u0432\u0435\u0442',
numberOfRounds: '\u041A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u0440\u0430\u0443\u043D\u0434\u043E\u0432', numberOfRounds: '\u041A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432',
play: '\u0418\u0433\u0440\u0430\u0442\u044C!', play: '\u0412\u043F\u0435\u0440\u0451\u0434!',
timeLeft: '\u041E\u0441\u0442\u0430\u043B\u043E\u0441\u044C', timeLeft: '\u23F1',
submit: '\u041E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C', submit: '\u041E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C',
next: '\u0414\u0430\u043B\u0435\u0435', next: '\u0414\u0430\u043B\u0435\u0435',
results: '\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B', results: '\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B',
mistakes: '\u041E\u0448\u0438\u0431\u043A\u0438', mistakes: '\u041E\u0448\u0438\u0431\u043A\u0438',
playAgain: '\u0418\u0433\u0440\u0430\u0442\u044C \u0441\u043D\u043E\u0432\u0430', playAgain: 'Начать заново',
menu: '\u041C\u0435\u043D\u044E', menu: '\u041C\u0435\u043D\u044E',
choose: '\u0412\u044B\u0431\u043E\u0440', choose: '\u0412\u044B\u0431\u043E\u0440',
type: '\u0412\u0432\u043E\u0434', type: '\u0412\u0432\u043E\u0434',
@@ -70,11 +76,16 @@ const STRINGS = {
congratulations: '\u041F\u043E\u0437\u0434\u0440\u0430\u0432\u043B\u044F\u0435\u043C!', congratulations: '\u041F\u043E\u0437\u0434\u0440\u0430\u0432\u043B\u044F\u0435\u043C!',
pleaseSelect: '\u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0441\u0435 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B', pleaseSelect: '\u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0441\u0435 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B',
installApp: '\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435', installApp: '\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435',
theoryUnavailable: '\u041C\u0430\u0442\u0435\u0440\u0438\u0430\u043B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D',
sec: '\u0441\u0435\u043A',
chooseAnswer: '\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043E\u0442\u0432\u0435\u0442',
enterEquivalent: '\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043B\u043E',
systemNames: { systemNames: {
binary: '\u0414\u0432\u043E\u0438\u0447\u043D\u0430\u044F', binary: '\u0414\u0432\u043E\u0438\u0447\u043D\u0430\u044F',
base3: '\u0422\u0440\u043E\u0438\u0447\u043D\u0430\u044F',
octal: '\u0412\u043E\u0441\u044C\u043C\u0435\u0440\u0438\u0447\u043D\u0430\u044F', octal: '\u0412\u043E\u0441\u044C\u043C\u0435\u0440\u0438\u0447\u043D\u0430\u044F',
hex: '\u0428\u0435\u0441\u0442\u043D\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043D\u0430\u044F', hex: '\u0428\u0435\u0441\u0442\u043D\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043D\u0430\u044F',
ternary: '\u0421\u0431\u0430\u043B\u0430\u043D\u0441\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u0430\u044F \u0442\u0440\u043E\u0438\u0447\u043D\u0430\u044F', ternary: '\u0421\u0438\u043C\u043C\u0435\u0442\u0440\u0438\u0447\u043D\u0430\u044F \u0442\u0440\u043E\u0438\u0447\u043D\u0430\u044F',
braille: '\u0414\u0435\u0441\u044F\u0442\u0438\u0447\u043D\u0430\u044F \u0411\u0440\u0430\u0439\u043B\u044F', braille: '\u0414\u0435\u0441\u044F\u0442\u0438\u0447\u043D\u0430\u044F \u0411\u0440\u0430\u0439\u043B\u044F',
roman: '\u0420\u0438\u043C\u0441\u043A\u0430\u044F', roman: '\u0420\u0438\u043C\u0441\u043A\u0430\u044F',
slavonic: '\u0426\u0435\u0440\u043A\u043E\u0432\u043D\u043E\u0441\u043B\u0430\u0432\u044F\u043D\u0441\u043A\u0430\u044F', slavonic: '\u0426\u0435\u0440\u043A\u043E\u0432\u043D\u043E\u0441\u043B\u0430\u0432\u044F\u043D\u0441\u043A\u0430\u044F',
@@ -104,6 +115,55 @@ function applyTranslations() {
}); });
} }
function refreshDynamicText() {
const active = document.querySelector('.screen.active');
if (!active) return;
switch (active.id) {
case 'screen-setup-system':
renderSetupSystem();
break;
case 'screen-setup-range':
renderSetupRange();
break;
case 'screen-setup-options':
document.querySelectorAll('#setup-modes .option-btn').forEach((btn, i) => {
btn.textContent = t(MODES[i].labelKey);
});
[...document.getElementById('setup-timing').options].forEach((opt, i) => {
opt.textContent = TIMINGS[i].value === 0 ? t('noLimit') : TIMINGS[i].value + ' ' + t('sec');
});
break;
case 'screen-theory-system':
renderTheorySystem();
break;
case 'screen-theory-content':
document.getElementById('theory-title').textContent = systemName(lastTheoryKey);
break;
case 'screen-game':
if (game.system && game.range) {
document.getElementById('score-system-range').textContent = systemName(game.system) + ': ' + game.range.label;
}
break;
case 'screen-results': {
const completed = game.currentRound >= game.rounds;
const total = completed ? game.rounds : game.currentRound;
const pct = total > 0 ? Math.round((game.correctCount / total) * 100) : 0;
document.getElementById('results-summary').innerHTML =
'<div class="big-number">' + game.correctCount + '/' + total + '</div>' +
'<div>' + t('pctCorrect', {pct: pct}) + '</div>';
const labelEl = document.getElementById('results-mistakes-label');
if (game.mistakes.length > 0 || labelEl.style.display !== 'none') {
labelEl.textContent = t('mistakes');
}
const congratsEl = document.getElementById('results-congrats');
if (congratsEl.textContent) {
congratsEl.textContent = t('congratulations');
}
break;
}
}
}
const SYSTEM_RANGES = { const SYSTEM_RANGES = {
binary: [ binary: [
{ label: '2\u2070 \u2013 2\u00B3', min: 1, max: 8 }, { label: '2\u2070 \u2013 2\u00B3', min: 1, max: 8 },
@@ -111,6 +171,12 @@ const SYSTEM_RANGES = {
{ label: '2\u2070 \u2013 2\u2077', min: 1, max: 128 }, { label: '2\u2070 \u2013 2\u2077', min: 1, max: 128 },
{ label: '2\u2070 \u2013 2\u00B9\u2075', min: 1, max: 32768 }, { label: '2\u2070 \u2013 2\u00B9\u2075', min: 1, max: 32768 },
], ],
base3: [
{ label: '3\u2070 \u2013 3\u00B9', min: 1, max: 3 },
{ label: '3\u00B9 \u2013 3\u00B2', min: 3, max: 9 },
{ label: '3\u00B2 \u2013 3\u00B3', min: 9, max: 27 },
{ label: '3\u2070 \u2013 3\u00B3', min: 1, max: 27 },
],
ternary: [ ternary: [
{ label: '3\u2070 \u2013 3\u00B9', min: 1, max: 3 }, { label: '3\u2070 \u2013 3\u00B9', min: 1, max: 3 },
{ label: '\u20133\u00B9 \u2013 3\u00B9', min: -3, max: 3 }, { label: '\u20133\u00B9 \u2013 3\u00B9', min: -3, max: 3 },
@@ -118,7 +184,7 @@ const SYSTEM_RANGES = {
{ label: '\u20133\u00B3 \u2013 3\u00B3', min: -27, max: 27 }, { label: '\u20133\u00B3 \u2013 3\u00B3', min: -27, max: 27 },
], ],
octal: [ octal: [
{ label: '8\u2070 \u2013 8\u00B9', min: 1, max: 8 }, { label: '8\u00B9 \u2013 8\u00B2', min: 8, max: 64 },
{ label: '8\u00B2 \u2013 8\u00B3', min: 64, max: 512 }, { label: '8\u00B2 \u2013 8\u00B3', min: 64, max: 512 },
{ label: '8\u2070 \u2013 8\u00B3', min: 1, max: 512 }, { label: '8\u2070 \u2013 8\u00B3', min: 1, max: 512 },
{ label: '8\u2070 \u2013 8\u2074', min: 1, max: 4096 }, { label: '8\u2070 \u2013 8\u2074', min: 1, max: 4096 },
@@ -143,41 +209,54 @@ const SYSTEM_RANGES = {
], ],
greek: [ greek: [
{ label: '1 \u2013 9', min: 1, max: 9 }, { label: '1 \u2013 9', min: 1, max: 9 },
{ label: '10 \u2013 99', min: 10, max: 99 }, { label: '10 \u2013 19', min: 10, max: 19 },
{ label: '20 \u2013 99', min: 20, max: 99 },
{ label: '1 \u2013 99', min: 1, max: 99 },
{ label: '100 \u2013 999', min: 100, max: 999 }, { label: '100 \u2013 999', min: 100, max: 999 },
{ label: '1000 \u2013 9999', min: 1000, max: 9999 }, { label: '1000 \u2013 9999', min: 1000, max: 9999 },
], ],
slavonic: [ slavonic: [
{ label: '1 \u2013 9', min: 1, max: 9 }, { label: '1 \u2013 9', min: 1, max: 9 },
{ label: '10 \u2013 99', min: 10, max: 99 }, { label: '10 \u2013 19', min: 10, max: 19 },
{ label: '20 \u2013 99', min: 20, max: 99 },
{ label: '1 \u2013 99', min: 1, max: 99 },
{ label: '100 \u2013 999', min: 100, max: 999 }, { label: '100 \u2013 999', min: 100, max: 999 },
{ label: '1000 \u2013 9999', min: 1000, max: 9999 }, { label: '1000 \u2013 9999', min: 1000, max: 9999 },
], ],
hebrew: [ hebrew: [
{ label: '1 \u2013 9', min: 1, max: 9 }, { label: '1 \u2013 9', min: 1, max: 9 },
{ label: '10 \u2013 99', min: 10, max: 99 }, { label: '10 \u2013 19', min: 10, max: 19 },
{ label: '100 \u2013 499', min: 100, max: 499 }, { label: '20 \u2013 99', min: 20, max: 99 },
{ label: '1 \u2013 99', min: 1, max: 99 },
{ label: '100 \u2013 999', min: 100, max: 999 },
{ label: '1000 \u2013 9999', min: 1000, max: 9999 },
], ],
}; };
const SYSTEM_GROUPS = { const SYSTEM_GROUPS = {
pos: ['binary', 'octal', 'hex', 'ternary', 'braille'], pos: ['binary', 'octal', 'hex', 'braille', 'base3', 'ternary'],
nonpos: ['roman', 'greek', 'slavonic', 'hebrew'], nonpos: ['roman', 'greek', 'slavonic', 'hebrew'],
}; };
const theoryCache = {};
let lastTheoryKey = null;
const MODES = [ const MODES = [
{ labelKey: 'choose', value: 'choice' }, { labelKey: 'choose', value: 'choice' },
{ labelKey: 'type', value: 'manual' }, { labelKey: 'type', value: 'manual' },
]; ];
const TIMINGS = [ const TIMINGS = [
{ label: '60 sec', value: 60 }, { value: 0 },
{ label: '40 sec', value: 40 }, { value: 30 },
{ label: '10 sec', value: 10 }, { value: 10 },
{ label: 'nolimit', value: 0 }, { value: 5 },
{ value: 3 },
{ value: 2 },
{ value: 1 },
]; ];
const ROUNDS = [10, 25, 50, 100]; const ROUNDS = [5, 10, 25, 50, 100];
let game = { let game = {
system: null, system: null,
@@ -194,9 +273,23 @@ let game = {
mistakes: [], mistakes: [],
}; };
let popstateHandled = false;
let navDepth = 0;
function showScreen(id) { function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById('screen-' + id).classList.add('active'); document.getElementById('screen-' + id).classList.add('active');
if (id === 'menu') {
if (navDepth > 0) {
const n = navDepth;
navDepth = 0;
history.go(-n);
}
} else if (!popstateHandled) {
navDepth++;
history.pushState({ screen: id }, '');
}
popstateHandled = false;
} }
function shuffle(arr) { function shuffle(arr) {
@@ -270,8 +363,8 @@ function renderSetupRange() {
function renderSetupOptions() { function renderSetupOptions() {
const defaultMode = 0; const defaultMode = 0;
const defaultTiming = 3; const defaultTiming = 0;
const defaultRounds = 0; const defaultRounds = 1;
setupState.mode = defaultMode; setupState.mode = defaultMode;
setupState.timing = defaultTiming; setupState.timing = defaultTiming;
@@ -291,51 +384,49 @@ function renderSetupOptions() {
modesDiv.appendChild(btn); modesDiv.appendChild(btn);
}); });
const timingDiv = document.getElementById('setup-timing'); const timingSelect = document.getElementById('setup-timing');
timingDiv.innerHTML = ''; timingSelect.innerHTML = '';
TIMINGS.forEach((tm, i) => { TIMINGS.forEach((tm, i) => {
const btn = document.createElement('button'); const opt = document.createElement('option');
btn.className = 'option-btn' + (i === defaultTiming ? ' selected' : ''); opt.value = i;
btn.textContent = tm.value === 0 ? t('noLimit') : tm.label; opt.textContent = tm.value === 0 ? t('noLimit') : tm.value + ' ' + t('sec');
btn.addEventListener('click', () => { timingSelect.appendChild(opt);
timingDiv.querySelectorAll('.option-btn').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
setupState.timing = i;
}); });
timingDiv.appendChild(btn); timingSelect.value = defaultTiming;
timingSelect.addEventListener('change', () => {
setupState.timing = parseInt(timingSelect.value, 10);
}); });
const roundsDiv = document.getElementById('setup-rounds'); const roundsSelect = document.getElementById('setup-rounds');
roundsDiv.innerHTML = ''; roundsSelect.value = defaultRounds;
ROUNDS.forEach((r, i) => { roundsSelect.addEventListener('change', () => {
const btn = document.createElement('button'); setupState.rounds = parseInt(roundsSelect.value, 10);
btn.className = 'option-btn' + (i === defaultRounds ? ' selected' : '');
btn.textContent = String(r);
btn.addEventListener('click', () => {
roundsDiv.querySelectorAll('.option-btn').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
setupState.rounds = i;
});
roundsDiv.appendChild(btn);
}); });
showScreen('setup-options'); showScreen('setup-options');
} }
// ---- GAME ---- // ---- GAME ----
let lastSetupState = null;
const MARK_CORRECT = '\u2713 ';
const MARK_INCORRECT = '\u2717 ';
function startGame() { function startGame() {
lastSetupState = { ...setupState };
game.system = setupState.system; game.system = setupState.system;
game.range = setupState.rangeData || RANGES[setupState.range]; game.range = setupState.rangeData;
game.mode = MODES[setupState.mode]?.value ?? setupState.mode; game.mode = MODES[setupState.mode]?.value ?? setupState.mode;
game.timing = TIMINGS[setupState.timing]?.value ?? setupState.timing; game.timing = TIMINGS[setupState.timing]?.value ?? setupState.timing;
game.rounds = ROUNDS[setupState.rounds]; game.rounds = ROUNDS[setupState.rounds];
game.currentRound = 0; game.currentRound = 0;
game.correctCount = 0; game.correctCount = 0;
game.mistakes = []; game.mistakes = [];
game.lastAnswer = null;
document.getElementById('score-correct').textContent = '0'; document.getElementById('score-system-range').textContent = systemName(game.system) + ': ' + game.range.label;
document.getElementById('score-total').textContent = '0'; document.getElementById('score-round').textContent = '0/' + game.rounds;
document.getElementById('score-system').textContent = systemName(game.system); document.getElementById('score-correct').textContent = MARK_CORRECT + '0';
document.getElementById('score-incorrect').textContent = MARK_INCORRECT + '0';
showScreen('game'); showScreen('game');
nextRound(); nextRound();
@@ -349,19 +440,26 @@ function nextRound() {
game.currentRound++; game.currentRound++;
game.answered = false; game.answered = false;
game.correctAnswer = randInt(game.range.min, game.range.max); let num;
do {
num = randInt(game.range.min, game.range.max);
} while (num === game.lastAnswer && game.range.min !== game.range.max);
game.correctAnswer = num;
game.lastAnswer = num;
document.getElementById('score-total').textContent = game.currentRound; document.getElementById('score-round').textContent = game.currentRound + '/' + game.rounds;
document.getElementById('score-correct').textContent = game.correctCount; document.getElementById('score-correct').textContent = MARK_CORRECT + game.correctCount;
const display = SYSTEMS[game.system].toDisplay(game.correctAnswer); const display = SYSTEMS[game.system].toDisplay(game.correctAnswer);
const numEl = document.getElementById('number-display'); const numEl = document.getElementById('number-display');
numEl.textContent = display; numEl.textContent = display;
numEl.classList.toggle('small-text', display.length > 12); numEl.classList.toggle('small-text', display.length > 12);
numEl.classList.toggle('slavonic-font', game.system === 'slavonic'); numEl.classList.toggle('slavonic-font', game.system === 'slavonic');
numEl.classList.toggle('mono', SYSTEM_GROUPS.pos.includes(game.system));
document.getElementById('answer-feedback').textContent = ''; const feedback = document.getElementById('answer-feedback');
document.getElementById('answer-feedback').className = 'answer-feedback'; feedback.textContent = game.mode === 'choice' ? t('chooseAnswer') : t('enterEquivalent');
feedback.className = 'answer-feedback';
document.getElementById('btn-next').classList.add('hidden'); document.getElementById('btn-next').classList.add('hidden');
if (game.mode === 'choice') { if (game.mode === 'choice') {
@@ -379,7 +477,8 @@ function renderChoices() {
choiceArea.classList.remove('hidden'); choiceArea.classList.remove('hidden');
numpadArea.classList.add('hidden'); numpadArea.classList.add('hidden');
const choices = generateChoices(game.correctAnswer, [game.range.min, game.range.max], 3); const nonpos = ['roman', 'greek', 'slavonic', 'hebrew'];
const choices = generateChoices(game.correctAnswer, [game.range.min, game.range.max], 3, nonpos.includes(game.system));
choices.forEach((val, i) => { choices.forEach((val, i) => {
const btn = document.getElementById('choice-' + i); const btn = document.getElementById('choice-' + i);
btn.textContent = val; btn.textContent = val;
@@ -401,22 +500,29 @@ function handleChoice(idx, val, choices) {
if (val === game.correctAnswer) { if (val === game.correctAnswer) {
game.correctCount++; game.correctCount++;
document.getElementById('score-correct').textContent = game.correctCount; document.getElementById('score-correct').textContent = MARK_CORRECT + game.correctCount;
allBtns[idx].classList.add('correct-choice'); allBtns[idx].classList.add('correct-choice');
feedback.textContent = t('correct'); feedback.textContent = t('correct');
feedback.className = 'answer-feedback correct'; feedback.className = 'answer-feedback correct';
const rect = allBtns[idx].getBoundingClientRect();
spawnAnswerConfetti(rect.left + rect.width / 2, rect.top + rect.height / 2);
} else { } else {
allBtns[idx].classList.add('incorrect-choice'); allBtns[idx].classList.add('incorrect-choice');
const correctIdx = choices.indexOf(game.correctAnswer); const correctIdx = choices.indexOf(game.correctAnswer);
allBtns[correctIdx].classList.add('correct-choice'); allBtns[correctIdx].classList.add('correct-choice');
feedback.textContent = t('incorrect', {n: game.correctAnswer}); feedback.textContent = t('incorrect', {n: game.correctAnswer});
feedback.className = 'answer-feedback incorrect'; feedback.className = 'answer-feedback incorrect';
if (!game.mistakes.some(m => m.decimal === game.correctAnswer)) {
game.mistakes.push({ game.mistakes.push({
key: game.system,
system: SYSTEMS[game.system].toDisplay(game.correctAnswer), system: SYSTEMS[game.system].toDisplay(game.correctAnswer),
decimal: game.correctAnswer, decimal: game.correctAnswer,
systemName: systemName(game.system), systemName: systemName(game.system),
wrong: val,
}); });
} }
}
document.getElementById('score-incorrect').textContent = MARK_INCORRECT + (game.currentRound - game.correctCount);
document.getElementById('btn-next').classList.remove('hidden'); document.getElementById('btn-next').classList.remove('hidden');
} }
@@ -439,7 +545,8 @@ function renderNumpad() {
clearBtn.dataset.key = 'clear'; clearBtn.dataset.key = 'clear';
} }
document.getElementById('btn-submit-answer').classList.remove('hidden'); const submitBtn = document.getElementById('btn-submit-answer');
submitBtn.classList.add('hidden');
numpadArea.querySelectorAll('.numpad-key').forEach(key => { numpadArea.querySelectorAll('.numpad-key').forEach(key => {
key.onclick = () => handleNumpadKey(key.dataset.key); key.onclick = () => handleNumpadKey(key.dataset.key);
@@ -456,18 +563,25 @@ function renderNumpad() {
const feedback = document.getElementById('answer-feedback'); const feedback = document.getElementById('answer-feedback');
if (val === game.correctAnswer) { if (val === game.correctAnswer) {
game.correctCount++; game.correctCount++;
document.getElementById('score-correct').textContent = game.correctCount; document.getElementById('score-correct').textContent = MARK_CORRECT + game.correctCount;
feedback.textContent = t('correct'); feedback.textContent = t('correct');
feedback.className = 'answer-feedback correct'; feedback.className = 'answer-feedback correct';
const rect = inputEl.getBoundingClientRect();
spawnAnswerConfetti(rect.left + rect.width / 2, rect.top + rect.height / 2);
} else { } else {
feedback.textContent = t('incorrect', {n: game.correctAnswer}); feedback.textContent = t('incorrect', {n: game.correctAnswer});
feedback.className = 'answer-feedback incorrect'; feedback.className = 'answer-feedback incorrect';
if (!game.mistakes.some(m => m.decimal === game.correctAnswer)) {
game.mistakes.push({ game.mistakes.push({
key: game.system,
system: SYSTEMS[game.system].toDisplay(game.correctAnswer), system: SYSTEMS[game.system].toDisplay(game.correctAnswer),
decimal: game.correctAnswer, decimal: game.correctAnswer,
systemName: systemName(game.system), systemName: systemName(game.system),
wrong: val,
}); });
} }
}
document.getElementById('score-incorrect').textContent = MARK_INCORRECT + (game.currentRound - game.correctCount);
document.getElementById('btn-next').classList.remove('hidden'); document.getElementById('btn-next').classList.remove('hidden');
}; };
@@ -492,6 +606,9 @@ function handleNumpadKey(key) {
inputEl.textContent += key; inputEl.textContent += key;
} }
} }
const submitBtn = document.getElementById('btn-submit-answer');
submitBtn.classList.toggle('hidden', isNaN(parseInt(inputEl.textContent, 10)));
} }
function startTimer() { function startTimer() {
@@ -508,12 +625,12 @@ function startTimer() {
timerArea.classList.add('visible'); timerArea.classList.add('visible');
game.timeLeft = game.timing; game.timeLeft = game.timing;
timerEl.textContent = game.timeLeft + 's'; timerEl.textContent = game.timeLeft + ' ' + t('sec');
timerEl.classList.remove('urgent'); timerEl.classList.remove('urgent');
game.timerInterval = setInterval(() => { game.timerInterval = setInterval(() => {
game.timeLeft--; game.timeLeft--;
timerEl.textContent = game.timeLeft + 's'; timerEl.textContent = game.timeLeft + ' ' + t('sec');
if (game.timeLeft <= 10) { if (game.timeLeft <= 10) {
timerEl.classList.add('urgent'); timerEl.classList.add('urgent');
@@ -526,11 +643,15 @@ function startTimer() {
const feedback = document.getElementById('answer-feedback'); const feedback = document.getElementById('answer-feedback');
feedback.textContent = t('timesUp', {n: game.correctAnswer}); feedback.textContent = t('timesUp', {n: game.correctAnswer});
feedback.className = 'answer-feedback incorrect'; feedback.className = 'answer-feedback incorrect';
if (!game.mistakes.some(m => m.decimal === game.correctAnswer)) {
game.mistakes.push({ game.mistakes.push({
key: game.system,
system: SYSTEMS[game.system].toDisplay(game.correctAnswer), system: SYSTEMS[game.system].toDisplay(game.correctAnswer),
decimal: game.correctAnswer, decimal: game.correctAnswer,
systemName: systemName(game.system), systemName: systemName(game.system),
}); });
}
document.getElementById('score-incorrect').textContent = MARK_INCORRECT + (game.currentRound - game.correctCount);
document.getElementById('btn-next').classList.remove('hidden'); document.getElementById('btn-next').classList.remove('hidden');
@@ -577,10 +698,15 @@ function endGame() {
game.mistakes.forEach(m => { game.mistakes.forEach(m => {
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'result-item'; item.className = 'result-item';
const sysClass = m.key === 'slavonic' ? ' slavonic-font' : '';
item.innerHTML = ` item.innerHTML = `
<div class="result-system">${m.system}</div> <div class="result-system${sysClass}">${m.system}</div>
<div class="result-arrow">\u2192</div> <div class="result-arrow">\u2B95</div>
<div class="result-decimal">${m.decimal}</div> <div class="result-decimal">${m.decimal}</div>
${m.wrong
? '<div class="result-wrong">\u2718 ' + m.wrong + '</div>'
: '<div class="result-wrong">\u2718 \u23F1</div>'
}
`; `;
listEl.appendChild(item); listEl.appendChild(item);
}); });
@@ -593,11 +719,34 @@ function endGame() {
} }
} }
function spawnAnswerConfetti(x, y) {
const chars = ['🎉', '🎊', '✨', '🎈', '🥳', '💫'];
const burst = document.createElement('div');
burst.className = 'answer-confetti-burst';
burst.style.left = x + 'px';
burst.style.top = y + 'px';
for (let j = 0; j < 8; j++) {
const particle = document.createElement('span');
particle.className = 'confetti-particle';
particle.textContent = chars[Math.floor(Math.random() * chars.length)];
particle.style.setProperty('--dx', (Math.random() - 0.5) * 80 + 'px');
particle.style.setProperty('--dy', -(30 + Math.random() * 60) + 'px');
particle.style.setProperty('--rot', Math.random() * 360 + 'deg');
particle.style.animationDuration = (0.5 + Math.random() * 0.3) + 's';
burst.appendChild(particle);
}
document.body.appendChild(burst);
setTimeout(() => burst.remove(), 1000);
}
function spawnConfetti() { function spawnConfetti() {
const chars = ['🎉', '🎊', '✨', '🎈', '🥳', '💫']; const chars = ['🎉', '🎊', '✨', '🎈', '🥳', '💫'];
const container = document.getElementById('results-congrats'); const container = document.getElementById('results-congrats');
for (let i = 0; i < 3; i++) { const bursts = Math.round(game.rounds / 3);
for (let i = 0; i < bursts; i++) {
setTimeout(() => { setTimeout(() => {
const burst = document.createElement('div'); const burst = document.createElement('div');
burst.className = 'confetti-burst'; burst.className = 'confetti-burst';
@@ -621,6 +770,58 @@ function spawnConfetti() {
} }
} }
// ---- THEORY ----
async function loadTheory(key) {
if (theoryCache[key]) return theoryCache[key];
try {
const resp = await fetch(`theory/${key}.${LANG}.html`);
if (!resp.ok) throw new Error(resp.status);
const html = await resp.text();
theoryCache[key] = html;
return html;
} catch {
if (LANG !== 'en') {
try {
const resp = await fetch(`theory/${key}.en.html`);
const html = await resp.text();
theoryCache[key] = html;
return html;
} catch { return null; }
}
return null;
}
}
function renderTheorySystem() {
const posDiv = document.getElementById('theory-systems-pos');
const nonposDiv = document.getElementById('theory-systems-nonpos');
posDiv.innerHTML = '';
nonposDiv.innerHTML = '';
for (const [group, keys] of Object.entries(SYSTEM_GROUPS)) {
const target = group === 'pos' ? posDiv : nonposDiv;
keys.forEach(key => {
const btn = document.createElement('button');
btn.className = 'option-btn';
btn.textContent = systemName(key);
btn.addEventListener('click', () => showTheoryContent(key));
target.appendChild(btn);
});
}
showScreen('theory-system');
}
async function showTheoryContent(key) {
lastTheoryKey = key;
const html = await loadTheory(key);
document.getElementById('theory-title').textContent = systemName(key);
document.getElementById('theory-scroll').innerHTML =
html || '<div class="theory-unavailable">' + t('theoryUnavailable') + '</div>';
showScreen('theory-content');
}
// ---- INIT ---- // ---- INIT ----
const SUBSCRIPTS = { '0':'\u2080', '1':'\u2081', '2':'\u2082', '3':'\u2083', const SUBSCRIPTS = { '0':'\u2080', '1':'\u2081', '2':'\u2082', '3':'\u2083',
@@ -630,11 +831,30 @@ function toSubscript(n) {
return String(n).split('').map(d => SUBSCRIPTS[d]).join(''); return String(n).split('').map(d => SUBSCRIPTS[d]).join('');
} }
const TICKER_COLORS_DARK = [
'#7ec8e3', '#a8d5a2', '#f0c674', '#d4a0d4', '#e8a87c',
'#87ceeb', '#98d4bb', '#deb887', '#c9b1ff', '#ff9a9e'
];
const TICKER_COLORS_LIGHT = [
'#2980b9', '#27ae60', '#d4a017', '#9b59b6', '#d35400',
'#3498db', '#1abc9c', '#b8860b', '#8e44ad', '#e74c3c'
];
const THEME_KEY = 'numnum-theme';
function getPreferredTheme() {
const saved = localStorage.getItem(THEME_KEY);
if (saved === 'light' || saved === 'dark') return saved;
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
const TICKER_SYSTEMS = [ const TICKER_SYSTEMS = [
{ key: 'binary', min: 1, max: 255, prefix: toSubscript(2) }, { key: 'binary', min: 1, max: 255, suffix: toSubscript(2) },
{ key: 'octal', min: 1, max: 511, prefix: toSubscript(8) }, { key: 'base3', min: 1, max: 81, suffix: toSubscript(3) },
{ key: 'octal', min: 1, max: 511, suffix: toSubscript(8) },
{ key: 'hex', min: 1, max: 255, prefix: '0x' }, { key: 'hex', min: 1, max: 255, prefix: '0x' },
{ key: 'ternary', min: 1, max: 27, prefix: toSubscript(3) }, { key: 'ternary', min: 1, max: 27, suffix: toSubscript(3) },
{ key: 'braille', min: 1, max: 99, prefix: null }, { key: 'braille', min: 1, max: 99, prefix: null },
{ key: 'roman', min: 1, max: 3999, prefix: null }, { key: 'roman', min: 1, max: 3999, prefix: null },
{ key: 'greek', min: 1, max: 999, prefix: null }, { key: 'greek', min: 1, max: 999, prefix: null },
@@ -642,22 +862,41 @@ const TICKER_SYSTEMS = [
]; ];
function generateTickerText() { function generateTickerText() {
const palette = document.documentElement.dataset.theme === 'light'
? TICKER_COLORS_LIGHT
: TICKER_COLORS_DARK;
const pairs = []; const pairs = [];
let lastKey = null;
let lastColor = null;
for (let i = 0; i < 20; i++) { for (let i = 0; i < 20; i++) {
const sys = TICKER_SYSTEMS[Math.floor(Math.random() * TICKER_SYSTEMS.length)]; let sys;
do {
sys = TICKER_SYSTEMS[Math.floor(Math.random() * TICKER_SYSTEMS.length)];
} while (sys.key === lastKey);
lastKey = sys.key;
let color;
do {
color = palette[Math.floor(Math.random() * palette.length)];
} while (color === lastColor);
lastColor = color;
const num = sys.min + Math.floor(Math.random() * (sys.max - sys.min + 1)); const num = sys.min + Math.floor(Math.random() * (sys.max - sys.min + 1));
const display = SYSTEMS[sys.key].toDisplay(num); const display = SYSTEMS[sys.key].toDisplay(num);
const prefixed = sys.prefix ? sys.prefix + display : display; let labelled = display;
if (sys.prefix) labelled = sys.prefix + display;
if (sys.suffix) labelled = display + sys.suffix;
const wrapped = sys.key === 'slavonic' const wrapped = sys.key === 'slavonic'
? '<span class="slavonic-font">' + prefixed + '</span>' ? '<span class="slavonic-font">' + labelled + '</span>'
: prefixed; : labelled;
pairs.push(wrapped + ' = ' + num); pairs.push('<span style="color:' + color + '">' + wrapped + ' \u2B95 ' + num + '</span>');
} }
return pairs.join('\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0'); return pairs.join('\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0');
} }
const tickerText = generateTickerText(); function refreshTicker() {
document.getElementById('ticker').innerHTML = tickerText + '\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0' + tickerText; const tickerText = generateTickerText();
document.getElementById('ticker').innerHTML = tickerText + '\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0' + tickerText;
}
const langSelect = document.getElementById('lang-select'); const langSelect = document.getElementById('lang-select');
langSelect.value = LANG; langSelect.value = LANG;
@@ -665,10 +904,25 @@ langSelect.addEventListener('change', () => {
LANG = langSelect.value; LANG = langSelect.value;
localStorage.setItem('numnum-lang', LANG); localStorage.setItem('numnum-lang', LANG);
applyTranslations(); applyTranslations();
refreshDynamicText();
}); });
applyTranslations(); applyTranslations();
function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
document.getElementById('theme-toggle').textContent = theme === 'dark' ? '\u2600' : '\u263E';
refreshTicker();
}
applyTheme(getPreferredTheme());
document.getElementById('theme-toggle').addEventListener('click', () => {
const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
localStorage.setItem(THEME_KEY, next);
applyTheme(next);
});
let deferredPrompt = null; let deferredPrompt = null;
window.addEventListener('beforeinstallprompt', (e) => { window.addEventListener('beforeinstallprompt', (e) => {
@@ -727,7 +981,8 @@ document.getElementById('btn-options-back').addEventListener('click', () => {
}); });
document.getElementById('btn-play').addEventListener('click', () => { document.getElementById('btn-play').addEventListener('click', () => {
if (setupState.mode === undefined || setupState.timing === undefined || setupState.rounds === undefined) { if (!setupState.system || !setupState.rangeData ||
setupState.mode === undefined || setupState.timing === undefined || setupState.rounds === undefined) {
showSetupMessage(t('pleaseSelect')); showSetupMessage(t('pleaseSelect'));
return; return;
} }
@@ -739,13 +994,26 @@ document.getElementById('btn-next').addEventListener('click', () => {
}); });
document.getElementById('btn-play-again').addEventListener('click', () => { document.getElementById('btn-play-again').addEventListener('click', () => {
renderSetupSystem(); setupState = { ...lastSetupState };
startGame();
}); });
document.getElementById('btn-results-menu').addEventListener('click', () => { document.getElementById('btn-results-menu').addEventListener('click', () => {
showScreen('menu'); showScreen('menu');
}); });
document.getElementById('btn-theory').addEventListener('click', () => {
renderTheorySystem();
});
document.getElementById('btn-theory-back').addEventListener('click', () => {
showScreen('menu');
});
document.getElementById('btn-theory-content-back').addEventListener('click', () => {
renderTheorySystem();
});
document.getElementById('btn-stop').addEventListener('click', () => { document.getElementById('btn-stop').addEventListener('click', () => {
stopTimer(); stopTimer();
endGame(); endGame();
@@ -780,3 +1048,26 @@ document.addEventListener('keydown', (e) => {
} }
} }
}); });
// Handle mobile back/cancel button
const BACK_MAP = {
'screen-setup-system': 'btn-system-back',
'screen-setup-range': 'btn-range-back',
'screen-setup-options': 'btn-options-back',
'screen-game': 'btn-stop',
'screen-results': 'btn-results-menu',
'screen-theory-system': 'btn-theory-back',
'screen-theory-content': 'btn-theory-content-back',
};
window.addEventListener('popstate', () => {
navDepth = Math.max(0, navDepth - 1);
const active = document.querySelector('.screen.active');
if (!active) return;
const btnId = BACK_MAP[active.id];
if (btnId) {
popstateHandled = true;
document.getElementById(btnId).click();
}
});
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+49 -11
View File
@@ -10,7 +10,7 @@
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<link rel="icon" type="image/svg+xml" href="icon.svg"> <link rel="icon" type="image/svg+xml" href="icon.svg">
<link rel="apple-touch-icon" href="icon-192.png"> <link rel="apple-touch-icon" href="icon-192.png">
<link rel="stylesheet" href="styles.css?v=35"> <link rel="stylesheet" href="styles.css">
</head> </head>
<body> <body>
<div id="app"> <div id="app">
@@ -24,10 +24,14 @@
<div class="ticker" id="ticker"></div> <div class="ticker" id="ticker"></div>
</div> </div>
<button class="btn btn-primary btn-large" id="btn-start" data-i18n="startGame">Start Game</button> <button class="btn btn-primary btn-large" id="btn-start" data-i18n="startGame">Start Game</button>
<button class="btn btn-secondary btn-large" id="btn-theory" data-i18n="theory">Theory</button>
<div class="menu-controls">
<select id="lang-select" class="lang-select"> <select id="lang-select" class="lang-select">
<option value="en">English</option> <option value="en">English</option>
<option value="ru">Русский</option> <option value="ru">Русский</option>
</select> </select>
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme"></button>
</div>
<button class="btn btn-secondary btn-large hidden" id="btn-install" data-i18n="installApp">Install App</button> <button class="btn btn-secondary btn-large hidden" id="btn-install" data-i18n="installApp">Install App</button>
</div> </div>
</div> </div>
@@ -38,13 +42,13 @@
<h2 data-i18n="chooseSystem">Choose numeral system</h2> <h2 data-i18n="chooseSystem">Choose numeral system</h2>
<div class="system-group"> <div class="system-group">
<h3 data-i18n="positional">Positional</h3> <h3 data-i18n="nonPositional">Non-Positional</h3>
<div id="setup-systems-pos" class="option-grid"></div> <div id="setup-systems-nonpos" class="option-grid"></div>
</div> </div>
<div class="system-group"> <div class="system-group">
<h3 data-i18n="nonPositional">Non-Positional</h3> <h3 data-i18n="positional">Positional</h3>
<div id="setup-systems-nonpos" class="option-grid"></div> <div id="setup-systems-pos" class="option-grid"></div>
</div> </div>
<button class="btn btn-secondary" id="btn-system-back" data-i18n="back">Back</button> <button class="btn btn-secondary" id="btn-system-back" data-i18n="back">Back</button>
@@ -76,12 +80,18 @@
<div class="setup-section"> <div class="setup-section">
<h3 data-i18n="timeLimit">Time Limit</h3> <h3 data-i18n="timeLimit">Time Limit</h3>
<div id="setup-timing" class="option-grid"></div> <select id="setup-timing" class="lang-select"></select>
</div> </div>
<div class="setup-section"> <div class="setup-section">
<h3 data-i18n="numberOfRounds">Number of Rounds</h3> <h3 data-i18n="numberOfRounds">Number of Rounds</h3>
<div id="setup-rounds" class="option-grid"></div> <select id="setup-rounds" class="lang-select">
<option value="0">5</option>
<option value="1">10</option>
<option value="2">25</option>
<option value="3">50</option>
<option value="4">100</option>
</select>
</div> </div>
<button class="btn btn-primary btn-large" id="btn-play" data-i18n="play">Play!</button> <button class="btn btn-primary btn-large" id="btn-play" data-i18n="play">Play!</button>
@@ -93,8 +103,9 @@
<div id="screen-game" class="screen"> <div id="screen-game" class="screen">
<div class="game-header"> <div class="game-header">
<div class="score-display"> <div class="score-display">
<span id="score-correct">0</span>/<span id="score-total">0</span> <span id="score-round">0/0</span>
<span id="score-system"></span> <span id="score-correct" class="score-correct">0</span>
<span id="score-incorrect" class="score-incorrect">0</span>
</div> </div>
<div class="timer-area" id="timer-area"> <div class="timer-area" id="timer-area">
<span class="timer-label" data-i18n="timeLeft">Time left</span> <span class="timer-label" data-i18n="timeLeft">Time left</span>
@@ -105,6 +116,8 @@
</div> </div>
</div> </div>
<div class="system-range-label" id="score-system-range"></div>
<div class="game-body"> <div class="game-body">
<div class="number-display" id="number-display"></div> <div class="number-display" id="number-display"></div>
<div class="answer-feedback" id="answer-feedback"></div> <div class="answer-feedback" id="answer-feedback"></div>
@@ -153,12 +166,37 @@
</div> </div>
</div> </div>
<!-- THEORY: SYSTEM SELECTION -->
<div id="screen-theory-system" class="screen">
<div class="setup-container">
<h2 data-i18n="chooseSystem">Choose numeral system</h2>
<div class="system-group">
<h3 data-i18n="nonPositional">Non-Positional</h3>
<div id="theory-systems-nonpos" class="option-grid"></div>
</div>
<div class="system-group">
<h3 data-i18n="positional">Positional</h3>
<div id="theory-systems-pos" class="option-grid"></div>
</div>
<button class="btn btn-secondary" id="btn-theory-back" data-i18n="back">Back</button>
</div>
</div> </div>
<script type="module" src="app.js?v=35"></script> <!-- THEORY: CONTENT -->
<div id="screen-theory-content" class="screen">
<div class="theory-container">
<h2 id="theory-title"></h2>
<div class="theory-scroll" id="theory-scroll"></div>
<button class="btn btn-secondary" id="btn-theory-content-back" data-i18n="back">Back</button>
</div>
</div>
</div>
<script type="module" src="app.js"></script>
<script> <script>
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js?v=35').catch(() => {}); navigator.serviceWorker.register('/sw.js').catch(() => {});
} }
</script> </script>
</body> </body>
+11
View File
@@ -12,6 +12,17 @@
"src": "icon.svg", "src": "icon.svg",
"sizes": "any", "sizes": "any",
"type": "image/svg+xml" "type": "image/svg+xml"
},
{
"src": "icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
} }
] ]
} }
+69 -16
View File
@@ -14,6 +14,11 @@ export const SYSTEMS = {
short: 'HEX', short: 'HEX',
toDisplay: (n) => n.toString(16).toUpperCase(), toDisplay: (n) => n.toString(16).toUpperCase(),
}, },
base3: {
name: 'Ternary',
short: 'TER',
toDisplay: toBase3,
},
ternary: { ternary: {
name: 'Balanced Ternary', name: 'Balanced Ternary',
short: 'BT3', short: 'BT3',
@@ -60,6 +65,17 @@ function toRoman(num) {
return result; return result;
} }
function toBase3(num) {
if (num === 0) return '0';
let n = Math.abs(num);
let result = '';
while (n > 0) {
result = (n % 3) + result;
n = Math.floor(n / 3);
}
return num < 0 ? '-' + result : result;
}
function toBalTernary(num) { function toBalTernary(num) {
if (num === 0) return '0'; if (num === 0) return '0';
@@ -170,38 +186,68 @@ const HEB_HUNDREDS = ['\u05E7','\u05E8','\u05E9','\u05EA'];
const HEB_GERESH = '\u05F3'; const HEB_GERESH = '\u05F3';
const HEB_GERSHAYIM = '\u05F4'; const HEB_GERSHAYIM = '\u05F4';
function toHebrew(num) { function toHebrewLetters(num) {
if (num <= 0 || num > 499) return String(num);
if (num === 15) return '\u05D8' + HEB_GERSHAYIM + '\u05D5';
if (num === 16) return '\u05D8' + HEB_GERSHAYIM + '\u05D6';
const letters = []; const letters = [];
const h = Math.floor(num / 100); const h = Math.floor(num / 100);
num %= 100; num %= 100;
const t = Math.floor(num / 10); const t = Math.floor(num / 10);
const u = num % 10; const u = num % 10;
if (h === 4) {
letters.push(HEB_HUNDREDS[3]);
if (t > 0) letters.push(HEB_TENS[t - 1]);
if (u > 0) letters.push(HEB_UNITS[u - 1]);
} else if (h >= 5) {
letters.push(HEB_HUNDREDS[3]);
const extra = h - 4;
if (extra > 0) letters.push(HEB_HUNDREDS[extra - 1]);
if (t > 0) letters.push(HEB_TENS[t - 1]);
if (u > 0) letters.push(HEB_UNITS[u - 1]);
} else {
if (h > 0) letters.push(HEB_HUNDREDS[h - 1]); if (h > 0) letters.push(HEB_HUNDREDS[h - 1]);
if (t > 0) letters.push(HEB_TENS[t - 1]); if (t > 0) letters.push(HEB_TENS[t - 1]);
if (u > 0) letters.push(HEB_UNITS[u - 1]); if (u > 0) letters.push(HEB_UNITS[u - 1]);
}
if (letters.length === 0) return '0'; return letters;
if (letters.length === 1) return letters[0] + HEB_GERESH;
return letters.slice(0, -1).join('') + HEB_GERSHAYIM + letters[letters.length - 1];
} }
export function generateChoices(correctAnswer, allRanges, count = 3) { function toHebrew(num) {
if (num <= 0 || num > 9999) return String(num);
if (num === 15) return '\u05D8' + HEB_GERSHAYIM + '\u05D5';
if (num === 16) return '\u05D8' + HEB_GERSHAYIM + '\u05D6';
const thousands = Math.floor(num / 1000);
const rest = num % 1000;
if (thousands > 0 && rest === 0) {
return HEB_UNITS[thousands - 1] + HEB_GERESH;
}
let result = '';
if (thousands > 0) {
result = HEB_UNITS[thousands - 1] + HEB_GERESH;
}
const letters = toHebrewLetters(rest);
if (letters.length === 0) return result;
if (letters.length === 1) return result + letters[0] + HEB_GERESH;
return result + letters.slice(0, -1).join('') + HEB_GERSHAYIM + letters[letters.length - 1];
}
export function generateChoices(correctAnswer, allRanges, count = 3, matchLastDigit = false) {
const choices = new Set([correctAnswer]); const choices = new Set([correctAnswer]);
const [min, max] = allRanges; const [min, max] = allRanges;
const useLastDigit = matchLastDigit && correctAnswer > 10;
let attempts = 0; let attempts = 0;
while (choices.size < count && attempts < 100) { while (choices.size < count && attempts < 100) {
let wrong; let wrong;
const offset = Math.floor(Math.random() * 20) + 1; if (useLastDigit) {
if (Math.random() < 0.5) { const offset = (Math.floor(Math.random() * 20) + 1) * 10;
wrong = correctAnswer + offset; wrong = Math.random() < 0.5 ? correctAnswer + offset : correctAnswer - offset;
} else { } else {
wrong = correctAnswer - offset; const offset = Math.floor(Math.random() * 20) + 1;
wrong = Math.random() < 0.5 ? correctAnswer + offset : correctAnswer - offset;
} }
if (wrong >= min && wrong <= max && wrong !== correctAnswer) { if (wrong >= min && wrong <= max && wrong !== correctAnswer) {
choices.add(wrong); choices.add(wrong);
@@ -209,7 +255,14 @@ export function generateChoices(correctAnswer, allRanges, count = 3) {
attempts++; attempts++;
} }
while (choices.size < count) { while (choices.size < count) {
const r = min + Math.floor(Math.random() * (max - min + 1)); let r;
if (useLastDigit) {
do {
r = min + Math.floor(Math.random() * (max - min + 1));
} while (r % 10 !== correctAnswer % 10 && choices.size < count + 50);
} else {
r = min + Math.floor(Math.random() * (max - min + 1));
}
choices.add(r); choices.add(r);
} }
const arr = Array.from(choices); const arr = Array.from(choices);
+221 -13
View File
@@ -10,15 +10,61 @@
--bg-card: #0f3460; --bg-card: #0f3460;
--accent: #e94560; --accent: #e94560;
--accent-hover: #ff6b81; --accent-hover: #ff6b81;
--accent-secondary: #ff9a56;
--text: #eaeaea; --text: #eaeaea;
--text-dim: #a0a0b0; --text-dim: #a0a0b0;
--correct: #2ecc71; --correct: #2ecc71;
--incorrect: #e74c3c; --incorrect: #e74c3c;
--border: rgba(255,255,255,0.08);
--border-radius: 12px; --border-radius: 12px;
--safe-top: env(safe-area-inset-top); --safe-top: env(safe-area-inset-top);
--safe-bottom: env(safe-area-inset-bottom); --safe-bottom: env(safe-area-inset-bottom);
} }
@media (prefers-color-scheme: light) {
:root {
--bg: #f5f5f5;
--bg-light: #ffffff;
--bg-card: #ffffff;
--accent: #e94560;
--accent-hover: #d63851;
--accent-secondary: #f09040;
--text: #1a1a2e;
--text-dim: #555566;
--correct: #27ae60;
--incorrect: #c0392b;
--border: #d0d0d0;
}
}
html[data-theme="dark"] {
--bg: #1a1a2e;
--bg-light: #16213e;
--bg-card: #0f3460;
--accent: #e94560;
--accent-hover: #ff6b81;
--accent-secondary: #ff9a56;
--text: #eaeaea;
--text-dim: #a0a0b0;
--correct: #2ecc71;
--incorrect: #e74c3c;
--border: rgba(255,255,255,0.08);
}
html[data-theme="light"] {
--bg: #f5f5f5;
--bg-light: #ffffff;
--bg-card: #ffffff;
--accent: #e94560;
--accent-hover: #d63851;
--accent-secondary: #f09040;
--text: #1a1a2e;
--text-dim: #555566;
--correct: #27ae60;
--incorrect: #c0392b;
--border: #d0d0d0;
}
@font-face { @font-face {
font-family: 'Ponomar'; font-family: 'Ponomar';
src: url('Ponomar-Regular.woff') format('woff'); src: url('Ponomar-Regular.woff') format('woff');
@@ -27,13 +73,31 @@
font-display: swap; font-display: swap;
} }
@font-face {
font-family: 'Noto Sans Symbols 2';
src: url('NotoSansSymbols2-subset.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
unicode-range: U+232B, U+23F1, U+2600, U+2713, U+2717, U+2801, U+2803, U+2809-280B, U+2811, U+2813, U+2819-281B, U+283C, U+2B95;
}
@font-face {
font-family: 'Noto Sans Symbols 2';
src: url('NotoSansMath-subset.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
unicode-range: U+2295-2296, U+263E;
}
html, body { html, body {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans Symbols 2', sans-serif;
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
@@ -72,7 +136,7 @@ body {
.logo { .logo {
font-size: 3.5rem; font-size: 3.5rem;
font-weight: 800; font-weight: 800;
background: linear-gradient(135deg, var(--accent), #ff9a56); background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
@@ -98,23 +162,53 @@ body {
.lang-select { .lang-select {
background: var(--bg-light); background: var(--bg-light);
color: var(--text); color: var(--text);
border: 2px solid transparent; border: 2px solid var(--border);
border-radius: 8px; border-radius: 8px;
padding: 10px 16px; padding: 10px 16px;
font-size: 0.95rem; font-size: 0.95rem;
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
max-width: 320px; max-width: 320px;
margin: 0 auto;
display: block;
}
.menu-controls {
display: flex;
gap: 8px;
width: 100%;
max-width: 320px;
}
.menu-controls .lang-select {
flex: 1;
width: auto;
max-width: none;
margin: 0;
}
.theme-toggle {
width: 44px;
height: 44px;
flex-shrink: 0;
background: var(--bg-light);
color: var(--text);
border: 2px solid var(--border);
border-radius: 8px;
font-size: 1.3rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
} }
.ticker { .ticker {
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
color: var(--text-dim); color: var(--text-dim);
font-size: 1.05rem; font-size: 1.3rem;
font-family: monospace; font-family: 'Noto Sans Symbols 2', monospace;
animation: ticker-scroll 40s linear infinite; animation: ticker-scroll 40s linear infinite;
padding-left: 100%;
} }
@keyframes ticker-scroll { @keyframes ticker-scroll {
@@ -149,6 +243,7 @@ body {
.btn-secondary { .btn-secondary {
background: var(--bg-card); background: var(--bg-card);
color: var(--text); color: var(--text);
border: 1px solid var(--border);
} }
.btn-large { .btn-large {
@@ -204,7 +299,7 @@ body {
.option-btn { .option-btn {
padding: 14px 12px; padding: 14px 12px;
background: var(--bg-light); background: var(--bg-light);
border: 2px solid transparent; border: 2px solid var(--border);
border-radius: 8px; border-radius: 8px;
color: var(--text); color: var(--text);
font-size: 0.95rem; font-size: 0.95rem;
@@ -248,19 +343,30 @@ body {
align-items: center; align-items: center;
padding: 16px 20px; padding: 16px 20px;
background: var(--bg-light); background: var(--bg-light);
border-bottom: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
position: relative; position: relative;
} }
.score-display { .score-display {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 700; font-weight: 700;
} }
#score-system { .score-correct {
margin-left: 8px; color: var(--correct);
margin-left: 16px;
}
.score-incorrect { color: var(--incorrect); }
.system-range-label {
text-align: center;
font-size: 1rem;
color: var(--text-dim); color: var(--text-dim);
font-size: 0.9rem; padding: 8px 20px 0;
} }
.timer-area { .timer-area {
@@ -366,6 +472,18 @@ body {
font-size: 3.2rem; font-size: 3.2rem;
} }
.number-display.mono {
font-family: 'Noto Sans Symbols 2', monospace;
}
.ticker .slavonic-font {
font-size: 1.5rem;
}
.result-system.slavonic-font {
font-size: 1.6rem;
}
.answer-feedback { .answer-feedback {
min-height: 40px; min-height: 40px;
font-size: 1.1rem; font-size: 1.1rem;
@@ -396,7 +514,7 @@ body {
.choice-btn { .choice-btn {
padding: 18px 20px; padding: 18px 20px;
background: var(--bg-card); background: var(--bg-card);
border: 2px solid transparent; border: 2px solid var(--border);
border-radius: var(--border-radius); border-radius: var(--border-radius);
color: var(--text); color: var(--text);
font-size: 1.3rem; font-size: 1.3rem;
@@ -438,7 +556,7 @@ body {
padding: 16px; padding: 16px;
background: var(--bg-card); background: var(--bg-card);
border-radius: 8px; border-radius: 8px;
min-height: 60px; height: 60px;
margin-bottom: 16px; margin-bottom: 16px;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -457,7 +575,7 @@ body {
.numpad-key { .numpad-key {
padding: 18px; padding: 18px;
background: var(--bg-light); background: var(--bg-light);
border: none; border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
color: var(--text); color: var(--text);
font-size: 1.4rem; font-size: 1.4rem;
@@ -523,6 +641,9 @@ body {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
color: var(--correct); color: var(--correct);
}
.results-congrats:not(:empty) {
margin-bottom: 16px; margin-bottom: 16px;
min-height: 48px; min-height: 48px;
} }
@@ -549,6 +670,7 @@ body {
padding: 14px 16px; padding: 14px 16px;
background: var(--bg-light); background: var(--bg-light);
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--border);
border-left: 4px solid var(--incorrect); border-left: 4px solid var(--incorrect);
} }
@@ -563,6 +685,7 @@ body {
.result-arrow { .result-arrow {
color: var(--text-dim); color: var(--text-dim);
flex-shrink: 0; flex-shrink: 0;
font-size: 1.4rem;
} }
.result-decimal { .result-decimal {
@@ -571,6 +694,13 @@ body {
color: var(--correct); color: var(--correct);
} }
.result-wrong {
font-size: 1.2rem;
font-weight: 600;
color: var(--incorrect);
margin-left: auto;
}
.no-mistakes { .no-mistakes {
text-align: center; text-align: center;
color: var(--correct); color: var(--correct);
@@ -614,6 +744,12 @@ body {
pointer-events: none; pointer-events: none;
} }
.answer-confetti-burst {
position: fixed;
pointer-events: none;
z-index: 1000;
}
.confetti-particle { .confetti-particle {
position: absolute; position: absolute;
font-size: 1.4rem; font-size: 1.4rem;
@@ -630,3 +766,75 @@ body {
opacity: 0; opacity: 0;
} }
} }
/* THEORY */
.theory-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 24px;
padding-top: calc(24px + var(--safe-top));
padding-bottom: calc(24px + var(--safe-bottom));
max-width: 600px;
margin: 0 auto;
width: 100%;
}
.theory-container h2 {
flex-shrink: 0;
margin-bottom: 16px;
}
.theory-scroll {
flex: 1;
overflow-y: auto;
padding-right: 8px;
}
.theory-scroll h3 {
color: var(--accent);
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 1px;
margin: 24px 0 8px;
}
.theory-scroll h3:first-child {
margin-top: 0;
}
.theory-scroll p {
line-height: 1.6;
margin-bottom: 12px;
color: var(--text);
}
.theory-scroll .theory-example {
background: var(--bg-card);
border-radius: 8px;
padding: 16px;
font-family: 'Noto Sans Symbols 2', monospace;
font-size: 1.1rem;
margin: 12px 0;
text-align: center;
letter-spacing: 2px;
}
.theory-unavailable {
text-align: center;
opacity: 0.65;
padding: 24px 0;
}
.theory-scroll .slavonic-font {
font-family: 'Ponomar', serif;
font-size: 1.4rem;
}
#btn-theory-content-back {
flex-shrink: 0;
margin-top: 16px;
max-width: 320px;
margin-left: auto;
margin-right: auto;
}
+35 -2
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = 'numnum-v35'; const CACHE_NAME = 'numnum-v28';
const ASSETS = [ const ASSETS = [
'/', '/',
'/index.html', '/index.html',
@@ -7,11 +7,44 @@ const ASSETS = [
'/numerals.js', '/numerals.js',
'/manifest.json', '/manifest.json',
'/icon.svg', '/icon.svg',
'/icon-192.png',
'/icon-512.png',
'/Ponomar-Regular.woff',
'/NotoSansSymbols2-subset.woff2',
'/NotoSansMath-subset.woff2',
'/theory/base3.en.html',
'/theory/base3.ru.html',
'/theory/binary.en.html',
'/theory/binary.ru.html',
'/theory/braille.en.html',
'/theory/braille.ru.html',
'/theory/greek.en.html',
'/theory/greek.ru.html',
'/theory/hebrew.en.html',
'/theory/hebrew.ru.html',
'/theory/hex.en.html',
'/theory/hex.ru.html',
'/theory/octal.en.html',
'/theory/octal.ru.html',
'/theory/roman.en.html',
'/theory/roman.ru.html',
'/theory/slavonic.en.html',
'/theory/slavonic.ru.html',
'/theory/ternary.en.html',
'/theory/ternary.ru.html',
]; ];
self.addEventListener('install', (e) => { self.addEventListener('install', (e) => {
e.waitUntil( e.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS)) caches.open(CACHE_NAME).then(cache =>
Promise.allSettled(ASSETS.map(url => cache.add(url)))
).then(results => {
results.forEach((r, i) => {
if (r.status === 'rejected') {
console.warn('SW precache failed:', ASSETS[i], r.reason);
}
});
})
); );
self.skipWaiting(); self.skipWaiting();
}); });
+8
View File
@@ -0,0 +1,8 @@
<h3>Base: 3</h3>
<p>The ternary (base-3) numeral system uses three digits: 0, 1, and 2. Each position represents a power of 3.</p>
<h3>How it works</h3>
<div class="theory-example">112 = 1×9 + 1×3 + 2×1 = 14</div>
<h3>Digits</h3>
<div class="theory-example">0 1 2</div>
<h3>Uses</h3>
<p>Ternary systems are used in some specialized computing applications (ternary logic). Some argue that base-3 is the most efficient integer base for computation, as it is the closest to the mathematical constant e ≈ 2.718.</p>
+8
View File
@@ -0,0 +1,8 @@
<h3>Троичная</h3>
<p>Троичная (несимметрично-троичная) система счисления использует три цифры: 0, 1 и 2. Каждая позиция представляет степень тройки.</p>
<h3>Как работает</h3>
<div class="theory-example">112 = 1×9 + 1×3 + 2×1 = 14</div>
<h3>Цифры</h3>
<div class="theory-example">0 1 2</div>
<h3>Применение</h3>
<p>Троичная система используется в некоторых специализированных вычислениях (троичная логика). Некоторые считают, что троичная система является наиболее эффективной для вычислений, поскольку близка к математической константе e ≈ 2,718.</p>
+9
View File
@@ -0,0 +1,9 @@
<h3>Base: 2</h3>
<p>The binary (base-2) numeral system uses only two digits: 0 and 1. It is the fundamental language of digital computers, where each digit represents a single bit — an electrical signal that is either off (0) or on (1).</p>
<h3>How it works</h3>
<p>Each position represents a power of 2, increasing from right to left:</p>
<div class="theory-example">1011 = 1×8 + 0×4 + 1×2 + 1×1 = 11</div>
<h3>Digits</h3>
<div class="theory-example">0 1</div>
<h3>Uses</h3>
<p>Binary is used internally by all modern computers. File storage, memory addresses, and network protocols all rely on binary representation at the lowest level.</p>
+9
View File
@@ -0,0 +1,9 @@
<h3>Двоичная система</h3>
<p>Двоичная система счисления использует только две цифры: 0 и 1. Это основной язык цифровых компьютеров, где каждый бит представляет электрический сигнал — выключен (0) или включен (1).</p>
<h3>Как работает</h3>
<p>Каждая позиция представляет степень двойки, увеличиваясь справа налево:</p>
<div class="theory-example">1011 = 1×8 + 0×4 + 1×2 + 1×1 = 11</div>
<h3>Цифры</h3>
<div class="theory-example">0 1</div>
<h3>Применение</h3>
<p>Двоичная система используется внутри всеми современными компьютерами. Хранение файлов, адреса памяти и сетевые протоколы работают на двоичном представлении на нижнем уровне.</p>
+9
View File
@@ -0,0 +1,9 @@
<h3>Braille Decimal</h3>
<p>Braille is a tactile writing system used by people who are visually impaired. Each digit is represented by a specific pattern of raised dots within a 2×3 cell. In this app, the Braille decimal system encodes digits 09.</p>
<h3>Pattern</h3>
<p>Each digit is preceded by the number indicator (⠼), which signals that the following characters represent numbers rather than letters.</p>
<h3>Digits (09)</h3>
<div class="theory-example">⠼⠚ ⠼⠁ ⠼⠃ ⠼⠉ ⠼⠙ ⠼⠑ ⠼⠋ ⠼⠛ ⠼⠓ ⠼⠊</div>
<p>These correspond to: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9</p>
<h3>Uses</h3>
<p>Braille numerals are used in Braille books, signage, and electronic Braille displays. The number sign distinguishes numeric content from alphabetic text.</p>
+9
View File
@@ -0,0 +1,9 @@
<h3>Десятичная система, шрифт Брайля</h3>
<p>Шрифт Брайля — это тактильная система письма, используемая людьми с нарушениями зрения. Каждая цифра представлена специальным паттерном выпуклых точек в ячейке 2×3. Цифры обозначаются теми же знаками, что и первые десять букв алфавита (A-K), но ей предшествует индикатор номера.</p>
<h3>Структура</h3>
<p>Каждая цифра предварена индикатором номера (⠼), который сигнализирует, что следующие символы являются числами.</p>
<h3>Цифры (09)</h3>
<div class="theory-example">⠼⠚ ⠼⠁ ⠼⠃ ⠼⠉ ⠼⠙ ⠼⠑ ⠼⠋ ⠼⠛ ⠼⠓ ⠼⠊</div>
<p>Соответствуют: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9</p>
<h3>Применение</h3>
<p>Шрифтовые числа используются в книгах и информационных табличках для незрячих, на досках и электронных брайлевских дисплеях.</p>
+15
View File
@@ -0,0 +1,15 @@
<h3>Greek Numerals</h3>
<p>Greek numerals are a decimal system that uses letters of the Greek alphabet to represent numbers. Known as Ionian or alphabetic numerals, this system was adopted around the 4th century BC.</p>
<h3>How it works</h3>
<p>Letters are assigned values from 1 to 9 (units), 10 to 90 (tens), and 100 to 900 (hundreds). A special mark (, keraia) is placed after the number. Thousands are indicated by a subscript mark (͵) before the units letter.</p>
<h3>Units (19)</h3>
<div class="theory-example">α β γ δ ε ϛ ζ η θ</div>
<p>(α=1, β=2, γ=3, δ=4, ε=5, ϛ=6, ζ=7, η=8, θ=9)</p>
<h3>Tens (1090)</h3>
<div class="theory-example">ι κ λ μ ν ξ ο π ϟ</div>
<p>(ι=10, κ=20, λ=30, μ=40, ν=50, ξ=60, ο=70, π=80, ϟ=90)</p>
<h3>Hundreds (100900)</h3>
<div class="theory-example">ρ σ τ υ φ χ ψ ω ϡ</div>
<p>(ρ=100, σ=200, τ=300, υ=400, φ=500, χ=600, ψ=700, ω=800, ϡ=900)</p>
<h3>Example</h3>
<div class="theory-example">ηπε = 8 + 80 + 5 = 93</div>
+18
View File
@@ -0,0 +1,18 @@
<h3>Греческие числа</h3>
<p>Греческие числа — алфавитная система записи десятичных чисел, использующая буквы греческого алфавита. Известна как ионическая или новогреческая, была популярна с IV-III века до н.э. и стала образцом для других алфавитных систем, в частности церковнославянской и еврейской</p>
<h3>Как работает</h3>
<p>Буквам присваиваются значения от 1 до 9 (единицы), 10 до 90 (десятки) и 100 до 900 (сотни). Число завершается символом кераия (´). Тысячи обозначаются индиксом (͵) перед буквой единиц.</p>
<h3>Единицы (19)</h3>
<div class="theory-example">α β γ δ ε ϛ ζ η θ</div>
<p>(α=1, β=2, γ=3, δ=4, ε=5, ϛ=6, ζ=7, η=8, θ=9)</p>
<h3>Десятки (1090)</h3>
<div class="theory-example">ι κ λ μ ν ξ ο π ϟ</div>
<p>(ι=10, κ=20, λ=30, μ=40, ν=50, ξ=60, ο=70, π=80, ϟ=90)</p>
<h3>Сотни (100900)</h3>
<div class="theory-example">ρ σ τ υ φ χ ψ ω ϡ</div>
<p>(ρ=100, σ=200, τ=300, υ=400, φ=500, χ=600, ψ=700, ω=800, ϡ=900)</p>
<h3>Пример</h3>
<div class="theory-example">ηπε = 8 + 80 + 5 = 93</div>
<h3>Применение</h3>
<p>До сих пор иногда используется для записи ординальных чисел в Греции.</p>
+22
View File
@@ -0,0 +1,22 @@
<h3>Hebrew Numerals</h3>
<p>Hebrew numerals are an alphanumeric system where letters of the Hebrew alphabet are assigned numerical values. Known as "gematria," this system has been used for centuries in Jewish tradition.</p>
<h3>How it works</h3>
<p>Letters represent values from 19 (units), 1090 (tens), and 100400 (hundreds). Numbers are written by combining letters, with the larger values first. A single apostrophe (geresh, ׳) follows single-letter numbers, and a double apostrophe (gershayim, ״) is placed before the last letter of multi-letter numbers.</p>
<h3>Special cases</h3>
<p>15 and 16 use טו and טז instead of יה and יו to avoid writing God's name.</p>
<h3>Units (19)</h3>
<div class="theory-example">א ב ג ד ה ו ז ח ט</div>
<p>(א=1, ב=2, ג=3, ד=4, ה=5, ו=6, ז=7, ח=8, ט=9)</p>
<h3>Tens (1090)</h3>
<div class="theory-example">י כ ל מ נ ס ע פ צ</div>
<p>(י=10, כ=20, ל=30, מ=40, נ=50, ס=60, ע=70, פ=80, צ=90)</p>
<h3>Hundreds (100400)</h3>
<div class="theory-example">ק ר ש ת</div>
<p>(ק=100, ר=200, ש=300, ת=400)</p>
<h3>Beyond 400</h3>
<p>Numbers above 400 are written additively by combining tav (400) with hundreds:</p>
<div class="theory-example">500 = ת״ק = 400 + 100<br>900 = תת״ק = 400 + 400 + 100</div>
<p>Thousands are written as the units letter followed by geresh:</p>
<div class="theory-example">1000 = א׳<br>5000 = ה׳</div>
<h3>Example</h3>
<div class="theory-example">5786 = ה׳תשפ״ו = 5000 + 400 + 300 + 80 + 6</div>
+22
View File
@@ -0,0 +1,22 @@
<h3>Еврейские числа</h3>
<p>Еврейские числа — алфавитная система, в которой буквам алфавита присваиваются числовые значения. Известна как гематрия, эта система является адаптацией греческой записи чисел и получила распространение около II в до н.э., заменив более древние системы на базе Арамейского и Финикийского алфаитов.</p>
<h3>Как работает</h3>
<p>Буквы представляют значения 1–9 (единицы), 10–90 (десятки) и 100–400 (сотни). Числа пишутся комбинацией букв, по правилу от больших к меньшим. Однобуквенные числа завершаются герешем (׳), многобуквенные — гершайми (״) перед последней буквой.</p>
<h3>Специальные случаи</h3>
<p>15 и 16 пишутся как טו и טז вместо יה и יו, чтобы не писать имя Б-га.</p>
<h3>Единицы (19)</h3>
<div class="theory-example">א ב ג ד ה ו ז ח ט</div>
<p>(א=1, ב=2, ג=3, ד=4, ה=5, ו=6, ז=7, ח=8, ט=9)</p>
<h3>Десятки (1090)</h3>
<div class="theory-example">י כ ל מ נ ס ע פ צ</div>
<p>(י=10, כ=20, ל=30, מ=40, נ=50, ס=60, ע=70, פ=80, צ=90)</p>
<h3>Сотни (100400)</h3>
<div class="theory-example">ק ר ש ת</div>
<p>(ק=100, ר=200, ש=300, ת=400)</p>
<h3>Свыше 400</h3>
<p>Числа более 400 записываются аддитивно сочетанием ת (400) с сотными:</p>
<div class="theory-example">500 = ת״ק = 400 + 100<br>900 = תת״ק = 400 + 400 + 100</div>
<p>Тысячи обозначаются буквой единиц с герешом:</p>
<div class="theory-example">1000 = א׳<br>5000 = ה׳</div>
<h3>Пример</h3>
<div class="theory-example">5786 = ה׳תשפ״ו = 5000 + 400 + 300 + 80 + 6</div>
+8
View File
@@ -0,0 +1,8 @@
<h3>Base: 16</h3>
<p>The hexadecimal (base-16) numeral system uses sixteen symbols: the digits 09 and the letters AF (representing values 1015).</p>
<h3>How it works</h3>
<div class="theory-example">1A3 = 1×256 + 10×16 + 3×1 = 419</div>
<h3>Digits</h3>
<div class="theory-example">0 1 2 3 4 5 6 7 8 9 A B C D E F</div>
<h3>Uses</h3>
<p>Hexadecimal is ubiquitous in computing. Memory addresses, color codes in CSS (#E94560), MAC addresses, and file hashes all use hex because each hex digit maps to exactly four binary digits, making it a compact and human-readable representation of binary data.</p>
+8
View File
@@ -0,0 +1,8 @@
<h3>Шестнадцатеричная система</h3>
<p>Шестнадцатеричная система счисления использует шестнадцать символов: цифры 0–9 и буквы A–F (значения 10–15).</p>
<h3>Как работает</h3>
<div class="theory-example">1A3 = 1×256 + 10×16 + 3×1 = 419</div>
<h3>Цифры</h3>
<div class="theory-example">0 1 2 3 4 5 6 7 8 9 A B C D E F</div>
<h3>Применение</h3>
<p>Шестнадцатеричная система повсеместно используется в компьютерной технике для компактной записи значений байт, поскольку каждая шестнадцатерная цифра соответствует четырем двоичным или полубайту.</p>
+8
View File
@@ -0,0 +1,8 @@
<h3>Base: 8</h3>
<p>The octal (base-8) numeral system uses eight digits: 0 through 7. Each position represents a power of 8.</p>
<h3>How it works</h3>
<div class="theory-example">177 = 1×64 + 7×8 + 7×1 = 127</div>
<h3>Digits</h3>
<div class="theory-example">0 1 2 3 4 5 6 7</div>
<h3>Uses</h3>
<p>Octal was widely used in early computing (e.g. PDP-8, IBM mainframes) because it provides a compact way to represent binary numbers: each octal digit maps to exactly three binary digits. Today it appears in Unix file permissions (chmod 755) and some legacy systems.</p>
+8
View File
@@ -0,0 +1,8 @@
<h3>Восьмеричная система</h3>
<p>Восьмеричная система счисления использует восемь цифр: 0–7. Каждая позиция представляет степень восьми.</p>
<h3>Как работает</h3>
<div class="theory-example">177 = 1×64 + 7×8 + 7×1 = 127</div>
<h3>Цифры</h3>
<div class="theory-example">0 1 2 3 4 5 6 7</div>
<h3>Применение</h3>
<p>Восьмеричная система была распространена в раннем программировании (например PDP-8, главные ЭВМ IBM), т.к. каждая восьмеричная цифра соответствует трем двоичным. Сейчас встречается в правах доступа к файлам Unix (chmod 755) и некоторых устаревших системах.</p>
+11
View File
@@ -0,0 +1,11 @@
<h3>Roman Numerals</h3>
<p>Roman numerals originated in ancient Rome and remained the standard way of writing numbers throughout Europe well into the Middle Ages. They use combinations of letters from the Latin alphabet.</p>
<h3>Symbols</h3>
<div class="theory-example">I = 1 V = 5 X = 10 L = 50 C = 100 D = 500 M = 1000</div>
<h3>How it works</h3>
<p>Roman numerals are primarily additive: values are summed left to right. However, a smaller value placed before a larger one indicates subtraction:</p>
<div class="theory-example">IV = 5 1 = 4 IX = 10 1 = 9<br>XL = 50 10 = 40 XC = 100 10 = 90 CM = 1000 100 = 900</div>
<h3>Examples</h3>
<div class="theory-example">XLII = 40 + 2 = 42<br><br>MCMXCIV = 1000 + 900 + 90 + 4 = 1994</div>
<h3>Rules</h3>
<p>Roman numerals can represent numbers from 1 to 3999. I, X, C, and M can be repeated up to three times. V, L, and D are never repeated. The subtractive combinations IV, IX, XL, XC, CM, and CD are the only allowed subtractions.</p>
+11
View File
@@ -0,0 +1,11 @@
<h3>Римские числа</h3>
<p>Римские числа возникли в древнем Риме и оставались стандартным способом записи чисел в Европе до Средневековья. Они используют буквы латинского алфавита.</p>
<h3>Символы</h3>
<div class="theory-example">I = 1 V = 5 X = 10 L = 50 C = 100 D = 500 M = 1000</div>
<h3>Как работает</h3>
<p>Римские числа преимущественно аддитивны: значения складываются слева направо. Однако меньшая цифра перед большей означает вычитание:</p>
<div class="theory-example">IV = 5 1 = 4 IX = 10 1 = 9<br>XL = 50 10 = 40 XC = 100 10 = 90 CM = 1000 100 = 900</div>
<h3>Примеры</h3>
<div class="theory-example">XLII = 40 + 2 = 42<br><br>MCMXCIV = 1000 + 900 + 90 + 4 = 1994</div>
<h3>Правила</h3>
<p>Римские числа могут представлять числа от 1 до 3999. I, X, C и M могут повторяться до трех раз. V, L и D никогда не повторяются.</p>
+15
View File
@@ -0,0 +1,15 @@
<h3>Church Slavonic Numerals</h3>
<p>Church Slavonic numerals are a Cyrillic alphanumeric system used historically in Russian and other Slavic Orthodox cultures. Letters of the early Cyrillic alphabet are assigned numerical values.</p>
<h3>How it works</h3>
<p>Like Greek numerals, Church Slavonic uses separate letter groups for units (19), tens (1090), and hundreds (100900). A titlo mark (<span class="slavonic-font"> ҃</span>) is placed above one of the letters to indicate it is a number. Thousands are marked with a special symbol (<span class="slavonic-font">҂</span>) placed before the number. The number ends with a period.</p>
<h3>Units (19)</h3>
<div class="theory-example slavonic-font">а в г д є ѕ з и ѳ</div>
<p>(<span class="slavonic-font">а</span>=1, <span class="slavonic-font">в</span>=2, <span class="slavonic-font">г</span>=3, <span class="slavonic-font">д</span>=4, <span class="slavonic-font">є</span>=5, <span class="slavonic-font">ѕ</span>=6, <span class="slavonic-font">з</span>=7, <span class="slavonic-font">и</span>=8, <span class="slavonic-font">ѳ</span>=9)</p>
<h3>Tens (1090)</h3>
<div class="theory-example slavonic-font">і к л м н ѯ ѻ п ч</div>
<p>(<span class="slavonic-font">і</span>=10, <span class="slavonic-font">к</span>=20, <span class="slavonic-font">л</span>=30, <span class="slavonic-font">м</span>=40, <span class="slavonic-font">н</span>=50, <span class="slavonic-font">ѯ</span>=60, <span class="slavonic-font">ѻ</span>=70, <span class="slavonic-font">п</span>=80, <span class="slavonic-font">ч</span>=90)</p>
<h3>Hundreds (100900)</h3>
<div class="theory-example slavonic-font">р с т у ф х ѱ Ѿ ц</div>
<p>(<span class="slavonic-font">р</span>=100, <span class="slavonic-font">с</span>=200, <span class="slavonic-font">т</span>=300, <span class="slavonic-font">у</span>=400, <span class="slavonic-font">ф</span>=500, <span class="slavonic-font">х</span>=600, <span class="slavonic-font">ѱ</span>=700, <span class="slavonic-font">Ѿ</span>=800, <span class="slavonic-font">ц</span>=900)</p>
<h3>Example</h3>
<div class="theory-example slavonic-font">цп҃з. = 900 + 80 + 3 = 983</div>
+15
View File
@@ -0,0 +1,15 @@
<h3>Церковнославянские числа</h3>
<p>Церковнославянские числа — кириллическая цифровая система, использующаяся в русской и других славянских православных культурах. Буквы древней кириллической азбуки, имеющие греческие аналоги, имеют числовые значения, равные значениям в греческой системе записи.</p>
<h3>Как работает</h3>
<p>Используются отдельные группы букв для единиц (1–9), десятков (10–90) и сотен (100–900). Над буквой ставится титло (<span class="slavonic-font"> ҃</span>). Тысячи обозначаются специальным знаком (<span class="slavonic-font">҂</span>). Число завершается точкой.</p>
<h3>Единицы (19)</h3>
<div class="theory-example slavonic-font">а в г д є ѕ з и ѳ</div>
<p>(<span class="slavonic-font">а</span>=1, <span class="slavonic-font">в</span>=2, <span class="slavonic-font">г</span>=3, <span class="slavonic-font">д</span>=4, <span class="slavonic-font">є</span>=5, <span class="slavonic-font">ѕ</span>=6, <span class="slavonic-font">з</span>=7, <span class="slavonic-font">и</span>=8, <span class="slavonic-font">ѳ</span>=9)</p>
<h3>Десятки (1090)</h3>
<div class="theory-example slavonic-font">і к л м н ѯ ѻ п ч</div>
<p>(<span class="slavonic-font">і</span>=10, <span class="slavonic-font">к</span>=20, <span class="slavonic-font">л</span>=30, <span class="slavonic-font">м</span>=40, <span class="slavonic-font">н</span>=50, <span class="slavonic-font">ѯ</span>=60, <span class="slavonic-font">ѻ</span>=70, <span class="slavonic-font">п</span>=80, <span class="slavonic-font">ч</span>=90)</p>
<h3>Сотни (100900)</h3>
<div class="theory-example slavonic-font">р с т у ф х ѱ Ѿ ц</div>
<p>(<span class="slavonic-font">р</span>=100, <span class="slavonic-font">с</span>=200, <span class="slavonic-font">т</span>=300, <span class="slavonic-font">у</span>=400, <span class="slavonic-font">ф</span>=500, <span class="slavonic-font">х</span>=600, <span class="slavonic-font">ѱ</span>=700, <span class="slavonic-font">Ѿ</span>=800, <span class="slavonic-font">ц</span>=900)</p>
<h3>Пример</h3>
<div class="theory-example slavonic-font">цп҃з. = 900 + 80 + 3 = 983</div>
+11
View File
@@ -0,0 +1,11 @@
<h3>Balanced Ternary: Base 3 with signed digits</h3>
<p>Balanced ternary is a positional Base 3 system where non-zero digits are signed: 1, 0, or +1. Instead of using the digits -1 and +1, any suitable symbols can be used, we use circled - and +:</p>
<h3>Digits</h3>
<div class="theory-example">⊖ = 1, 0 = 0, ⊕ = +1</div>
<p>⊖ (Circled Minus) represents 1, and ⊕ (Circled Plus) represents +1.</p>
<h3>How it works</h3>
<p>Each position represents a power of 3, but positions can be negative:</p>
<div class="theory-example">⊕0⊖ = (+1)×9 + 0×3 + (1)×1 = 8</div>
<div class="theory-example">⊖⊕0 = (-1)×9 + (+1)×3 + 0×1 = -6</div>
<h3>Properties</h3>
<p>Balanced ternary has the unique property that the sign of a number can be determined from its most significant non-zero digit, eliminating the need for a separate sign. For negative numbers, the signs of all non-zero digits are flipped (⊕ ↔ ⊖). The system was studied by Leonardo Pisano (Fibonacci, 11701250) in connection with the weight problem — using weights of powers of 3, any integer mass from 1 can be measured with the fewest weights. In 1958, the Soviet engineer Nikolai Brusentsov built the Setun computer at Moscow State University, which used balanced ternary arithmetic and became one of the most elegant ternary computers.</p>
+11
View File
@@ -0,0 +1,11 @@
<h3>Симметричная троичная система</h3>
<p>Симметричная (уравновешенная) троичная система — это позиционная система по основанию три, использующая цифры со значениями −1, 0 или +1. Значения -1 и +1 для удобства могут обозначаться по-разному, любыми подходящими симполами, стандарта не существует. Мы используем знаки "-" и "+" в окружностях:</p>
<h3>Цифры</h3>
<div class="theory-example">⊖ = 1, 0 = 0, ⊕ = +1</div>
<p>⊖ (Кружок с минусом) обозначает −1, а ⊕ (Кружок с плюсом) — +1.</p>
<h3>Как работает</h3>
<p>Каждая позиция представляет степень тройки, значение в позиции становится отрицательным, если в нем стоит -1:</p>
<div class="theory-example">⊕0⊖ = (+1)×9 + 0×3 + (1)×1 = 8</div>
<div class="theory-example">⊖⊕0 = (-1)×9 + (+1)×3 + 0×1 = -6</div>
<h3>Свойства</h3>
<p>Самая экономичная среди целочисленных позиционных систем. В симметричной троичной системе знак числа определяется по первой ненулевой цифре старшего разряда, что устраняет необходимость использовать отдельный знак для отрицательных чисел. Для смены знака числа ненулевые цифры инвертируются (⊕ ↔ ⊖). Эту систему изучал Леонардо Писано (Фибоначчи, 1170–1250) в связи с задачей о гирях: используя гири массой со степенями тройки, любую целую массу можно отвесить минимальным числом гирь. В 1958 году советский инженер Николай Брусенцов спроектировал ЭВМ Сетунь, который использовал симметрично-троичную арифметику и стал одним из самых элегантных троичных компьютеров.</p>