Шахматные часы с тремя режимами контроля времени: различия между версиями

Материал из WikiPrometheus.ru
Перейти к навигацииПерейти к поиску
Удалено перенаправление на Трёхрежимные шахматные часы
Метки: удалено перенаправление через визуальный редактор
 
(не показано 6 промежуточных версий этого же участника)
Строка 1: Строка 1:
== Общие сведения ==
*Шахматы — это не только стратегия и логика, но и строгий регламент. В соревнованиях любого уровня действует контроль времени: у каждого игрока есть лимит на партию, и превысить его нельзя. Для этого существуют специальные шахматные часы. Они фиксируют ходы, переключают время между соперниками и подают сигнал об окончании партии.


__ИНДЕКС__
*Готовые фабричные часы с полным набором функций стоят относительно дорого, а доступные по цене модели часто оказываются неудобными или ненадёжными. В данном проекте ставится задача разработать собственный вариант шахматных часов — на доступной элементной базе, с понятной схемой и возможностью повторения.
__ССЫЛКА_НА_НОВЫЙ_РАЗДЕЛ__
 
== Краткая историческая справка ==
В XIX веке шахматные партии могли длиться бесконечно, и первые попытки контроля времени были кустарными — использовали песочные или карманные часы, которые судья запускал вручную. В 1883 году появились механические часы с двумя циферблатами и перекидным рычагом, а в 1950-х к ним добавили «падающий флажок». Однако такие часы были сложны в производстве и практически не подлежали ремонту — сломавшееся устройство проще выбрасывали. В конце 1980-х фирма DGT выпустила первые цифровые часы с высокой точностью и поддержкой инкремента, но их цена (от 12 000 рублей) сделала их малодоступными для массового пользователя, а дешёвые китайские аналоги (2 000–3 000 рублей) оказались неремонтопригодными.
 
Наш проект продолжает эту историю, но делает шаг в сторону доступности и открытости. Мы используем современную элементную базу — микроконтроллер, LED-дисплей, энкодер и две кнопки — и собираем часы, которые по функционалу приближаются к профессиональным, а по цене и ремонтопригодности — к самодельным. В отличие от заводских аналогов, наше устройство можно разобрать, заменить любую деталь и починить своими руками.
 
== Основные требования к современным электронным шахматным часам==
 
*Точность хода — без неё невозможно объективно определить победителя при истечении времени.
*Эргономика — кнопка должна давать чёткий тактильный отклик, чтобы игрок чувствовал переключение хода даже не глядя на часы.
*Видимость — экран должен быть читаем под любым углом и не бликовать, чтобы игрок мгновенно считывал время боковым зрением.
*Интуитивная настройка — интерфейс должен быть понятен без инструкции, чтобы судья мог быстро восстановить время при сбое.
*Устойчивость на столе — часы должны иметь достаточный вес и нескользящее основание, чтобы не сдвигаться при ударах.
*Ремонтопригодность — конструкция должна позволять замену любой детали (кнопки, дисплея, платы) без специального инструмента.
*Надёжность — программа должна корректно обрабатывать дребезг контактов и исключать зависания от случайных нажатий.
*Автономность — часы должны долго работать от доступных источников (AA/AAA или USB), чтобы не отвлекать игроков частой заменой батарей.
*Компактность — часы должны быть удобны для перевозки в сумке с шахматным набором и не иметь хрупких выступающих элементов.
*Доступная цена — стоимость не должна превышать 3000 рублей, чтобы часы были доступны массовому пользователю.
*Минимализм в управлении — управление должно быть сведено к минимуму (старт, стоп, сброс), чтобы часы мог использовать любой человек без инструкции.
 
==Технические характеристики==
 
*Микроконтроллер Arduino Nano
*Дисплеи 2× TM1637 (4 разряда)
*Управление Энкодер
*Кнопки игроков 2 кнопки
*Звук Пьезоизлучатель (зуммер)
*Питание от встроенного аккумулятора
*Потребление от 50 мА до 250 мА (зависит от яркости)
*Время непрерывной работы: около 18 часов
 
*Примечания
 
*· При быстром вращении энкодера скорость изменения значений автоматически увеличивается
*· Время сохраняется только во время работы устройства, при отключении питания сбрасывается
*· Максимальное время партии: 999 минут (~16.5 часов)
 
==Органы управления==
*Элемент Назначение
*Энкодер (поворот) Навигация по меню, изменение значений
*Энкодер (нажатие) Подтверждение выбора, пауза во время игры
*Кнопка игрока 1 Ход первого игрока, запуск игры
*Кнопка игрока 2 Ход второго игрока, запуск игры
*Обе кнопки (удержание 2 сек) Полный сброс всех настроек
 
==ГЛАВНОЕ МЕНЮ==
*При включении часы показывают главное меню. На дисплеях отображается номер текущего пункта (от 1 до 6).
 
*Навигация: поворот энкодера — перемещение по пунктам, нажатие энкодера — вход в выбранный пункт.
 
*Пункт 1 — установка времени. Диапазон 1–999 минут. При быстром вращении энкодера шаг увеличивается до 5 минут. На дисплее 1 отображается значение минут, на дисплее 2 — цифра 1.
 
*Пункт 2 — выбор режима игры. 0 = CLS (классический), 1 = ADD (с добавкой), 2 = DLY (с задержкой). На дисплее 1 отображается название режима, на дисплее 2 — его номер.
 
*Пункт 3 — настройка добавки или задержки. В режиме ADD: добавка 0–60 секунд. В режиме DLY: задержка 1–60 секунд. В режиме CLS: настройка не используется. На дисплее 2 отображается тип настройки: Add или dLY.
 
*Пункт 4 — старт игры. Запуск отсчёта времени.
 
*Пункт 5 — яркость дисплеев. Диапазон 0–7 (0 — минимум, 7 — максимум). На дисплее 1 отображается brI, на дисплее 2 — значение.
 
*Пункт 6 — громкость звука. Диапазон 0–100 (0 — звук выключен, 100 — максимум). На дисплее 1 отображается VoL, на дисплее 2 — значение, делённое на 10.
 
==Режимы игры==
 
*CLS (классический) — обычный контроль времени. Время каждого игрока уменьшается только во время его хода. Без добавки и задержки. Применение: стандартные партии, классические турниры.
 
*ADD (с добавкой) — после каждого хода к оставшемуся времени добавляется указанное количество секунд (0–60). Пример: начальное время 5 минут, добавка 3 секунды — после каждого хода игрок получает +3 секунды. Применение: блиц, рапид с добавлением времени (инкремент по Фишеру).
 
*DLY (с задержкой) — время не уменьшается в течение указанной задержки (1–60 секунд) после нажатия кнопки соперником. Пример: задержка 5 секунд — после хода соперника у вас есть 5 секунд на обдумывание без потери основного времени. Применение: традиционные турниры, игра с задержкой Бронштейна.
 
==Игровой процесс==
 
*Запуск игры: настройте параметры в меню, перейдите в пункт 4 (Старт), нажмите кнопку игрока, который ходит первым (кнопка 1 — игрок 1, кнопка 2 — игрок 2).
 
*Во время партии: нажатие своей кнопки — завершение хода и переключение времени на соперника. Нажатие энкодера — пауза или снятие с паузы. Удержание энкодера (2 секунды) — показ статистики ходов (на дисплее 1 — ходы игрока 1, на дисплее 2 — ходы игрока 2).
 
*Окончание игры: когда у одного из игроков истекает время, дисплей проигравшего мигает, звучит длинный звуковой сигнал, часы возвращаются в главное меню.
 
*Пауза: нажмите энкодер — на дисплеях отображается PUSE, время останавливается, кнопки игроков блокируются. Повторное нажатие энкодера снимает паузу.
 
==Звуковые сигналы==
 
*Поворот энкодера — 5 мс, 4000 Гц.
 
*Нажатие кнопки игрока — 20 мс, 3000 Гц.
 
*Нажатие энкодера — 30 мс, 2800 Гц.
 
*Старт игры — 50 мс, 2500 Гц.
 
*Пауза — 100 мс, 2200 Гц.
 
*Снятие с паузы — 100 мс, 2400 Гц.
 
*Сброс настроек — 100 мс, 2000 Гц.
 
*Длинное удержание — 100 мс, 1800 Гц.
 
*Конец игры — 1000 мс, 1500 Гц.
 
==Формат отображения времени==
 
*Обычный режим (ММ:СС) — когда время меньше 99 минут. Дисплей показывает минуты и секунды, двоеточие мигает.
 
*Часовой режим (ЧЧ:ММ) — когда время достигает 99 минут (5940 секунд) и более. Дисплей автоматически переключается на часы и минуты. Максимальное отображение — 99 часов 59 минут.
 
==Пасхалки==
 
«К Элизе» Бетховена. Активация: зайдите в настройки громкости (пункт 6), установите громкость 0, в течение 3 секунд установите 100, снова установите 0, и наконец 100. На дисплеях появится ELIS, затем заиграет мелодия.
 
