335 lines
16 KiB
Python
335 lines
16 KiB
Python
# Импорт стандартных библиотек
|
||
from machine import Pin
|
||
from machine import PWM
|
||
from machine import I2C
|
||
from time import sleep
|
||
from time import ticks_ms
|
||
from neopixel import NeoPixel
|
||
from random import randrange
|
||
import _thread
|
||
|
||
# Импорт библиотеки для "OLED" дисплея
|
||
from ssd1306 import SSD1306_I2C
|
||
|
||
# Импорт пользовательских игровых файлов
|
||
import game_01
|
||
import game_02
|
||
import game_03
|
||
import game_04
|
||
|
||
# Блокировки памяти должны использоваться, когда используются два ядра и они работают с одной и той же памятью
|
||
# Однако далее я не буду их использовать
|
||
lock = _thread.allocate_lock()
|
||
|
||
# Определения объектов кнопок (input)
|
||
# Плюс глобальные переменные, которые хранят инверсированные значения состояний кнопок
|
||
# Эти переменные избыточны, просто более удобный способ чтения состояний кнопок
|
||
up = Pin(15, Pin.IN, Pin.PULL_UP)
|
||
is_up = False
|
||
down = Pin(27, Pin.IN, Pin.PULL_UP)
|
||
is_down = False
|
||
right = Pin(28, Pin.IN, Pin.PULL_UP)
|
||
is_right = False
|
||
left = Pin(26, Pin.IN, Pin.PULL_UP)
|
||
is_left = False
|
||
action_a = Pin(5, Pin.IN, Pin.PULL_UP)
|
||
is_a = False
|
||
action_b = Pin(4, Pin.IN, Pin.PULL_UP)
|
||
is_b = False
|
||
mute = Pin(1, Pin.IN, Pin.PULL_UP)
|
||
is_mute = False
|
||
main_menu = Pin(7, Pin.IN, Pin.PULL_UP)
|
||
is_menu = False
|
||
|
||
# Определения объектов светодиодов (output)
|
||
led_mute = Pin(0, Pin.OUT)
|
||
led_user_1 = Pin(14, Pin.OUT)
|
||
led_user_2 = Pin(6, Pin.OUT)
|
||
|
||
# Определения объекта АЦП
|
||
# Считывание фактического напряжения батареи не работает должным образом с текущей схемой
|
||
# Во время разряда батареи напряжение, считываемое АЦП, сначала падает, а затем начинает расти, вызывая проблемы
|
||
batt_voltage = machine.ADC(29)
|
||
read_voltage = 0
|
||
|
||
# Определения объектов I2C и дисплея
|
||
i2c = I2C(1, sda = Pin(2), scl = Pin(3), freq = 400000)
|
||
oled = SSD1306_I2C(128, 64, i2c)
|
||
|
||
# Определения объектов пищалки (PWM)
|
||
buzzer = PWM(8, freq = 440, duty_u16 = 0)
|
||
buzzer_is_on = True
|
||
|
||
# Определения объектов NeoPixel
|
||
rgb_led = Pin(16, Pin.OUT)
|
||
rgb = NeoPixel(rgb_led, 1)
|
||
color_components = [0, 0, 0]
|
||
|
||
# Последовательность звуков для экрана загрузки
|
||
# Выполняется на втором ядре, поэтому используются "sleep"
|
||
def loading_screen_tune():
|
||
#C4 нота
|
||
buzzer.freq(262)
|
||
buzzer.duty_u16(32768)
|
||
sleep(0.2)
|
||
buzzer.duty_u16(0)
|
||
sleep(0.1)
|
||
#D4 нота
|
||
buzzer.freq(294)
|
||
buzzer.duty_u16(32768)
|
||
sleep(0.2)
|
||
buzzer.duty_u16(0)
|
||
sleep(0.1)
|
||
#E4 нота
|
||
buzzer.freq(330)
|
||
buzzer.duty_u16(32768)
|
||
sleep(0.2)
|
||
buzzer.duty_u16(0)
|
||
sleep(0.1)
|
||
|
||
|
||
# АБСОЛЮТНО (не)ОБХОДИМЫЙ ЭКРАН ЗАГРУЗКИ
|
||
def loading_screen(oled, rgb, led_user_1, led_user_2, buzzer):
|
||
|
||
# Очистить экран и выключить светодиоды и зуммер на всякий случай
|
||
oled.fill(0)
|
||
oled.show()
|
||
rgb[0] = (0, 0, 0)
|
||
rgb.write()
|
||
led_user_1.value(0)
|
||
led_user_2.value(0)
|
||
buzzer.duty_u16(0)
|
||
|
||
# Эта функция запускает код на втором ядре
|
||
# В этом случае этот код просто воспроизводит короткую мелодию из трёх нот
|
||
_thread.start_new_thread(loading_screen_tune, ())
|
||
|
||
# Этот массив содержит слова, которые будут отображаться по порядку в процессе загрузки
|
||
words_to_show = ["TETRIS", "by", "SHKLJ"]
|
||
|
||
# Итерация по ранее упомянутому массиву и отображение содержимого в центре экрана, каждое на новой строке
|
||
# Именно здесь я отказываюсь от парадигмы обобщённого программирования и просто грубой силой рисую пиксели на экране
|
||
for i in range(len(words_to_show)):
|
||
oled.text(words_to_show[i], 64 - (len(words_to_show[i]) * 4), (i * 9) + 1, 1)
|
||
oled.show()
|
||
sleep(0.3)
|
||
|
||
# Нарисовать ограничивающий прямоугольник для индикатора прогресса
|
||
oled.line(10, 55, 117, 55, 1)
|
||
oled.line(10, 60, 117, 60, 1)
|
||
oled.line(10, 55, 10, 60, 1)
|
||
oled.line(117, 55, 117, 60, 1)
|
||
oled.show()
|
||
|
||
# Заполнение шкалы загрузки
|
||
for i in range(12, 116):
|
||
oled.fill_rect(i, 57, 1, 2, 1)
|
||
oled.show()
|
||
|
||
# Закрасить экран белым
|
||
oled.fill(1)
|
||
oled.show()
|
||
sleep(0.2)
|
||
oled.fill(0)
|
||
oled.show()
|
||
|
||
# Вызов и выполнение абсолютно (не)обходимого экрана загрузки
|
||
# Если вы хотите отключить "экран загрузки", просто закомментируйте эту строку
|
||
loading_screen(oled, rgb, led_user_1, led_user_2, buzzer)
|
||
|
||
# Код второго ядра
|
||
# Этот код не является строго необходимым, но отвечает за поведение кнопки отключения звука и кнопки сброса
|
||
# На самом RP2040-Zero уже есть кнопка сброса, фактически нет необходимости во второй кнопке, её можно перепрофилировать, как и кнопку отключения звука
|
||
# Ещё одна причина отказаться от второго ядра и использовать его для других задач — это то, что при каждой итерации оно отключает зуммер, если кнопка не нажата
|
||
# Это делает невозможным воспроизведение звуков на другом ядре
|
||
# Но если удалить этот код, то кнопки на главном меню перестанут работать
|
||
def second_core(up, right, left, down, action_a, action_b, mute, main_menu, buzzer):
|
||
|
||
# Определение глобальных переменных, которые будут использоваться в главном меню
|
||
# Глобальные переменные в целом плохая идея в программировании
|
||
# Особенно в многозадачных системах, таких как эта, из-за проблем с доступом к памяти
|
||
global is_up
|
||
global is_down
|
||
global is_left
|
||
global is_right
|
||
global is_a
|
||
global is_b
|
||
global is_mute
|
||
global is_menu
|
||
global buzzer_is_on
|
||
|
||
# Эта переменная необходима для обнаружения возрастующего фронта нажатия кнопки, чтобы срабатывать один раз, а не всё время
|
||
previous_mute_state = 0
|
||
|
||
while(True):
|
||
|
||
# Глупая часть кода, которая просто инвертирует состояние переменной кнопки и сохраняет в другую переменную
|
||
# Снова, если удалить, нарушится работа главного меню, кнопки отключения звука и пользовательской кнопки сброса
|
||
# Инвертирование могло бы быть выполнено на уровне железа, подтягивая входные пины и подключив кнопки к 3.3V
|
||
if (up.value() == False):
|
||
is_up = 1
|
||
else:
|
||
is_up = 0
|
||
if (down.value() == False):
|
||
is_down = 1
|
||
else:
|
||
is_down = 0
|
||
if (right.value() == False):
|
||
is_right = 1
|
||
else:
|
||
is_right = 0
|
||
if (left.value() == False):
|
||
is_left = 1
|
||
else:
|
||
is_left = 0
|
||
if (action_a.value() == False):
|
||
is_a = 1
|
||
else:
|
||
is_a = 0
|
||
if (action_b.value() == False):
|
||
is_b = 1
|
||
else:
|
||
is_b = 0
|
||
if (mute.value() == False):
|
||
is_mute = 1
|
||
else:
|
||
is_mute = 0
|
||
if (main_menu.value() == False):
|
||
is_menu = 1
|
||
else:
|
||
is_menu = 0
|
||
|
||
# Поведение кнопки отключения звука для возрастающего фронта сиграла с нажатия кнопки
|
||
if (previous_mute_state != is_mute):
|
||
if (is_mute == True):
|
||
if (led_mute.value() == 0):
|
||
led_mute.value(1)
|
||
buzzer_is_on = False
|
||
else:
|
||
led_mute.value(0)
|
||
buzzer_is_on = True
|
||
previous_mute_state = is_mute
|
||
|
||
# Этот код отвечает за воспроизведение звуков при нажатии кнопок
|
||
# Он также отключает все звуки, когда кнопки не нажаты
|
||
# Это делает невозможным воспроизведение звуков на других ядрах, когда этот код активен
|
||
if (buzzer_is_on == False):
|
||
buzzer.duty_u16(0)
|
||
else:
|
||
if (is_up):
|
||
buzzer.duty_u16(32768)
|
||
buzzer.freq(330)
|
||
elif (is_left):
|
||
buzzer.duty_u16(32768)
|
||
buzzer.freq(349)
|
||
elif (is_right):
|
||
buzzer.duty_u16(32768)
|
||
buzzer.freq(440)
|
||
elif (is_down):
|
||
buzzer.duty_u16(32768)
|
||
buzzer.freq(494)
|
||
elif (is_a):
|
||
buzzer.duty_u16(32768)
|
||
buzzer.freq(523)
|
||
elif (is_b):
|
||
buzzer.duty_u16(32768)
|
||
buzzer.freq(659)
|
||
else:
|
||
buzzer.duty_u16(0)
|
||
|
||
# Этот код перезагружает МК, когда нажата кнопка "main menu"
|
||
# Поскольку к кнопкам не подключены параллельные конденсаторы, иногда игра зависает из-за дребезга контактов
|
||
# Как уже упоминалось, эту кнопку лучше перепрофилировать для другой функции
|
||
# В качестве альтернативы можно реализовать программный таймер для сброса тетриса после длительного нажатия вместо конденсаторов
|
||
if (is_menu):
|
||
machine.reset()
|
||
|
||
# Запустить код "second_core" на втором ядре
|
||
# Второе ядро не может выполнять два потока одновременно, но к этому времени "loading_screen_tune" уже завершён
|
||
# Будьте осторожны при изменении временных интервалов последовательности экрана загрузки, так как это может привести к зависанию игры
|
||
_thread.start_new_thread(second_core, (up, right, left, down, action_a, action_b, mute, main_menu, buzzer))
|
||
|
||
# Считывает напряжение батареи один раз при запуске игры и сохраняет в переменную
|
||
read_voltage = batt_voltage.read_u16()
|
||
|
||
# Переменные, необходимые для срабатывания на возрастающий фронт нажатия кнопки в главном меню
|
||
previous_up_state = 0
|
||
previous_right_state = 0
|
||
previous_left_state = 0
|
||
previous_down_state = 0
|
||
previous_a_state = 0
|
||
previous_b_state = 0
|
||
|
||
# Начать главное меню с первым выделенным элементом
|
||
list_hightlight = 1
|
||
|
||
while (True):
|
||
|
||
# Нарисовать "main menu" и уровень батареи в верхней и нижней части экрана
|
||
oled.show()
|
||
oled.fill(0)
|
||
oled.text("Main Menu", 28, 0, 1)
|
||
oled.line(0, 8, 127, 8, 1)
|
||
oled.line(0, 54, 127, 54 ,1)
|
||
oled.text("Battery:", 0, 56, 1)
|
||
if (read_voltage > 59000):
|
||
oled.text("FULL", 64, 56, 1)
|
||
elif (read_voltage > 10000 and read_voltage <= 59000):
|
||
oled.text("NOFULL", 64, 56, 1)
|
||
elif (read_voltage <= 10000):
|
||
oled.text("NO", 64, 56, 1)
|
||
|
||
# Обрабатывать нажатия кнопок для перемещения выделения вверх и вниз с помощью кнопок направления
|
||
if (previous_up_state != is_up):
|
||
if (is_up == True):
|
||
if (list_hightlight == 1):
|
||
list_hightlight = 4
|
||
else:
|
||
list_hightlight = list_hightlight - 1
|
||
previous_up_state = is_up
|
||
if (previous_right_state != is_right):
|
||
if (is_right == True):
|
||
if (list_hightlight == 4):
|
||
list_hightlight = 1
|
||
else:
|
||
list_hightlight = list_hightlight + 1
|
||
previous_right_state = is_right
|
||
if (previous_left_state != is_left):
|
||
if (is_left == True):
|
||
if (list_hightlight == 1):
|
||
list_hightlight = 4
|
||
else:
|
||
list_hightlight = list_hightlight - 1
|
||
previous_left_state = is_left
|
||
if (previous_down_state != is_down):
|
||
if (is_down == True):
|
||
if (list_hightlight == 4):
|
||
list_hightlight = 1
|
||
else:
|
||
list_hightlight = list_hightlight + 1
|
||
previous_down_state = is_down
|
||
|
||
# Запустить функцию игры после нажатия кнопки действия в зависимости от того, какой элемент был выделен
|
||
if ((previous_a_state != is_a) or (previous_b_state != is_b)):
|
||
if ((is_a == True) or (is_b == True)):
|
||
if (list_hightlight == 1):
|
||
game_01.game_loop(oled, led_user_1, led_user_2)
|
||
elif (list_hightlight == 2):
|
||
game_02.game_loop(oled, led_user_1, led_user_2)
|
||
elif (list_hightlight == 3):
|
||
game_03.game_loop(oled, rgb)
|
||
elif (list_hightlight == 4):
|
||
game_04.game_loop(oled)
|
||
previous_a_state = is_a
|
||
previous_b_state = is_b
|
||
|
||
# Нарисовать белый прямоугольник в позиции, соответствующей выделенной игре
|
||
oled.fill_rect(0, list_hightlight * 11 - 1, 127, 10, 1)
|
||
|
||
# Нарисовать названия игр белым цветом, за исключением выделенной игры
|
||
# Всё ещё не уверен, как это работает, но это работает
|
||
oled.text(game_01.get_game_name(), 0, 11, list_hightlight - 1)
|
||
oled.text(game_02.get_game_name(), 0, 22, list_hightlight - 2)
|
||
oled.text(game_03.get_game_name(), 0, 33, list_hightlight - 3)
|
||
oled.text(game_04.get_game_name(), 0, 44, list_hightlight - 4)
|
||
|