Шахматные часы с тремя режимами контроля времени: различия между версиями
Maya (обсуждение | вклад) Нет описания правки |
Maya (обсуждение | вклад) |
||
| (не показано 5 промежуточных версий этого же участника) | |||
| Строка 1: | Строка 1: | ||
== Общие сведения == | == Общие сведения == | ||
Шахматы — это не только стратегия и логика, но и строгий регламент. В соревнованиях любого уровня действует контроль времени: у каждого игрока есть лимит на партию, и превысить его нельзя. Для этого существуют специальные шахматные часы. Они фиксируют ходы, переключают время между соперниками и подают сигнал об окончании партии. | *Шахматы — это не только стратегия и логика, но и строгий регламент. В соревнованиях любого уровня действует контроль времени: у каждого игрока есть лимит на партию, и превысить его нельзя. Для этого существуют специальные шахматные часы. Они фиксируют ходы, переключают время между соперниками и подают сигнал об окончании партии. | ||
Готовые фабричные часы с полным набором функций стоят относительно дорого, а доступные по цене модели часто оказываются неудобными или ненадёжными. В данном проекте ставится задача разработать собственный вариант шахматных часов — на доступной элементной базе, с понятной схемой и возможностью повторения. | *Готовые фабричные часы с полным набором функций стоят относительно дорого, а доступные по цене модели часто оказываются неудобными или ненадёжными. В данном проекте ставится задача разработать собственный вариант шахматных часов — на доступной элементной базе, с понятной схемой и возможностью повторения. | ||
== Краткая историческая справка == | == Краткая историческая справка == | ||
| Строка 11: | Строка 11: | ||
== Основные требования к современным электронным шахматным часам== | == Основные требования к современным электронным шахматным часам== | ||
Точность хода — без неё невозможно объективно определить победителя при истечении времени. | *Точность хода — без неё невозможно объективно определить победителя при истечении времени. | ||
Эргономика — кнопка должна давать чёткий тактильный отклик, чтобы игрок чувствовал переключение хода даже не глядя на часы. | *Эргономика — кнопка должна давать чёткий тактильный отклик, чтобы игрок чувствовал переключение хода даже не глядя на часы. | ||
Видимость — экран должен быть читаем под любым углом и не бликовать, чтобы игрок мгновенно считывал время боковым зрением. | *Видимость — экран должен быть читаем под любым углом и не бликовать, чтобы игрок мгновенно считывал время боковым зрением. | ||
Интуитивная настройка — интерфейс должен быть понятен без инструкции, чтобы судья мог быстро восстановить время при сбое. | *Интуитивная настройка — интерфейс должен быть понятен без инструкции, чтобы судья мог быстро восстановить время при сбое. | ||
Устойчивость на столе — часы должны иметь достаточный вес и нескользящее основание, чтобы не сдвигаться при ударах. | *Устойчивость на столе — часы должны иметь достаточный вес и нескользящее основание, чтобы не сдвигаться при ударах. | ||
Ремонтопригодность — конструкция должна позволять замену любой детали (кнопки, дисплея, платы) без специального инструмента. | *Ремонтопригодность — конструкция должна позволять замену любой детали (кнопки, дисплея, платы) без специального инструмента. | ||
Надёжность — программа должна корректно обрабатывать дребезг контактов и исключать зависания от случайных нажатий. | *Надёжность — программа должна корректно обрабатывать дребезг контактов и исключать зависания от случайных нажатий. | ||
Автономность — часы должны долго работать от доступных источников (AA/AAA или USB), чтобы не отвлекать игроков частой заменой батарей. | *Автономность — часы должны долго работать от доступных источников (AA/AAA или USB), чтобы не отвлекать игроков частой заменой батарей. | ||
Компактность — часы должны быть удобны для перевозки в сумке с шахматным набором и не иметь хрупких выступающих элементов. | *Компактность — часы должны быть удобны для перевозки в сумке с шахматным набором и не иметь хрупких выступающих элементов. | ||
Доступная цена — стоимость не должна превышать 3000 рублей, чтобы часы были доступны массовому пользователю. | *Доступная цена — стоимость не должна превышать 3000 рублей, чтобы часы были доступны массовому пользователю. | ||
Минимализм в управлении — управление должно быть сведено к минимуму (старт, стоп, сброс), чтобы часы мог использовать любой человек без инструкции. | *Минимализм в управлении — управление должно быть сведено к минимуму (старт, стоп, сброс), чтобы часы мог использовать любой человек без инструкции. | ||
== Технические характеристики== | ==Технические характеристики== | ||
Микроконтроллер Arduino Nano | *Микроконтроллер Arduino Nano | ||
Дисплеи 2× TM1637 (4 разряда) | *Дисплеи 2× TM1637 (4 разряда) | ||
Управление Энкодер | *Управление Энкодер | ||
Кнопки игроков 2 кнопки | *Кнопки игроков 2 кнопки | ||
Звук Пьезоизлучатель (зуммер) | *Звук Пьезоизлучатель (зуммер) | ||
Питание от встроенного аккумулятора | *Питание от встроенного аккумулятора | ||
Потребление от 50 мА до 250 мА (зависит от яркости) | *Потребление от 50 мА до 250 мА (зависит от яркости) | ||
Время непрерывной работы: около 18 часов | *Время непрерывной работы: около 18 часов | ||
Примечания | *Примечания | ||
· При быстром вращении энкодера скорость изменения значений автоматически увеличивается | *· При быстром вращении энкодера скорость изменения значений автоматически увеличивается | ||
· Время сохраняется только во время работы устройства, при отключении питания сбрасывается | *· Время сохраняется только во время работы устройства, при отключении питания сбрасывается | ||
· Максимальное время партии: 999 минут (~16.5 часов) | *· Максимальное время партии: 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 <TM1637Display.h> | ||
| Строка 862: | Строка 931: | ||
noTone(BUZZER); | 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 секунды) — полный сброс.
Код программы
- 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);
}
Руководитель
Бизяев Алексей Анатольевич-старший преподаватель кафедры конструирования и технологии радиоэлектронных средств, научный сотрудник научно-исследовательской лаборатории перспективных космических разработок НГТУ НЭТИ
Команда проекта
- Козырев Андрей-электронщик
- Хавкунова Екатерина-конструктор
- Тельпухова Майя-журналист