==Краткое руководство (памятка)==
 
*Навигация по меню: вращение энкодера — выбор пункта, нажатие — вход.
 
*Пункты меню: 1 — время партии, 2 — режим игры, 3 — добавка/задержка, 4 — старт, 5 — яркость, 6 — громкость.
 
*Режимы игры: CLS — классический, ADD — с добавкой, DLY — с задержкой.
 
*Во время партии: нажатие своей кнопки — завершение хода, нажатие энкодера — пауза, удержание энкодера — статистика ходов, удержание обеих кнопок (2 секунды) — полный сброс.
 
==Код программы ==
 
#include <TM1637Display.h>
#include <GyverEncoder.h>
 
// ==================== ПИНЫ ====================
#define ENC_S1 11
#define ENC_S2 10
#define ENC_KEY 9
#define BTN_P1 7
#define BTN_P2 8
#define TM1637_CLK1 6
#define TM1637_DIO1 5
#define TM1637_CLK2 4
#define TM1637_DIO2 3
#define BUZZER 2
 
// ==================== НАСТРОЙКИ ====================
#define DEBOUNCE_DELAY  50
#define LONG_PRESS_TIME 2000
#define ENC_DELAY_NORMAL 150
#define ENC_DELAY_FAST  50
#define ENC_FAST_THRESHOLD 3
 
// ==================== ЗВУКОВЫЕ ЧАСТОТЫ ====================
#define BEEP_ENC        4000
#define BEEP_BUTTON    3000
#define BEEP_START      2500
#define BEEP_CLICK      2800
#define BEEP_RESET      2000
#define BEEP_GAME_OVER  1500
#define BEEP_LONG_PRESS 1800
#define BEEP_PAUSE      2200
#define BEEP_UNPAUSE    2400
 
// ==================== ПРОТОТИПЫ ====================
void beep(int duration, int frequency);
void showTime();
void showMenu();
void showSetTime();
void showSetAdd();
void showSetDelay();
void showSetMode();
void showSetBrightness();
void showSetVolume();
void handleGame();
void startGame(int firstPlayer);
void gameOver(int loser);
void resetAll();
void playFurElise();
void checkVolumeEasterEgg();
 
// ==================== ПЕРЕМЕННЫЕ ====================
TM1637Display display1(TM1637_CLK1, TM1637_DIO1);
TM1637Display display2(TM1637_CLK2, TM1637_DIO2);
Encoder encoder(ENC_S1, ENC_S2, ENC_KEY);
 
int mode = 0;
int gameMode = 0;
 
long timeP1 = 300;
long timeP2 = 300;
long addTime = 3;
long delaySec = 5;
 
bool turn = 0;
bool playing = false;
bool paused = false;
unsigned long lastTickTime = 0;
unsigned long pauseStartTime = 0;
bool gameStarted = false;
 
int movesP1 = 0;
int movesP2 = 0;
bool showStats = false;
unsigned long statsShowTime = 0;
 
int setMinutes = 5;
int setAdd = 3;
int setDelay = 5;
int setMode = 0;
int setBrightness = 7;
int setVolume = 100;
 
// Переменные для пасхалки
int easterEggStep = 0;
unsigned long lastEasterEggTime = 0;
 
bool lastP1 = HIGH, lastP2 = HIGH;
unsigned long lastP1Time = 0, lastP2Time = 0;
unsigned long bothStart = 0;
 
int menuPos = 0;
unsigned long lastEncTime = 0;
 
int encFastCounter = 0;
unsigned long lastEncTurnTime = 0;
int lastEncDirection = 0;
 
// ==================== СЕГМЕНТНЫЕ МАСКИ ====================
const uint8_t MY_SEG_A = 0b01110111;
const uint8_t MY_SEG_b = 0b01111100;
const uint8_t MY_SEG_C = 0b00111001;
const uint8_t MY_SEG_d = 0b01011110;
const uint8_t MY_SEG_L = 0b00111000;
const uint8_t MY_SEG_S = 0b01101101;
const uint8_t MY_SEG_Y = 0b01101110;
const uint8_t MY_SEG_r = 0b01010000;
const uint8_t MY_SEG_I = 0b01101100;
const uint8_t MY_SEG_U = 0b00111110;
const uint8_t MY_SEG_o = 0b01011100;
 
const uint8_t MODE_CLS[] = { MY_SEG_C, MY_SEG_L, MY_SEG_S, 0x00 };
const uint8_t MODE_ADD[] = { MY_SEG_A, MY_SEG_d, MY_SEG_d, 0x00 };
const uint8_t MODE_DLY[] = { MY_SEG_d, MY_SEG_L, MY_SEG_Y, 0x00 };
const uint8_t MODE_bRI[] = { MY_SEG_b, MY_SEG_r, MY_SEG_I, 0x00 };
const uint8_t MODE_Vol[] = { MY_SEG_U, MY_SEG_o, MY_SEG_L, 0x00 };
const uint8_t PUSE[] = { 0b01110011, 0b01111100, 0b01101101, 0b01111001 };
const uint8_t ELISE_SEG[] = { 0b01111001, 0b00111000, 0b00110000, 0b01101101 }; // E L I S
 
// ==================== SETUP ====================
void setup() {
    pinMode(BTN_P1, INPUT_PULLUP);
    pinMode(BTN_P2, INPUT_PULLUP);
    pinMode(BUZZER, OUTPUT);
 
    display1.setBrightness(setBrightness);
    display2.setBrightness(setBrightness);
 
    display1.showNumberDec(8888);
    display2.showNumberDec(8888);
    delay(800);
 
    showTime();
    showMenu();
    beep(100, BEEP_START);
}
 
// ==================== LOOP ====================
void loop() {
    encoder.tick();
 
    if (mode == 0) {
        handleEncoderMenu();
 
        if (encoder.isClick()) {
            beep(30, BEEP_CLICK);
            if (menuPos == 0) {
                mode = 3;
                setMinutes = timeP1 / 60;
                showSetTime();
                lastEncTime = millis();
            }
            else if (menuPos == 1) {
                mode = 5;
                setMode = gameMode;
                showSetMode();
                lastEncTime = millis();
            }
            else if (menuPos == 2) {
                if (gameMode == 1) {
                    mode = 4;
                    setAdd = addTime;
                    showSetAdd();
                }
                else if (gameMode == 2) {
                    mode = 7;
                    setDelay = delaySec;
                    showSetDelay();
                }
                else {
                    mode = 4;
                    setAdd = addTime;
                    showSetAdd();
                }
                lastEncTime = millis();
            }
            else if (menuPos == 3) {
                mode = 1;
                timeP1 = (long)setMinutes * 60;
                timeP2 = timeP1;
                movesP1 = 0;
                movesP2 = 0;
                gameStarted = false;
                turn = 0;
                paused = false;
                showTime();
                beep(50, BEEP_START);
            }
            else if (menuPos == 4) {
                mode = 6;
                showSetBrightness();
                lastEncTime = millis();
            }
            else if (menuPos == 5) {
                mode = 8;
                showSetVolume();
                lastEncTime = millis();
                easterEggStep = 0; // Сброс пасхалки при входе
            }
        }
 
        if (encoder.isHolded()) {
            beep(100, BEEP_LONG_PRESS);
            resetAll();
        }
    }
    else if (mode == 1) {
        if (digitalRead(BTN_P1) == LOW && !gameStarted) {
            delay(DEBOUNCE_DELAY);
            startGame(0);
        }
        else if (digitalRead(BTN_P2) == LOW && !gameStarted) {
            delay(DEBOUNCE_DELAY);
            startGame(1);
        }
 
        if (encoder.isHolded()) {
            beep(100, BEEP_LONG_PRESS);
            mode = 0;
            menuPos = 0;
            showTime();
            showMenu();
        }
    }
    else if (mode == 2) {
        if (encoder.isClick() && playing) {
            if (!paused) {
                paused = true;
                pauseStartTime = millis();
                beep(100, BEEP_PAUSE);
                display1.setSegments(PUSE, 4, 0);
                display2.setSegments(PUSE, 4, 0);
            } else {
                paused = false;
                unsigned long pauseDuration = millis() - pauseStartTime;
                lastTickTime += pauseDuration;
                beep(100, BEEP_UNPAUSE);
                showTime();
            }
            delay(200);
        }
 
        if (encoder.isHolded() && !showStats) {
            showStats = true;
            statsShowTime = millis();
            display1.showNumberDec(movesP1);
            display2.showNumberDec(movesP2);
            beep(50, BEEP_CLICK);
        }
 
        if (showStats && millis() - statsShowTime > 2000) {
            showStats = false;
            showTime();
        }
 
        handleGame();
    }
    else if (mode == 3) {
        handleEncoderValueDynamic(setMinutes, 1, 999, showSetTime);
        if (encoder.isClick()) {
            beep(30, BEEP_CLICK);
            mode = 0;
            showMenu();
        }
    }
    else if (mode == 4) {
        handleEncoderValueDynamic(setAdd, 0, 60, showSetAdd);
        if (encoder.isClick()) {
            beep(30, BEEP_CLICK);
            addTime = setAdd;
            mode = 0;
            showMenu();
        }
    }
    else if (mode == 5) {
        handleEncoderValue(setMode, 0, 2, showSetMode);
        if (encoder.isClick()) {
            beep(30, BEEP_CLICK);
            gameMode = setMode;
            mode = 0;
            showMenu();
        }
    }
    else if (mode == 6) {
        handleEncoderValue(setBrightness, 0, 7, showSetBrightness);
        if (encoder.isClick()) {
            beep(30, BEEP_CLICK);
            display1.setBrightness(setBrightness);
            display2.setBrightness(setBrightness);
            mode = 0;
            showMenu();
        }
    }
    else if (mode == 7) {
        handleEncoderValueDynamic(setDelay, 1, 60, showSetDelay);
        if (encoder.isClick()) {
            beep(30, BEEP_CLICK);
            delaySec = setDelay;
            mode = 0;
            showMenu();
        }
    }
    else if (mode == 8) {
        int oldVolume = setVolume;
        handleEncoderValueFast(setVolume, 0, 100, showSetVolume, 10);
       
        // Проверяем изменение громкости для пасхалки
        if (oldVolume != setVolume) {
            checkVolumeEasterEgg();
        }
       
        if (encoder.isClick()) {
            beep(30, BEEP_CLICK);
            mode = 0;
            showMenu();
            easterEggStep = 0;
        }
    }
 
    delay(5);
}
 
