diff --git a/micropython/game_01.py b/micropython/game_01.py new file mode 100644 index 0000000..805e200 --- /dev/null +++ b/micropython/game_01.py @@ -0,0 +1,102 @@ +# Импортировать стандартные модули, используемые в этой игре +# Модуль экрана не импортируется, потому что он передается как объект в вызываемую функцию +from time import sleep +from random import randint +from machine import Pin + +# Функция, которая возвращает название вашей игры +def get_game_name(): + game_name = "2P Pong" + return game_name + +# Основная функция игры, которая вызывается в главном меню тетриса +def game_loop(oled, led_user_1, led_user_2): + + # Определить пины кнопок, которые будут использоваться в этой игре и обрабатываться на втором ядра + up = Pin(15, Pin.IN, Pin.PULL_UP) + down = Pin(27, Pin.IN, Pin.PULL_UP) + action_a = Pin(5, Pin.IN, Pin.PULL_UP) + action_b = Pin(4, Pin.IN, Pin.PULL_UP) + + # Игровой цикл, который выполняется бесконечно, пока не будет вызвано условие сброса + while (True): + + # Хардкодированные значения начальных позиций ракетки + paddle_1_coord = 27 + paddle_2_coord = 27 + + # Хардкодированные значения начальной позиции и скорости "мяча" + x_coord = 62 + y_coord = 30 + x_speed = 2 + y_speed = 1 + + # Случайным образом назначить направление мяча в начале игры + if (randint(0, 1) == 0): + x_speed = x_speed * -1 + if (randint(0, 1) == 0): + y_speed = y_speed * -1 + + # Установить в false, когда мяч покидает игровое поле + game_continue_condition = True + + # Хранит номер игрока, который победил + winner = 0 + + # Продолжать игру, пока мяч находится на игровом поле + while (game_continue_condition): + + # Нарисовать ракетки и мяч в соответствии с их координатами + oled.fill(0) + oled.fill_rect(x_coord, y_coord, 4, 4, 1) + oled.fill_rect(0, paddle_1_coord, 4, 10, 1) + oled.fill_rect(124, paddle_2_coord, 4, 10, 1) + oled.show() + + # Изменить координаты мяча на значение скорости + x_coord = x_coord + x_speed + y_coord = y_coord + y_speed + + # Инвертировать значение скорости мяча при столкновении с ракеткой, чтобы он отразился + if ((x_coord > 120) and (y_coord + 2 >= paddle_2_coord) and (y_coord + 2 <= paddle_2_coord + 10)): + x_speed = x_speed * -1 + if ((x_coord < 8) and (y_coord + 2 >= paddle_1_coord) and (y_coord + 2 <= paddle_1_coord + 10)): + x_speed = x_speed * -1 + + # Инвертировать значение скорости мяча при столкновении с потолком и полом, чтобы он отразился + if (y_coord > 58): + y_speed = y_speed * -1 + if (y_coord < 1): + y_speed = y_speed * -1 + + # Если мяч проходит мимо ракетки, завершить игру и назначить соответствующего победителя + if (x_coord > 130): + game_continue_condition = False + winner = 1 + if (x_coord < -6): + game_continue_condition = False + winner = 2 + + # Изменить координаты ракеток, пока кнопки удерживаются + if (up.value() == 0): + if (paddle_2_coord >= 1): + paddle_2_coord = paddle_2_coord - 2 + if (down.value() == 0): + if (paddle_2_coord <= 53): + paddle_2_coord = paddle_2_coord + 2 + if (action_a.value() == 0): + if (paddle_1_coord >= 1): + paddle_1_coord = paddle_1_coord - 2 + if (action_b.value() == 0): + if (paddle_1_coord <= 53): + paddle_1_coord = paddle_1_coord + 2 + + # Очистить экран и нарисовать таблички с плашки победителей, пока не вернемся к началу игры и не перезапустится код игры + oled.fill(0) + if (winner == 1): + oled.text("PLAYER 1", 32, 24, 1) + if (winner == 2): + oled.text("PLAYER 2", 32, 24, 1) + oled.text("WON", 52, 33, 1) + oled.show() + sleep(1) \ No newline at end of file diff --git a/micropython/game_02.py b/micropython/game_02.py new file mode 100644 index 0000000..cfd7e58 --- /dev/null +++ b/micropython/game_02.py @@ -0,0 +1,69 @@ +# Импортировать стандартные модули, используемые в этой игре +# Модуль экрана не импортируется, потому что он передается как объект в вызываемую функцию +from time import sleep +from random import randrange +from machine import Pin + +# Функция, которая возвращает название вашей игры +def get_game_name(): + game_name = "Button Cowboys" + return game_name + +# Основная функция игры, которая вызывается в главном меню тетриса +def game_loop(oled, led_user_1, led_user_2): + + # Определить пины кнопок, которые будут использоваться в этой игре и обрабатываться на первом ядре + down = Pin(27, Pin.IN, Pin.PULL_UP) + action_b = Pin(4, Pin.IN, Pin.PULL_UP) + + # Игровой цикл, который выполняется бесконечно, пока не будет вызвано условие сброса из второго ядра + while(True): + + # Очистить экран и установить переменную для цикла while + oled.fill(0) + oled.show() + go_on = True + + # Нарисовать инструкции для игры и подождать 3 секунды + oled.text("PL1 - B", 0, 0, 1) + oled.text("PL2 - DOWN", 0, 10, 1) + oled.line(0, 19, 127, 19, 1) + oled.text("first one to", 0, 21, 1) + oled.text("press button", 0, 30, 1) + oled.text("after 'GO' wins", 0, 39, 1) + oled.show() + sleep(3) + + # Очистить экран и подождать случайное время перед противостоянием + oled.fill(0) + oled.text("get ready...", 16, 29, 1) + oled.show() + sleep(randrange(3, 15, 1)) + + # "Высокий полдень" и запомнить состояние кнопки, чтобы человек, удерживающий кнопку, не выигрывал автоматически + oled.fill(1) + oled.text("GO", 56, 29, 0) + previous_down_state = down.value() + previous_b_state = action_b.value() + oled.show() + + # Проверить изменение состояния кнопки, выйти из цикла while и назначить победителя соответствующим образом + while (go_on): + if (previous_b_state != action_b.value()): + led_user_2.value(1) + oled.fill(0) + oled.text("PLAYER 1 WINS", 12, 29, 1) + go_on = False + if (previous_down_state != down.value()): + led_user_1.value(1) + oled.fill(0) + oled.text("PLAYER 2 WINS", 12, 29, 1) + go_on = False + + # Показать имя победившего игрока и выключить светодиоды перед перезапуском игры + oled.show() + sleep(3) + oled.fill(0) + oled.show() + led_user_1.value(0) + led_user_2.value(0) \ No newline at end of file diff --git a/micropython/game_03.py b/micropython/game_03.py new file mode 100644 index 0000000..7bc55b4 --- /dev/null +++ b/micropython/game_03.py @@ -0,0 +1,14 @@ +# Функция, которая возвращает название вашей игры +def get_game_name(): + game_name = "Flashlight" + return game_name + +# Основная функция игры, которая вызывается в главном меню тетриса +def game_loop(oled, rgb): + + # Очистить экран и бесконечно отправлять команду для включения белого цвета на RGB-светодиоде + while(True): + oled.fill(0) + oled.show() + rgb[0] = (255, 255, 255) + rgb.write() \ No newline at end of file diff --git a/micropython/game_04.py b/micropython/game_04.py new file mode 100644 index 0000000..ed9b085 --- /dev/null +++ b/micropython/game_04.py @@ -0,0 +1,13 @@ +# Функция, которая возвращает название вашей игры +def get_game_name(): + game_name = "Your Game" + return game_name + +# Основная функция игры, которая вызывается в главном меню тетриса +def game_loop(oled): + oled.fill(0) + oled.show() + + # Бесконечный цикл, внутри этого цикла нужно написать свою игру + while (True): + pass \ No newline at end of file diff --git a/micropython/main.py b/micropython/main.py new file mode 100644 index 0000000..27fbb2b --- /dev/null +++ b/micropython/main.py @@ -0,0 +1,335 @@ +# Импорт стандартных библиотек +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) + \ No newline at end of file