Шахматные часы с тремя режимами контроля времени: различия между версиями
Maya (обсуждение | вклад) Maya переименовал страницу Шахматные часы с тремя режимами контроля времени в Трёхрежимные шахматные часы Метка: новое перенаправление |
Maya (обсуждение | вклад) |
||
| (не показано 9 промежуточных версий этого же участника) | |||
| Строка 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 секунды) — полный сброс.
Код программы
- 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);
}
Руководитель
Бизяев Алексей Анатольевич-старший преподаватель кафедры конструирования и технологии радиоэлектронных средств, научный сотрудник научно-исследовательской лаборатории перспективных космических разработок НГТУ НЭТИ
Команда проекта
- Козырев Андрей-электронщик
- Хавкунова Екатерина-конструктор
- Тельпухова Майя-журналист