// ==================== ПАСХАЛКА ====================
void checkVolumeEasterEgg() {
    unsigned long now = millis();
   
    // Если прошло больше 3 секунд, сбрасываем
    if (now - lastEasterEggTime > 3000) {
        easterEggStep = 0;
    }
   
    lastEasterEggTime = now;
   
    // Проверяем последовательность: 0 -> 100 -> 0 -> 100
    if (easterEggStep == 0 && setVolume == 0) {
        easterEggStep = 1;
    }
    else if (easterEggStep == 1 && setVolume == 100) {
        easterEggStep = 2;
    }
    else if (easterEggStep == 2 && setVolume == 0) {
        easterEggStep = 3;
    }
    else if (easterEggStep == 3 && setVolume == 100) {
        // Пасхалка активирована!
        easterEggStep = 0;
       
        // Показываем ELIS
        display1.setSegments(ELISE_SEG, 4, 0);
        display2.setSegments(ELISE_SEG, 4, 0);
       
        // Играем мелодию
        playFurElise();
       
        // Возвращаем отображение
        showSetVolume();
    }
}
 
void playFurElise() {
    // Частоты нот
    const int E5 = 659;
    const int DS5 = 622;
    const int E4 = 330;
    const int B4 = 494;
    const int D5 = 587;
    const int C5 = 523;
    const int A4 = 440;
    const int REST = 0;
   
    // Упрощенная мелодия "К Элизе"
    int melody[] = {
        E5, DS5, E5, DS5, E5, B4, D5, C5, A4,
        REST, C5, E4, A4, B4, REST, E4, DS5, E5, B4, D5, C5, A4,
        REST, C5, E4, A4, B4, REST, E4, C5, B4, A4
    };
   
    int durations[] = {
        150, 150, 150, 150, 150, 150, 150, 150, 300,
        50, 150, 150, 150, 300, 50, 150, 150, 150, 150, 150, 150, 300,
        50, 150, 150, 150, 300, 50, 150, 150, 150, 300
    };
   
    int notesCount = sizeof(melody) / sizeof(melody[0]);
   
    for (int i = 0; i < notesCount; i++) {
        if (melody[i] != REST) {
            // Используем громкость, но не меньше 20%
            int volumePercent = (setVolume > 20) ? setVolume : 50;
            int adjustedFreq = map(volumePercent, 0, 100, 500, melody[i]);
            tone(BUZZER, adjustedFreq, durations[i] * 0.9);
        }
        delay(durations[i]);
        noTone(BUZZER);
    }
   
    delay(300);
}
 
// ==================== ОБРАБОТКА ЭНКОДЕРА ====================
void handleEncoderMenu() {
    if (encoder.isRight()) {
        unsigned long now = millis();
        if (now - lastEncTurnTime < 150) {
            if (lastEncDirection == 1) encFastCounter++;
            else { encFastCounter = 1; lastEncDirection = 1; }
        } else { encFastCounter = 0; lastEncDirection = 1; }
        lastEncTurnTime = now;
        int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
        if (now - lastEncTime > currentDelay) {
            beep(5, BEEP_ENC);
            menuPos++;
            if (menuPos > 5) menuPos = 0;
            showMenu();
            lastEncTime = now;
        }
    }
 
    if (encoder.isLeft()) {
        unsigned long now = millis();
        if (now - lastEncTurnTime < 150) {
            if (lastEncDirection == -1) encFastCounter++;
            else { encFastCounter = 1; lastEncDirection = -1; }
        } else { encFastCounter = 0; lastEncDirection = -1; }
        lastEncTurnTime = now;
        int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
        if (now - lastEncTime > currentDelay) {
            beep(5, BEEP_ENC);
            menuPos--;
            if (menuPos < 0) menuPos = 5;
            showMenu();
            lastEncTime = now;
        }
    }
}
 
void handleEncoderValue(int &value, int minVal, int maxVal, void (*updateFunc)()) {
    if (encoder.isRight()) {
        unsigned long now = millis();
        if (now - lastEncTurnTime < 150) {
            if (lastEncDirection == 1) encFastCounter++;
            else { encFastCounter = 1; lastEncDirection = 1; }
        } else { encFastCounter = 0; lastEncDirection = 1; }
        lastEncTurnTime = now;
        int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
        if (now - lastEncTime > currentDelay) {
            beep(5, BEEP_ENC);
            value++;
            if (value > maxVal) value = maxVal;
            updateFunc();
            lastEncTime = now;
        }
    }
 
    if (encoder.isLeft()) {
        unsigned long now = millis();
        if (now - lastEncTurnTime < 150) {
            if (lastEncDirection == -1) encFastCounter++;
            else { encFastCounter = 1; lastEncDirection = -1; }
        } else { encFastCounter = 0; lastEncDirection = -1; }
        lastEncTurnTime = now;
        int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
        if (now - lastEncTime > currentDelay) {
            beep(5, BEEP_ENC);
            value--;
            if (value < minVal) value = minVal;
            updateFunc();
            lastEncTime = now;
        }
    }
}
 
void handleEncoderValueDynamic(int &value, int minVal, int maxVal, void (*updateFunc)()) {
    if (encoder.isRight()) {
        unsigned long now = millis();
        if (now - lastEncTurnTime < 150) {
            if (lastEncDirection == 1) { encFastCounter++; }
            else { encFastCounter = 1; lastEncDirection = 1; }
        } else { encFastCounter = 0; lastEncDirection = 1; }
        lastEncTurnTime = now;
        int step = (encFastCounter > ENC_FAST_THRESHOLD) ? 5 : 1;
        int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
        if (now - lastEncTime > currentDelay) {
            beep(5, BEEP_ENC);
            value += step;
            if (value > maxVal) value = maxVal;
            updateFunc();
            lastEncTime = now;
        }
    }
 
    if (encoder.isLeft()) {
        unsigned long now = millis();
        if (now - lastEncTurnTime < 150) {
            if (lastEncDirection == -1) { encFastCounter++; }
            else { encFastCounter = 1; lastEncDirection = -1; }
        } else { encFastCounter = 0; lastEncDirection = -1; }
        lastEncTurnTime = now;
        int step = (encFastCounter > ENC_FAST_THRESHOLD) ? 5 : 1;
        int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
        if (now - lastEncTime > currentDelay) {
            beep(5, BEEP_ENC);
            value -= step;
            if (value < minVal) value = minVal;
            updateFunc();
            lastEncTime = now;
        }
    }
}
 
void handleEncoderValueFast(int &value, int minVal, int maxVal, void (*updateFunc)(), int step) {
    if (encoder.isRight()) {
        unsigned long now = millis();
        if (now - lastEncTurnTime < 150) {
            if (lastEncDirection == 1) encFastCounter++;
            else { encFastCounter = 1; lastEncDirection = 1; }
        } else { encFastCounter = 0; lastEncDirection = 1; }
        lastEncTurnTime = now;
        int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
        if (now - lastEncTime > currentDelay) {
            beep(5, BEEP_ENC);
            value += step;
            if (value > maxVal) value = maxVal;
            updateFunc();
            beep(20, BEEP_CLICK);
            lastEncTime = now;
        }
    }
 
    if (encoder.isLeft()) {
        unsigned long now = millis();
        if (now - lastEncTurnTime < 150) {
            if (lastEncDirection == -1) encFastCounter++;
            else { encFastCounter = 1; lastEncDirection = -1; }
        } else { encFastCounter = 0; lastEncDirection = -1; }
        lastEncTurnTime = now;
        int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
        if (now - lastEncTime > currentDelay) {
            beep(5, BEEP_ENC);
            value -= step;
            if (value < minVal) value = minVal;
            updateFunc();
            beep(20, BEEP_CLICK);
            lastEncTime = now;
        }
    }
}
 
// ==================== ЛОГИКА ИГРЫ ====================
void handleGame() {
    if (!playing) return;
    unsigned long now = millis();
 
    if (!paused) {
        bool p1Pressed = digitalRead(BTN_P1) == LOW;
        if (p1Pressed && !lastP1) {
            beep(20, BEEP_BUTTON);
            if (turn == 0) {
                long elapsed = now - lastTickTime;
               
                if (gameMode == 2) {
                    if (elapsed > delaySec * 1000L) {
                        long extraTime = elapsed - (delaySec * 1000L);
                        int secondsElapsed = extraTime / 1000;
                        if (secondsElapsed > 0) {
                            timeP1 -= secondsElapsed;
                            if (timeP1 <= 0) { gameOver(1); return; }
                            lastTickTime += secondsElapsed * 1000;
                        }
                    }
                    lastTickTime = now;
                } else {
                    int secondsElapsed = elapsed / 1000;
                    if (secondsElapsed > 0) {
                        timeP1 -= secondsElapsed;
                        if (timeP1 <= 0) { gameOver(1); return; }
                        lastTickTime += secondsElapsed * 1000;
                    }
                }
               
                if (gameMode == 1) timeP1 += addTime;
                movesP1++;
                turn = 1;
                showTime();
            }
        }
        lastP1 = p1Pressed;
 
        bool p2Pressed = digitalRead(BTN_P2) == LOW;
        if (p2Pressed && !lastP2) {
            beep(20, BEEP_BUTTON);
            if (turn == 1) {
                long elapsed = now - lastTickTime;
               
                if (gameMode == 2) {
                    if (elapsed > delaySec * 1000L) {
                        long extraTime = elapsed - (delaySec * 1000L);
                        int secondsElapsed = extraTime / 1000;
                        if (secondsElapsed > 0) {
                            timeP2 -= secondsElapsed;
                            if (timeP2 <= 0) { gameOver(2); return; }
                            lastTickTime += secondsElapsed * 1000;
                        }
                    }
                    lastTickTime = now;
                } else {
                    int secondsElapsed = elapsed / 1000;
                    if (secondsElapsed > 0) {
                        timeP2 -= secondsElapsed;
                        if (timeP2 <= 0) { gameOver(2); return; }
                        lastTickTime += secondsElapsed * 1000;
                    }
                }
               
                if (gameMode == 1) timeP2 += addTime;
                movesP2++;
                turn = 0;
                showTime();
            }
        }
        lastP2 = p2Pressed;
    }
 
    if (gameStarted && !paused) {
        long elapsed = millis() - lastTickTime;
       
        if (gameMode == 2) {
            if (elapsed > delaySec * 1000L) {
                long activeTime = elapsed - (delaySec * 1000L);
                int secondsElapsed = activeTime / 1000;
                if (secondsElapsed > 0) {
                    if (turn == 0) {
                        timeP1 -= secondsElapsed;
                        if (timeP1 <= 0) { gameOver(1); return; }
                    } else {
                        timeP2 -= secondsElapsed;
                        if (timeP2 <= 0) { gameOver(2); return; }
                    }
                    lastTickTime += secondsElapsed * 1000;
                    showTime();
                }
            }
        } else {
            int secondsElapsed = elapsed / 1000;
            if (secondsElapsed > 0) {
                if (turn == 0) {
                    timeP1 -= secondsElapsed;
                    if (timeP1 <= 0) { gameOver(1); return; }
                } else {
                    timeP2 -= secondsElapsed;
                    if (timeP2 <= 0) { gameOver(2); return; }
                }
                lastTickTime += secondsElapsed * 1000;
                showTime();
            }
        }
    }
 
    if (digitalRead(BTN_P1) == LOW && digitalRead(BTN_P2) == LOW) {
        if (bothStart == 0) bothStart = millis();
        if (millis() - bothStart > LONG_PRESS_TIME) {
            resetAll();
            bothStart = 0;
        }
    } else {
        bothStart = 0;
    }
}
 
// ==================== СТАРТ ====================
void startGame(int firstPlayer) {
    mode = 2;
    playing = true;
    paused = false;
    gameStarted = true;
    bothStart = 0;
    movesP1 = 0;
    movesP2 = 0;
 
    if (firstPlayer == 0) turn = 0;
    else turn = 1;
 
    lastTickTime = millis();
    showTime();
    beep(50, BEEP_START);
}
 
// ==================== ОТОБРАЖЕНИЕ ====================
void showTime() {
    bool p1Hours = (timeP1 >= 5940);
    bool p2Hours = (timeP2 >= 5940);
   
    if (p1Hours) {
        int hours = timeP1 / 3600;
        int minutes = (timeP1 % 3600) / 60;
        if (hours > 99) hours = 99;
        if (minutes > 59) minutes = 59;
        display1.showNumberDecEx(hours * 100 + minutes, 0b01000000, true, 4, 0);
    } else {
        int minutes = timeP1 / 60;
        int seconds = timeP1 % 60;
        if (minutes > 99) minutes = 99;
        if (seconds > 59) seconds = 59;
        display1.showNumberDecEx(minutes * 100 + seconds, 0b01000000, true, 4, 0);
    }
   
    if (p2Hours) {
        int hours = timeP2 / 3600;
        int minutes = (timeP2 % 3600) / 60;
        if (hours > 99) hours = 99;
        if (minutes > 59) minutes = 59;
        display2.showNumberDecEx(hours * 100 + minutes, 0b01000000, true, 4, 0);
    } else {
        int minutes = timeP2 / 60;
        int seconds = timeP2 % 60;
        if (minutes > 99) minutes = 99;
        if (seconds > 59) seconds = 59;
        display2.showNumberDecEx(minutes * 100 + seconds, 0b01000000, true, 4, 0);
    }
}
 
void showMenu() {
    display1.showNumberDec(menuPos + 1);
    display2.showNumberDec(menuPos + 1);
}
 
void showSetTime() {
    static int lastMinutes = -1;
    if (setMinutes < 100) {
        if (lastMinutes >= 100 && setMinutes < 100) display1.clear();
        display1.showNumberDecEx(setMinutes, 0b00000000, true, 2, 2);
    } else {
        if (lastMinutes < 100 && setMinutes >= 100) display1.clear();
        display1.showNumberDec(setMinutes);
    }
    lastMinutes = setMinutes;
    display2.showNumberDec(1);
}
 
void showSetAdd() {
    display1.showNumberDecEx(setAdd, 0b00000000, true, 2, 2);
    display2.setSegments(MODE_ADD, 4, 0);
}
 
void showSetDelay() {
    display1.showNumberDecEx(setDelay, 0b00000000, true, 2, 2);
    display2.setSegments(MODE_DLY, 4, 0);
}
 
void showSetMode() {
    if (setMode == 0) { display1.setSegments(MODE_CLS, 4, 0); display2.showNumberDec(1); }
    else if (setMode == 1) { display1.setSegments(MODE_ADD, 4, 0); display2.showNumberDec(2); }
    else if (setMode == 2) { display1.setSegments(MODE_DLY, 4, 0); display2.showNumberDec(3); }
}
 
void showSetBrightness() {
    display1.setSegments(MODE_bRI, 4, 0);
    display2.showNumberDec(setBrightness);
}
 
void showSetVolume() {
    display1.setSegments(MODE_Vol, 4, 0);
    display2.showNumberDec(setVolume / 10);
}
 
void gameOver(int loser) {
    playing = false;
    beep(1000, BEEP_GAME_OVER);
    for (int i = 0; i < 6; i++) {
        if (loser == 1) display1.clear();
        else display2.clear();
        delay(200);
        showTime();
        delay(200);
    }
    mode = 0;
    menuPos = 0;
    showMenu();
}
 
void resetAll() {
    timeP1 = (long)setMinutes * 60;
    timeP2 = timeP1;
    addTime = setAdd;
    delaySec = setDelay;
    gameMode = setMode;
    movesP1 = 0;
    movesP2 = 0;
    mode = 0;
    playing = false;
    paused = false;
    gameStarted = false;
    menuPos = 0;
    showTime();
    showMenu();
    beep(100, BEEP_RESET);
}
 
void beep(int duration, int frequency) {
    if (setVolume == 0) return;
    int adjustedFreq = map(setVolume, 0, 100, 500, frequency);
    if (adjustedFreq < 500) adjustedFreq = 500;
    if (adjustedFreq > frequency) adjustedFreq = frequency;
    tone(BUZZER, adjustedFreq, duration);
    delay(duration);
    noTone(BUZZER);
}
 
==Руководитель==
Бизяев Алексей Анатольевич-старший преподаватель кафедры конструирования и технологии радиоэлектронных средств, научный сотрудник  научно-исследовательской лаборатории перспективных космических  разработок НГТУ НЭТИ
 
== Команда проекта ==
*Козырев Андрей-электронщик
*Хавкунова Екатерина-конструктор
*Тельпухова Майя-журналист
 
 
[[Категория:работы студентов]]

Текущая версия от 14:47, 11 мая 2026

Общие сведения

  • Шахматы — это не только стратегия и логика, но и строгий регламент. В соревнованиях любого уровня действует контроль времени: у каждого игрока есть лимит на партию, и превысить его нельзя. Для этого существуют специальные шахматные часы. Они фиксируют ходы, переключают время между соперниками и подают сигнал об окончании партии.
  • Готовые фабричные часы с полным набором функций стоят относительно дорого, а доступные по цене модели часто оказываются неудобными или ненадёжными. В данном проекте ставится задача разработать собственный вариант шахматных часов — на доступной элементной базе, с понятной схемой и возможностью повторения.

Краткая историческая справка

В XIX веке шахматные партии могли длиться бесконечно, и первые попытки контроля времени были кустарными — использовали песочные или карманные часы, которые судья запускал вручную. В 1883 году появились механические часы с двумя циферблатами и перекидным рычагом, а в 1950-х к ним добавили «падающий флажок». Однако такие часы были сложны в производстве и практически не подлежали ремонту — сломавшееся устройство проще выбрасывали. В конце 1980-х фирма DGT выпустила первые цифровые часы с высокой точностью и поддержкой инкремента, но их цена (от 12 000 рублей) сделала их малодоступными для массового пользователя, а дешёвые китайские аналоги (2 000–3 000 рублей) оказались неремонтопригодными.

Наш проект продолжает эту историю, но делает шаг в сторону доступности и открытости. Мы используем современную элементную базу — микроконтроллер, LED-дисплей, энкодер и две кнопки — и собираем часы, которые по функционалу приближаются к профессиональным, а по цене и ремонтопригодности — к самодельным. В отличие от заводских аналогов, наше устройство можно разобрать, заменить любую деталь и починить своими руками.

Основные требования к современным электронным шахматным часам

  • Точность хода — без неё невозможно объективно определить победителя при истечении времени.
  • Эргономика — кнопка должна давать чёткий тактильный отклик, чтобы игрок чувствовал переключение хода даже не глядя на часы.
  • Видимость — экран должен быть читаем под любым углом и не бликовать, чтобы игрок мгновенно считывал время боковым зрением.
  • Интуитивная настройка — интерфейс должен быть понятен без инструкции, чтобы судья мог быстро восстановить время при сбое.
  • Устойчивость на столе — часы должны иметь достаточный вес и нескользящее основание, чтобы не сдвигаться при ударах.
  • Ремонтопригодность — конструкция должна позволять замену любой детали (кнопки, дисплея, платы) без специального инструмента.
  • Надёжность — программа должна корректно обрабатывать дребезг контактов и исключать зависания от случайных нажатий.
  • Автономность — часы должны долго работать от доступных источников (AA/AAA или USB), чтобы не отвлекать игроков частой заменой батарей.
  • Компактность — часы должны быть удобны для перевозки в сумке с шахматным набором и не иметь хрупких выступающих элементов.
  • Доступная цена — стоимость не должна превышать 3000 рублей, чтобы часы были доступны массовому пользователю.
  • Минимализм в управлении — управление должно быть сведено к минимуму (старт, стоп, сброс), чтобы часы мог использовать любой человек без инструкции.

Технические характеристики

  • Микроконтроллер Arduino Nano
  • Дисплеи 2× TM1637 (4 разряда)
  • Управление Энкодер
  • Кнопки игроков 2 кнопки
  • Звук Пьезоизлучатель (зуммер)
  • Питание от встроенного аккумулятора
  • Потребление от 50 мА до 250 мА (зависит от яркости)
  • Время непрерывной работы: около 18 часов
  • Примечания
  • · При быстром вращении энкодера скорость изменения значений автоматически увеличивается
  • · Время сохраняется только во время работы устройства, при отключении питания сбрасывается
  • · Максимальное время партии: 999 минут (~16.5 часов)

Органы управления

  • Элемент Назначение
  • Энкодер (поворот) Навигация по меню, изменение значений
  • Энкодер (нажатие) Подтверждение выбора, пауза во время игры
  • Кнопка игрока 1 Ход первого игрока, запуск игры
  • Кнопка игрока 2 Ход второго игрока, запуск игры
  • Обе кнопки (удержание 2 сек) Полный сброс всех настроек

ГЛАВНОЕ МЕНЮ

  • При включении часы показывают главное меню. На дисплеях отображается номер текущего пункта (от 1 до 6).
  • Навигация: поворот энкодера — перемещение по пунктам, нажатие энкодера — вход в выбранный пункт.
  • Пункт 1 — установка времени. Диапазон 1–999 минут. При быстром вращении энкодера шаг увеличивается до 5 минут. На дисплее 1 отображается значение минут, на дисплее 2 — цифра 1.
  • Пункт 2 — выбор режима игры. 0 = CLS (классический), 1 = ADD (с добавкой), 2 = DLY (с задержкой). На дисплее 1 отображается название режима, на дисплее 2 — его номер.
  • Пункт 3 — настройка добавки или задержки. В режиме ADD: добавка 0–60 секунд. В режиме DLY: задержка 1–60 секунд. В режиме CLS: настройка не используется. На дисплее 2 отображается тип настройки: Add или dLY.
  • Пункт 4 — старт игры. Запуск отсчёта времени.
  • Пункт 5 — яркость дисплеев. Диапазон 0–7 (0 — минимум, 7 — максимум). На дисплее 1 отображается brI, на дисплее 2 — значение.
  • Пункт 6 — громкость звука. Диапазон 0–100 (0 — звук выключен, 100 — максимум). На дисплее 1 отображается VoL, на дисплее 2 — значение, делённое на 10.

Режимы игры

  • CLS (классический) — обычный контроль времени. Время каждого игрока уменьшается только во время его хода. Без добавки и задержки. Применение: стандартные партии, классические турниры.
  • ADD (с добавкой) — после каждого хода к оставшемуся времени добавляется указанное количество секунд (0–60). Пример: начальное время 5 минут, добавка 3 секунды — после каждого хода игрок получает +3 секунды. Применение: блиц, рапид с добавлением времени (инкремент по Фишеру).
  • DLY (с задержкой) — время не уменьшается в течение указанной задержки (1–60 секунд) после нажатия кнопки соперником. Пример: задержка 5 секунд — после хода соперника у вас есть 5 секунд на обдумывание без потери основного времени. Применение: традиционные турниры, игра с задержкой Бронштейна.

Игровой процесс

  • Запуск игры: настройте параметры в меню, перейдите в пункт 4 (Старт), нажмите кнопку игрока, который ходит первым (кнопка 1 — игрок 1, кнопка 2 — игрок 2).
  • Во время партии: нажатие своей кнопки — завершение хода и переключение времени на соперника. Нажатие энкодера — пауза или снятие с паузы. Удержание энкодера (2 секунды) — показ статистики ходов (на дисплее 1 — ходы игрока 1, на дисплее 2 — ходы игрока 2).
  • Окончание игры: когда у одного из игроков истекает время, дисплей проигравшего мигает, звучит длинный звуковой сигнал, часы возвращаются в главное меню.
  • Пауза: нажмите энкодер — на дисплеях отображается PUSE, время останавливается, кнопки игроков блокируются. Повторное нажатие энкодера снимает паузу.

Звуковые сигналы

  • Поворот энкодера — 5 мс, 4000 Гц.
  • Нажатие кнопки игрока — 20 мс, 3000 Гц.
  • Нажатие энкодера — 30 мс, 2800 Гц.
  • Старт игры — 50 мс, 2500 Гц.
  • Пауза — 100 мс, 2200 Гц.
  • Снятие с паузы — 100 мс, 2400 Гц.
  • Сброс настроек — 100 мс, 2000 Гц.
  • Длинное удержание — 100 мс, 1800 Гц.
  • Конец игры — 1000 мс, 1500 Гц.

Формат отображения времени

  • Обычный режим (ММ:СС) — когда время меньше 99 минут. Дисплей показывает минуты и секунды, двоеточие мигает.
  • Часовой режим (ЧЧ:ММ) — когда время достигает 99 минут (5940 секунд) и более. Дисплей автоматически переключается на часы и минуты. Максимальное отображение — 99 часов 59 минут.

Пасхалки

«К Элизе» Бетховена. Активация: зайдите в настройки громкости (пункт 6), установите громкость 0, в течение 3 секунд установите 100, снова установите 0, и наконец 100. На дисплеях появится ELIS, затем заиграет мелодия.

Краткое руководство (памятка)

  • Навигация по меню: вращение энкодера — выбор пункта, нажатие — вход.
  • Пункты меню: 1 — время партии, 2 — режим игры, 3 — добавка/задержка, 4 — старт, 5 — яркость, 6 — громкость.
  • Режимы игры: CLS — классический, ADD — с добавкой, DLY — с задержкой.
  • Во время партии: нажатие своей кнопки — завершение хода, нажатие энкодера — пауза, удержание энкодера — статистика ходов, удержание обеих кнопок (2 секунды) — полный сброс.

Код программы

  1. include <TM1637Display.h>
  2. include <GyverEncoder.h>

// ==================== ПИНЫ ====================

  1. define ENC_S1 11
  2. define ENC_S2 10
  3. define ENC_KEY 9
  4. define BTN_P1 7
  5. define BTN_P2 8
  6. define TM1637_CLK1 6
  7. define TM1637_DIO1 5
  8. define TM1637_CLK2 4
  9. define TM1637_DIO2 3
  10. define BUZZER 2

// ==================== НАСТРОЙКИ ====================

  1. define DEBOUNCE_DELAY 50
  2. define LONG_PRESS_TIME 2000
  3. define ENC_DELAY_NORMAL 150
  4. define ENC_DELAY_FAST 50
  5. define ENC_FAST_THRESHOLD 3

// ==================== ЗВУКОВЫЕ ЧАСТОТЫ ====================

  1. define BEEP_ENC 4000
  2. define BEEP_BUTTON 3000
  3. define BEEP_START 2500
  4. define BEEP_CLICK 2800
  5. define BEEP_RESET 2000
  6. define BEEP_GAME_OVER 1500
  7. define BEEP_LONG_PRESS 1800
  8. define BEEP_PAUSE 2200
  9. define BEEP_UNPAUSE 2400

// ==================== ПРОТОТИПЫ ==================== void beep(int duration, int frequency); void showTime(); void showMenu(); void showSetTime(); void showSetAdd(); void showSetDelay(); void showSetMode(); void showSetBrightness(); void showSetVolume(); void handleGame(); void startGame(int firstPlayer); void gameOver(int loser); void resetAll(); void playFurElise(); void checkVolumeEasterEgg();

// ==================== ПЕРЕМЕННЫЕ ==================== TM1637Display display1(TM1637_CLK1, TM1637_DIO1); TM1637Display display2(TM1637_CLK2, TM1637_DIO2); Encoder encoder(ENC_S1, ENC_S2, ENC_KEY);

int mode = 0; int gameMode = 0;

long timeP1 = 300; long timeP2 = 300; long addTime = 3; long delaySec = 5;

bool turn = 0; bool playing = false; bool paused = false; unsigned long lastTickTime = 0; unsigned long pauseStartTime = 0; bool gameStarted = false;

int movesP1 = 0; int movesP2 = 0; bool showStats = false; unsigned long statsShowTime = 0;

int setMinutes = 5; int setAdd = 3; int setDelay = 5; int setMode = 0; int setBrightness = 7; int setVolume = 100;

// Переменные для пасхалки int easterEggStep = 0; unsigned long lastEasterEggTime = 0;

bool lastP1 = HIGH, lastP2 = HIGH; unsigned long lastP1Time = 0, lastP2Time = 0; unsigned long bothStart = 0;

int menuPos = 0; unsigned long lastEncTime = 0;

int encFastCounter = 0; unsigned long lastEncTurnTime = 0; int lastEncDirection = 0;

// ==================== СЕГМЕНТНЫЕ МАСКИ ==================== const uint8_t MY_SEG_A = 0b01110111; const uint8_t MY_SEG_b = 0b01111100; const uint8_t MY_SEG_C = 0b00111001; const uint8_t MY_SEG_d = 0b01011110; const uint8_t MY_SEG_L = 0b00111000; const uint8_t MY_SEG_S = 0b01101101; const uint8_t MY_SEG_Y = 0b01101110; const uint8_t MY_SEG_r = 0b01010000; const uint8_t MY_SEG_I = 0b01101100; const uint8_t MY_SEG_U = 0b00111110; const uint8_t MY_SEG_o = 0b01011100;

const uint8_t MODE_CLS[] = { MY_SEG_C, MY_SEG_L, MY_SEG_S, 0x00 }; const uint8_t MODE_ADD[] = { MY_SEG_A, MY_SEG_d, MY_SEG_d, 0x00 }; const uint8_t MODE_DLY[] = { MY_SEG_d, MY_SEG_L, MY_SEG_Y, 0x00 }; const uint8_t MODE_bRI[] = { MY_SEG_b, MY_SEG_r, MY_SEG_I, 0x00 }; const uint8_t MODE_Vol[] = { MY_SEG_U, MY_SEG_o, MY_SEG_L, 0x00 }; const uint8_t PUSE[] = { 0b01110011, 0b01111100, 0b01101101, 0b01111001 }; const uint8_t ELISE_SEG[] = { 0b01111001, 0b00111000, 0b00110000, 0b01101101 }; // E L I S

// ==================== SETUP ==================== void setup() {

   pinMode(BTN_P1, INPUT_PULLUP);
   pinMode(BTN_P2, INPUT_PULLUP);
   pinMode(BUZZER, OUTPUT);
   display1.setBrightness(setBrightness);
   display2.setBrightness(setBrightness);
   display1.showNumberDec(8888);
   display2.showNumberDec(8888);
   delay(800);
   showTime();
   showMenu();
   beep(100, BEEP_START);

}

// ==================== LOOP ==================== void loop() {

   encoder.tick();
   if (mode == 0) {
       handleEncoderMenu();
       if (encoder.isClick()) {
           beep(30, BEEP_CLICK);
           if (menuPos == 0) {
               mode = 3;
               setMinutes = timeP1 / 60;
               showSetTime();
               lastEncTime = millis();
           }
           else if (menuPos == 1) {
               mode = 5;
               setMode = gameMode;
               showSetMode();
               lastEncTime = millis();
           }
           else if (menuPos == 2) {
               if (gameMode == 1) {
                   mode = 4;
                   setAdd = addTime;
                   showSetAdd();
               }
               else if (gameMode == 2) {
                   mode = 7;
                   setDelay = delaySec;
                   showSetDelay();
               }
               else {
                   mode = 4;
                   setAdd = addTime;
                   showSetAdd();
               }
               lastEncTime = millis();
           }
           else if (menuPos == 3) {
               mode = 1;
               timeP1 = (long)setMinutes * 60;
               timeP2 = timeP1;
               movesP1 = 0;
               movesP2 = 0;
               gameStarted = false;
               turn = 0;
               paused = false;
               showTime();
               beep(50, BEEP_START);
           }
           else if (menuPos == 4) {
               mode = 6;
               showSetBrightness();
               lastEncTime = millis();
           }
           else if (menuPos == 5) {
               mode = 8;
               showSetVolume();
               lastEncTime = millis();
               easterEggStep = 0; // Сброс пасхалки при входе
           }
       }
       if (encoder.isHolded()) {
           beep(100, BEEP_LONG_PRESS);
           resetAll();
       }
   }
   else if (mode == 1) {
       if (digitalRead(BTN_P1) == LOW && !gameStarted) {
           delay(DEBOUNCE_DELAY);
           startGame(0);
       }
       else if (digitalRead(BTN_P2) == LOW && !gameStarted) {
           delay(DEBOUNCE_DELAY);
           startGame(1);
       }
       if (encoder.isHolded()) {
           beep(100, BEEP_LONG_PRESS);
           mode = 0;
           menuPos = 0;
           showTime();
           showMenu();
       }
   }
   else if (mode == 2) {
       if (encoder.isClick() && playing) {
           if (!paused) {
               paused = true;
               pauseStartTime = millis();
               beep(100, BEEP_PAUSE);
               display1.setSegments(PUSE, 4, 0);
               display2.setSegments(PUSE, 4, 0);
           } else {
               paused = false;
               unsigned long pauseDuration = millis() - pauseStartTime;
               lastTickTime += pauseDuration;
               beep(100, BEEP_UNPAUSE);
               showTime();
           }
           delay(200);
       }
       if (encoder.isHolded() && !showStats) {
           showStats = true;
           statsShowTime = millis();
           display1.showNumberDec(movesP1);
           display2.showNumberDec(movesP2);
           beep(50, BEEP_CLICK);
       }
       if (showStats && millis() - statsShowTime > 2000) {
           showStats = false;
           showTime();
       }
       handleGame();
   }
   else if (mode == 3) {
       handleEncoderValueDynamic(setMinutes, 1, 999, showSetTime);
       if (encoder.isClick()) {
           beep(30, BEEP_CLICK);
           mode = 0;
           showMenu();
       }
   }
   else if (mode == 4) {
       handleEncoderValueDynamic(setAdd, 0, 60, showSetAdd);
       if (encoder.isClick()) {
           beep(30, BEEP_CLICK);
           addTime = setAdd;
           mode = 0;
           showMenu();
       }
   }
   else if (mode == 5) {
       handleEncoderValue(setMode, 0, 2, showSetMode);
       if (encoder.isClick()) {
           beep(30, BEEP_CLICK);
           gameMode = setMode;
           mode = 0;
           showMenu();
       }
   }
   else if (mode == 6) {
       handleEncoderValue(setBrightness, 0, 7, showSetBrightness);
       if (encoder.isClick()) {
           beep(30, BEEP_CLICK);
           display1.setBrightness(setBrightness);
           display2.setBrightness(setBrightness);
           mode = 0;
           showMenu();
       }
   }
   else if (mode == 7) {
       handleEncoderValueDynamic(setDelay, 1, 60, showSetDelay);
       if (encoder.isClick()) {
           beep(30, BEEP_CLICK);
           delaySec = setDelay;
           mode = 0;
           showMenu();
       }
   }
   else if (mode == 8) {
       int oldVolume = setVolume;
       handleEncoderValueFast(setVolume, 0, 100, showSetVolume, 10);
       
       // Проверяем изменение громкости для пасхалки
       if (oldVolume != setVolume) {
           checkVolumeEasterEgg();
       }
       
       if (encoder.isClick()) {
           beep(30, BEEP_CLICK);
           mode = 0;
           showMenu();
           easterEggStep = 0;
       }
   }
   delay(5);

}

// ==================== ПАСХАЛКА ==================== void checkVolumeEasterEgg() {

   unsigned long now = millis();
   
   // Если прошло больше 3 секунд, сбрасываем
   if (now - lastEasterEggTime > 3000) {
       easterEggStep = 0;
   }
   
   lastEasterEggTime = now;
   
   // Проверяем последовательность: 0 -> 100 -> 0 -> 100
   if (easterEggStep == 0 && setVolume == 0) {
       easterEggStep = 1;
   }
   else if (easterEggStep == 1 && setVolume == 100) {
       easterEggStep = 2;
   }
   else if (easterEggStep == 2 && setVolume == 0) {
       easterEggStep = 3;
   }
   else if (easterEggStep == 3 && setVolume == 100) {
       // Пасхалка активирована!
       easterEggStep = 0;
       
       // Показываем ELIS
       display1.setSegments(ELISE_SEG, 4, 0);
       display2.setSegments(ELISE_SEG, 4, 0);
       
       // Играем мелодию
       playFurElise();
       
       // Возвращаем отображение
       showSetVolume();
   }

}

void playFurElise() {

   // Частоты нот
   const int E5 = 659;
   const int DS5 = 622;
   const int E4 = 330;
   const int B4 = 494;
   const int D5 = 587;
   const int C5 = 523;
   const int A4 = 440;
   const int REST = 0;
   
   // Упрощенная мелодия "К Элизе"
   int melody[] = {
       E5, DS5, E5, DS5, E5, B4, D5, C5, A4,
       REST, C5, E4, A4, B4, REST, E4, DS5, E5, B4, D5, C5, A4,
       REST, C5, E4, A4, B4, REST, E4, C5, B4, A4
   };
   
   int durations[] = {
       150, 150, 150, 150, 150, 150, 150, 150, 300,
       50, 150, 150, 150, 300, 50, 150, 150, 150, 150, 150, 150, 300,
       50, 150, 150, 150, 300, 50, 150, 150, 150, 300
   };
   
   int notesCount = sizeof(melody) / sizeof(melody[0]);
   
   for (int i = 0; i < notesCount; i++) {
       if (melody[i] != REST) {
           // Используем громкость, но не меньше 20%
           int volumePercent = (setVolume > 20) ? setVolume : 50;
           int adjustedFreq = map(volumePercent, 0, 100, 500, melody[i]);
           tone(BUZZER, adjustedFreq, durations[i] * 0.9);
       }
       delay(durations[i]);
       noTone(BUZZER);
   }
   
   delay(300);

}

// ==================== ОБРАБОТКА ЭНКОДЕРА ==================== void handleEncoderMenu() {

   if (encoder.isRight()) {
       unsigned long now = millis();
       if (now - lastEncTurnTime < 150) {
           if (lastEncDirection == 1) encFastCounter++;
           else { encFastCounter = 1; lastEncDirection = 1; }
       } else { encFastCounter = 0; lastEncDirection = 1; }
       lastEncTurnTime = now;
       int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
       if (now - lastEncTime > currentDelay) {
           beep(5, BEEP_ENC);
           menuPos++;
           if (menuPos > 5) menuPos = 0;
           showMenu();
           lastEncTime = now;
       }
   }
   if (encoder.isLeft()) {
       unsigned long now = millis();
       if (now - lastEncTurnTime < 150) {
           if (lastEncDirection == -1) encFastCounter++;
           else { encFastCounter = 1; lastEncDirection = -1; }
       } else { encFastCounter = 0; lastEncDirection = -1; }
       lastEncTurnTime = now;
       int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
       if (now - lastEncTime > currentDelay) {
           beep(5, BEEP_ENC);
           menuPos--;
           if (menuPos < 0) menuPos = 5;
           showMenu();
           lastEncTime = now;
       }
   }

}

void handleEncoderValue(int &value, int minVal, int maxVal, void (*updateFunc)()) {

   if (encoder.isRight()) {
       unsigned long now = millis();
       if (now - lastEncTurnTime < 150) {
           if (lastEncDirection == 1) encFastCounter++;
           else { encFastCounter = 1; lastEncDirection = 1; }
       } else { encFastCounter = 0; lastEncDirection = 1; }
       lastEncTurnTime = now;
       int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
       if (now - lastEncTime > currentDelay) {
           beep(5, BEEP_ENC);
           value++;
           if (value > maxVal) value = maxVal;
           updateFunc();
           lastEncTime = now;
       }
   }
   if (encoder.isLeft()) {
       unsigned long now = millis();
       if (now - lastEncTurnTime < 150) {
           if (lastEncDirection == -1) encFastCounter++;
           else { encFastCounter = 1; lastEncDirection = -1; }
       } else { encFastCounter = 0; lastEncDirection = -1; }
       lastEncTurnTime = now;
       int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
       if (now - lastEncTime > currentDelay) {
           beep(5, BEEP_ENC);
           value--;
           if (value < minVal) value = minVal;
           updateFunc();
           lastEncTime = now;
       }
   }

}

void handleEncoderValueDynamic(int &value, int minVal, int maxVal, void (*updateFunc)()) {

   if (encoder.isRight()) {
       unsigned long now = millis();
       if (now - lastEncTurnTime < 150) {
           if (lastEncDirection == 1) { encFastCounter++; } 
           else { encFastCounter = 1; lastEncDirection = 1; }
       } else { encFastCounter = 0; lastEncDirection = 1; }
       lastEncTurnTime = now;
       int step = (encFastCounter > ENC_FAST_THRESHOLD) ? 5 : 1;
       int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
       if (now - lastEncTime > currentDelay) {
           beep(5, BEEP_ENC);
           value += step;
           if (value > maxVal) value = maxVal;
           updateFunc();
           lastEncTime = now;
       }
   }
   if (encoder.isLeft()) {
       unsigned long now = millis();
       if (now - lastEncTurnTime < 150) {
           if (lastEncDirection == -1) { encFastCounter++; } 
           else { encFastCounter = 1; lastEncDirection = -1; }
       } else { encFastCounter = 0; lastEncDirection = -1; }
       lastEncTurnTime = now;
       int step = (encFastCounter > ENC_FAST_THRESHOLD) ? 5 : 1;
       int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
       if (now - lastEncTime > currentDelay) {
           beep(5, BEEP_ENC);
           value -= step;
           if (value < minVal) value = minVal;
           updateFunc();
           lastEncTime = now;
       }
   }

}

void handleEncoderValueFast(int &value, int minVal, int maxVal, void (*updateFunc)(), int step) {

   if (encoder.isRight()) {
       unsigned long now = millis();
       if (now - lastEncTurnTime < 150) {
           if (lastEncDirection == 1) encFastCounter++;
           else { encFastCounter = 1; lastEncDirection = 1; }
       } else { encFastCounter = 0; lastEncDirection = 1; }
       lastEncTurnTime = now;
       int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
       if (now - lastEncTime > currentDelay) {
           beep(5, BEEP_ENC);
           value += step;
           if (value > maxVal) value = maxVal;
           updateFunc();
           beep(20, BEEP_CLICK);
           lastEncTime = now;
       }
   }
   if (encoder.isLeft()) {
       unsigned long now = millis();
       if (now - lastEncTurnTime < 150) {
           if (lastEncDirection == -1) encFastCounter++;
           else { encFastCounter = 1; lastEncDirection = -1; }
       } else { encFastCounter = 0; lastEncDirection = -1; }
       lastEncTurnTime = now;
       int currentDelay = (encFastCounter > ENC_FAST_THRESHOLD) ? ENC_DELAY_FAST : ENC_DELAY_NORMAL;
       if (now - lastEncTime > currentDelay) {
           beep(5, BEEP_ENC);
           value -= step;
           if (value < minVal) value = minVal;
           updateFunc();
           beep(20, BEEP_CLICK);
           lastEncTime = now;
       }
   }

}

// ==================== ЛОГИКА ИГРЫ ==================== void handleGame() {

   if (!playing) return;
   unsigned long now = millis();
   if (!paused) {
       bool p1Pressed = digitalRead(BTN_P1) == LOW;
       if (p1Pressed && !lastP1) {
           beep(20, BEEP_BUTTON);
           if (turn == 0) {
               long elapsed = now - lastTickTime;
               
               if (gameMode == 2) {
                   if (elapsed > delaySec * 1000L) {
                       long extraTime = elapsed - (delaySec * 1000L);
                       int secondsElapsed = extraTime / 1000;
                       if (secondsElapsed > 0) {
                           timeP1 -= secondsElapsed;
                           if (timeP1 <= 0) { gameOver(1); return; }
                           lastTickTime += secondsElapsed * 1000;
                       }
                   }
                   lastTickTime = now;
               } else {
                   int secondsElapsed = elapsed / 1000;
                   if (secondsElapsed > 0) {
                       timeP1 -= secondsElapsed;
                       if (timeP1 <= 0) { gameOver(1); return; }
                       lastTickTime += secondsElapsed * 1000;
                   }
               }
               
               if (gameMode == 1) timeP1 += addTime;
               movesP1++;
               turn = 1;
               showTime();
           }
       }
       lastP1 = p1Pressed;
       bool p2Pressed = digitalRead(BTN_P2) == LOW;
       if (p2Pressed && !lastP2) {
           beep(20, BEEP_BUTTON);
           if (turn == 1) {
               long elapsed = now - lastTickTime;
               
               if (gameMode == 2) {
                   if (elapsed > delaySec * 1000L) {
                       long extraTime = elapsed - (delaySec * 1000L);
                       int secondsElapsed = extraTime / 1000;
                       if (secondsElapsed > 0) {
                           timeP2 -= secondsElapsed;
                           if (timeP2 <= 0) { gameOver(2); return; }
                           lastTickTime += secondsElapsed * 1000;
                       }
                   }
                   lastTickTime = now;
               } else {
                   int secondsElapsed = elapsed / 1000;
                   if (secondsElapsed > 0) {
                       timeP2 -= secondsElapsed;
                       if (timeP2 <= 0) { gameOver(2); return; }
                       lastTickTime += secondsElapsed * 1000;
                   }
               }
               
               if (gameMode == 1) timeP2 += addTime;
               movesP2++;
               turn = 0;
               showTime();
           }
       }
       lastP2 = p2Pressed;
   }
   if (gameStarted && !paused) {
       long elapsed = millis() - lastTickTime;
       
       if (gameMode == 2) {
           if (elapsed > delaySec * 1000L) {
               long activeTime = elapsed - (delaySec * 1000L);
               int secondsElapsed = activeTime / 1000;
               if (secondsElapsed > 0) {
                   if (turn == 0) {
                       timeP1 -= secondsElapsed;
                       if (timeP1 <= 0) { gameOver(1); return; }
                   } else {
                       timeP2 -= secondsElapsed;
                       if (timeP2 <= 0) { gameOver(2); return; }
                   }
                   lastTickTime += secondsElapsed * 1000;
                   showTime();
               }
           }
       } else {
           int secondsElapsed = elapsed / 1000;
           if (secondsElapsed > 0) {
               if (turn == 0) {
                   timeP1 -= secondsElapsed;
                   if (timeP1 <= 0) { gameOver(1); return; }
               } else {
                   timeP2 -= secondsElapsed;
                   if (timeP2 <= 0) { gameOver(2); return; }
               }
               lastTickTime += secondsElapsed * 1000;
               showTime();
           }
       }
   }
   if (digitalRead(BTN_P1) == LOW && digitalRead(BTN_P2) == LOW) {
       if (bothStart == 0) bothStart = millis();
       if (millis() - bothStart > LONG_PRESS_TIME) {
           resetAll();
           bothStart = 0;
       }
   } else {
       bothStart = 0;
   }

}

// ==================== СТАРТ ==================== void startGame(int firstPlayer) {

   mode = 2;
   playing = true;
   paused = false;
   gameStarted = true;
   bothStart = 0;
   movesP1 = 0;
   movesP2 = 0;
   if (firstPlayer == 0) turn = 0;
   else turn = 1;
   lastTickTime = millis();
   showTime();
   beep(50, BEEP_START);

}

// ==================== ОТОБРАЖЕНИЕ ==================== void showTime() {

   bool p1Hours = (timeP1 >= 5940);
   bool p2Hours = (timeP2 >= 5940);
   
   if (p1Hours) {
       int hours = timeP1 / 3600;
       int minutes = (timeP1 % 3600) / 60;
       if (hours > 99) hours = 99;
       if (minutes > 59) minutes = 59;
       display1.showNumberDecEx(hours * 100 + minutes, 0b01000000, true, 4, 0);
   } else {
       int minutes = timeP1 / 60;
       int seconds = timeP1 % 60;
       if (minutes > 99) minutes = 99;
       if (seconds > 59) seconds = 59;
       display1.showNumberDecEx(minutes * 100 + seconds, 0b01000000, true, 4, 0);
   }
   
   if (p2Hours) {
       int hours = timeP2 / 3600;
       int minutes = (timeP2 % 3600) / 60;
       if (hours > 99) hours = 99;
       if (minutes > 59) minutes = 59;
       display2.showNumberDecEx(hours * 100 + minutes, 0b01000000, true, 4, 0);
   } else {
       int minutes = timeP2 / 60;
       int seconds = timeP2 % 60;
       if (minutes > 99) minutes = 99;
       if (seconds > 59) seconds = 59;
       display2.showNumberDecEx(minutes * 100 + seconds, 0b01000000, true, 4, 0);
   }

}

void showMenu() {

   display1.showNumberDec(menuPos + 1);
   display2.showNumberDec(menuPos + 1);

}

void showSetTime() {

   static int lastMinutes = -1;
   if (setMinutes < 100) {
       if (lastMinutes >= 100 && setMinutes < 100) display1.clear();
       display1.showNumberDecEx(setMinutes, 0b00000000, true, 2, 2);
   } else {
       if (lastMinutes < 100 && setMinutes >= 100) display1.clear();
       display1.showNumberDec(setMinutes);
   }
   lastMinutes = setMinutes;
   display2.showNumberDec(1);

}

void showSetAdd() {

   display1.showNumberDecEx(setAdd, 0b00000000, true, 2, 2);
   display2.setSegments(MODE_ADD, 4, 0);

}

void showSetDelay() {

   display1.showNumberDecEx(setDelay, 0b00000000, true, 2, 2);
   display2.setSegments(MODE_DLY, 4, 0);

}

void showSetMode() {

   if (setMode == 0) { display1.setSegments(MODE_CLS, 4, 0); display2.showNumberDec(1); }
   else if (setMode == 1) { display1.setSegments(MODE_ADD, 4, 0); display2.showNumberDec(2); }
   else if (setMode == 2) { display1.setSegments(MODE_DLY, 4, 0); display2.showNumberDec(3); }

}

void showSetBrightness() {

   display1.setSegments(MODE_bRI, 4, 0);
   display2.showNumberDec(setBrightness);

}

void showSetVolume() {

   display1.setSegments(MODE_Vol, 4, 0);
   display2.showNumberDec(setVolume / 10);

}

void gameOver(int loser) {

   playing = false;
   beep(1000, BEEP_GAME_OVER);
   for (int i = 0; i < 6; i++) {
       if (loser == 1) display1.clear();
       else display2.clear();
       delay(200);
       showTime();
       delay(200);
   }
   mode = 0;
   menuPos = 0;
   showMenu();

}

void resetAll() {

   timeP1 = (long)setMinutes * 60;
   timeP2 = timeP1;
   addTime = setAdd;
   delaySec = setDelay;
   gameMode = setMode;
   movesP1 = 0;
   movesP2 = 0;
   mode = 0;
   playing = false;
   paused = false;
   gameStarted = false;
   menuPos = 0;
   showTime();
   showMenu();
   beep(100, BEEP_RESET);

}

void beep(int duration, int frequency) {

   if (setVolume == 0) return;
   int adjustedFreq = map(setVolume, 0, 100, 500, frequency);
   if (adjustedFreq < 500) adjustedFreq = 500;
   if (adjustedFreq > frequency) adjustedFreq = frequency;
   tone(BUZZER, adjustedFreq, duration);
   delay(duration);
   noTone(BUZZER);

}

Руководитель

Бизяев Алексей Анатольевич-старший преподаватель кафедры конструирования и технологии радиоэлектронных средств, научный сотрудник научно-исследовательской лаборатории перспективных космических разработок НГТУ НЭТИ

Команда проекта

  • Козырев Андрей-электронщик
  • Хавкунова Екатерина-конструктор
  • Тельпухова Майя-журналист