|
| 1 | +# SPDX-FileCopyrightText: 2024 Tim Cocks |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: MIT |
| 4 | +""" |
| 5 | +Blinka Says - A game inspired by Simon. Test your memory by |
| 6 | +following along to the pattern that Blinka puts forth. |
| 7 | +
|
| 8 | +This project uses asyncio for cooperative multitasking |
| 9 | +through tasks. There is one task for the players actions |
| 10 | +and another for Blinka's actions. |
| 11 | +
|
| 12 | +The player action reads input from the buttons being |
| 13 | +pressed by the player and reacts to them as appropriate. |
| 14 | +
|
| 15 | +The Blinka action blinks the randomized sequence that |
| 16 | +the player must then try to follow and replicate. |
| 17 | +""" |
| 18 | +import random |
| 19 | +import time |
| 20 | + |
| 21 | +import asyncio |
| 22 | +import board |
| 23 | +from digitalio import DigitalInOut, Direction |
| 24 | +from displayio import Group |
| 25 | +import keypad |
| 26 | +import terminalio |
| 27 | + |
| 28 | +from adafruit_display_text.bitmap_label import Label |
| 29 | +import foamyguy_nvm_helper as nvm_helper |
| 30 | + |
| 31 | +# State Machine variables |
| 32 | +STATE_WAITING_TO_START = 0 |
| 33 | +STATE_PLAYER_TURN = 1 |
| 34 | +STATE_BLINKA_TURN = 2 |
| 35 | + |
| 36 | +# list of color shortcut letters |
| 37 | +COLORS = ("Y", "G", "R", "B") |
| 38 | + |
| 39 | +# keypad initialization to read the button pins |
| 40 | +buttons = keypad.Keys( |
| 41 | + (board.D5, board.D6, board.D9, board.D10), value_when_pressed=False, pull=True) |
| 42 | + |
| 43 | +# Init LED output pins |
| 44 | +leds = { |
| 45 | + "Y": DigitalInOut(board.A0), |
| 46 | + "G": DigitalInOut(board.A1), |
| 47 | + "R": DigitalInOut(board.A3), |
| 48 | + "B": DigitalInOut(board.A2) |
| 49 | +} |
| 50 | + |
| 51 | +for color in COLORS: |
| 52 | + leds[color].direction = Direction.OUTPUT |
| 53 | + |
| 54 | +# display setup |
| 55 | +display = board.DISPLAY |
| 56 | +main_group = Group() |
| 57 | + |
| 58 | +# Label to show the "High" score label |
| 59 | +highscore_lbl = Label(terminalio.FONT, text="High ", scale=2) |
| 60 | +highscore_lbl.anchor_point = (1.0, 0.0) |
| 61 | +highscore_lbl.anchored_position = (display.width - 4, 4) |
| 62 | +main_group.append(highscore_lbl) |
| 63 | + |
| 64 | +# Label to show the "Current" score label |
| 65 | +curscore_lbl = Label(terminalio.FONT, text="Current", scale=2) |
| 66 | +curscore_lbl.anchor_point = (0.0, 0.0) |
| 67 | +curscore_lbl.anchored_position = (4, 4) |
| 68 | +main_group.append(curscore_lbl) |
| 69 | + |
| 70 | +# Label to show the current score numerical value |
| 71 | +curscore_val = Label(terminalio.FONT, text="0", scale=4) |
| 72 | +curscore_val.anchor_point = (0.0, 0.0) |
| 73 | +curscore_val.anchored_position = (4, |
| 74 | + curscore_lbl.bounding_box[1] + |
| 75 | + (curscore_lbl.bounding_box[3] * curscore_lbl.scale) |
| 76 | + + 10) |
| 77 | +main_group.append(curscore_val) |
| 78 | + |
| 79 | +# Label to show the high score numerical value |
| 80 | +highscore_val = Label(terminalio.FONT, text="0", scale=4) |
| 81 | +highscore_val.anchor_point = (1.0, 0.0) |
| 82 | +highscore_val.anchored_position = (display.width - 4, |
| 83 | + highscore_lbl.bounding_box[1] + |
| 84 | + highscore_lbl.bounding_box[3] * curscore_lbl.scale |
| 85 | + + 10) |
| 86 | +main_group.append(highscore_val) |
| 87 | + |
| 88 | +# Label to show the "Game Over" message. |
| 89 | +game_over_lbl = Label(terminalio.FONT, text="Game Over", scale=3) |
| 90 | +game_over_lbl.anchor_point = (0.5, 1.0) |
| 91 | +game_over_lbl.anchored_position = (display.width // 2, display.height - 4) |
| 92 | +game_over_lbl.hidden = True |
| 93 | +main_group.append(game_over_lbl) |
| 94 | + |
| 95 | +# set the main_group to show on the display |
| 96 | +display.root_group = main_group |
| 97 | + |
| 98 | + |
| 99 | +class GameState: |
| 100 | + """ |
| 101 | + Class that stores all the information about the game state. |
| 102 | + Used for keeping track of everything and sharing it between |
| 103 | + the asyncio tasks. |
| 104 | + """ |
| 105 | + def __init__(self, difficulty: int, led_off_time: int, led_on_time: int): |
| 106 | + # how many blinks per sequence |
| 107 | + self.difficulty = difficulty |
| 108 | + |
| 109 | + # how long the LED should spend off during a blink |
| 110 | + self.led_off_time = led_off_time |
| 111 | + |
| 112 | + # how long the LED should spend on during a blink |
| 113 | + self.led_on_time = led_on_time |
| 114 | + |
| 115 | + # the player's current score |
| 116 | + self.score = 0 |
| 117 | + |
| 118 | + # the current state for the state machine that controls how the game behaves. |
| 119 | + self.current_state = STATE_WAITING_TO_START |
| 120 | + |
| 121 | + # list to hold the sequence of colors that have been chosen |
| 122 | + self.sequence = [] |
| 123 | + |
| 124 | + # the current index within the sequence |
| 125 | + self.index = 0 |
| 126 | + |
| 127 | + # a timestamp that will be used to ignore button presses for a short period of time |
| 128 | + # to avoid accidental double presses. |
| 129 | + self.btn_cooldown_time = -1 |
| 130 | + |
| 131 | + # a variable to hold the eventual high-score |
| 132 | + self.highscore = None |
| 133 | + |
| 134 | + try: |
| 135 | + # read data from NVM storage |
| 136 | + read_data = nvm_helper.read_data() |
| 137 | + # if we found data check if it's a high-score value |
| 138 | + if isinstance(read_data, list) and read_data[0] == "bls_hs": |
| 139 | + # it is a high-score so populate the label with its value |
| 140 | + self.highscore = read_data[1] |
| 141 | + except EOFError: |
| 142 | + # no high-score data |
| 143 | + pass |
| 144 | + |
| 145 | + |
| 146 | +async def player_action(game_state: GameState): |
| 147 | + """ |
| 148 | + Read the buttons to determine if the player has pressed any of them, and react |
| 149 | + appropriately if so. |
| 150 | +
|
| 151 | + :param game_state: The GameState object that holds the current state of the game. |
| 152 | + :return: None |
| 153 | + """ |
| 154 | + # pylint: disable=too-many-branches, too-many-statements |
| 155 | + |
| 156 | + # loop forever inside of this task |
| 157 | + while True: |
| 158 | + # get any events that have occurred from the keypad object |
| 159 | + key_event = buttons.events.get() |
| 160 | + |
| 161 | + # if we're Waiting To Start |
| 162 | + if game_state.current_state == STATE_WAITING_TO_START: |
| 163 | + |
| 164 | + # if the buttons aren't locked out for cool down |
| 165 | + if game_state.btn_cooldown_time < time.monotonic(): |
| 166 | + |
| 167 | + # if there is a released event on any key |
| 168 | + if key_event and key_event.released: |
| 169 | + |
| 170 | + # hide the game over label |
| 171 | + game_over_lbl.hidden = True |
| 172 | + |
| 173 | + # show the starting score |
| 174 | + curscore_val.text = str(game_state.score) |
| 175 | + print("Starting game!") |
| 176 | + # ready set go blinks |
| 177 | + for _, led_obj in leds.items(): |
| 178 | + led_obj.value = True |
| 179 | + await asyncio.sleep(250 / 1000) |
| 180 | + for _, led_obj in leds.items(): |
| 181 | + led_obj.value = False |
| 182 | + await asyncio.sleep(250 / 1000) |
| 183 | + for _, led_obj in leds.items(): |
| 184 | + led_obj.value = True |
| 185 | + await asyncio.sleep(250 / 1000) |
| 186 | + for _, led_obj in leds.items(): |
| 187 | + led_obj.value = False |
| 188 | + await asyncio.sleep(250 / 1000) |
| 189 | + for _, led_obj in leds.items(): |
| 190 | + led_obj.value = True |
| 191 | + await asyncio.sleep(250 / 1000) |
| 192 | + for _, led_obj in leds.items(): |
| 193 | + led_obj.value = False |
| 194 | + |
| 195 | + # change the state to Blinka's Turn |
| 196 | + game_state.current_state = STATE_BLINKA_TURN |
| 197 | + |
| 198 | + # if it's Blinka's Turn |
| 199 | + elif game_state.current_state == STATE_BLINKA_TURN: |
| 200 | + # ignore buttons on Blinka's turn |
| 201 | + pass |
| 202 | + |
| 203 | + # if it's the Player's Turn |
| 204 | + elif game_state.current_state == STATE_PLAYER_TURN: |
| 205 | + |
| 206 | + # if a button has been pressed |
| 207 | + if key_event and key_event.pressed: |
| 208 | + # light up the corresponding LED in the button |
| 209 | + leds[COLORS[key_event.key_number]].value = True |
| 210 | + |
| 211 | + # if a button has been released |
| 212 | + if key_event and key_event.released: |
| 213 | + # turn off the corresponding LED in the button |
| 214 | + leds[COLORS[key_event.key_number]].value = False |
| 215 | + #print(key_event) |
| 216 | + #print(game_state.sequence) |
| 217 | + |
| 218 | + # if the color of the button pressed matches the current color in the sequence |
| 219 | + if COLORS[key_event.key_number] == game_state.sequence[0]: |
| 220 | + |
| 221 | + # remove the current color from the sequence |
| 222 | + game_state.sequence.pop(0) |
| 223 | + |
| 224 | + # increment the score value |
| 225 | + game_state.score += 1 |
| 226 | + |
| 227 | + # update the score label |
| 228 | + curscore_val.text = str(game_state.score) |
| 229 | + |
| 230 | + # if there are no colors left in the sequence |
| 231 | + # i.e. the level is complete |
| 232 | + if len(game_state.sequence) == 0: |
| 233 | + |
| 234 | + # give a bonus point for finishing the level |
| 235 | + game_state.score += 1 |
| 236 | + |
| 237 | + # increase the difficulty for next level |
| 238 | + game_state.difficulty += 1 |
| 239 | + |
| 240 | + # update the score label |
| 241 | + curscore_val.text = str(game_state.score) |
| 242 | + |
| 243 | + # change the state to Blinka's Turn |
| 244 | + game_state.current_state = STATE_BLINKA_TURN |
| 245 | + print(f"difficulty after lvl: {game_state.difficulty}") |
| 246 | + |
| 247 | + # The pressed button color does not match the current color in the sequence |
| 248 | + # i.e. player pressed the wrong button |
| 249 | + else: |
| 250 | + print("player lost!") |
| 251 | + # show the game over label |
| 252 | + game_over_lbl.hidden = False |
| 253 | + |
| 254 | + # if the player's current score is higher than the highscore |
| 255 | + if game_state.highscore is None or game_state.score > game_state.highscore: |
| 256 | + |
| 257 | + # save new high score value to NVM storage |
| 258 | + nvm_helper.save_data(("bls_hs", game_state.score), test_run=False) |
| 259 | + |
| 260 | + # update highscore variable to the players score |
| 261 | + game_state.highscore = game_state.score |
| 262 | + |
| 263 | + # update the high score label |
| 264 | + highscore_val.text = str(game_state.score) |
| 265 | + |
| 266 | + # change to Waiting to Start |
| 267 | + game_state.current_state = STATE_WAITING_TO_START |
| 268 | + |
| 269 | + # reset the current score to zero |
| 270 | + game_state.score = 0 |
| 271 | + |
| 272 | + # reset the difficulty to 1 |
| 273 | + game_state.difficulty = 1 |
| 274 | + |
| 275 | + # enable the button cooldown timer to ignore any button presses |
| 276 | + # in the near future to avoid double presses |
| 277 | + game_state.btn_cooldown_time = time.monotonic() + 1.5 |
| 278 | + |
| 279 | + # reset the sequence to an empty list |
| 280 | + game_state.sequence = [] |
| 281 | + |
| 282 | + # sleep, allowing other asyncio tasks to take action |
| 283 | + await asyncio.sleep(0) |
| 284 | + |
| 285 | + |
| 286 | +async def blinka_action(game_state: GameState): |
| 287 | + """ |
| 288 | + Choose colors randomly to add to the sequence. Blink the LEDs in accordance |
| 289 | + with the sequence. |
| 290 | +
|
| 291 | + :param game_state: The GameState object that holds the current state of the game. |
| 292 | + :return: None |
| 293 | + """ |
| 294 | + |
| 295 | + # loop forever inside of this task |
| 296 | + while True: |
| 297 | + # if it's Blinka's Turn |
| 298 | + if game_state.current_state == STATE_BLINKA_TURN: |
| 299 | + print(f"difficulty start of blinka turn: {game_state.difficulty}") |
| 300 | + |
| 301 | + # if the sequence is empty |
| 302 | + if len(game_state.sequence) == 0: |
| 303 | + |
| 304 | + # loop for the current difficulty |
| 305 | + for _ in range(game_state.difficulty): |
| 306 | + # append a random color to the sequence |
| 307 | + game_state.sequence.append(random.choice(COLORS)) |
| 308 | + print(game_state.sequence) |
| 309 | + |
| 310 | + # wait for LED_OFF amount of time |
| 311 | + await asyncio.sleep(game_state.led_off_time / 1000) |
| 312 | + |
| 313 | + # turn on the LED for the current color in the sequence |
| 314 | + leds[game_state.sequence[game_state.index]].value = True |
| 315 | + |
| 316 | + # wait for LED_ON amount of time |
| 317 | + await asyncio.sleep(game_state.led_on_time / 1000) |
| 318 | + |
| 319 | + # turn off the LED for the current color in the sequence |
| 320 | + leds[game_state.sequence[game_state.index]].value = False |
| 321 | + |
| 322 | + # wait for LED_OFF amount of time |
| 323 | + await asyncio.sleep(game_state.led_off_time / 1000) |
| 324 | + |
| 325 | + # increment the index |
| 326 | + game_state.index += 1 |
| 327 | + |
| 328 | + # if the last index in the sequence has been passed |
| 329 | + if game_state.index >= len(game_state.sequence): |
| 330 | + |
| 331 | + # reset the index to zero |
| 332 | + game_state.index = 0 |
| 333 | + |
| 334 | + # change to the Players Turn |
| 335 | + game_state.current_state = STATE_PLAYER_TURN |
| 336 | + print("players turn!") |
| 337 | + |
| 338 | + # sleep, allowing other asyncio tasks to take action |
| 339 | + await asyncio.sleep(0) |
| 340 | + |
| 341 | + |
| 342 | +async def main(): |
| 343 | + """ |
| 344 | + Main asyncio task that will initialize the Game State and |
| 345 | + start the other tasks running. |
| 346 | +
|
| 347 | + :return: None |
| 348 | + """ |
| 349 | + |
| 350 | + # initialize the Game State |
| 351 | + game_state = GameState(1, 500, 500) |
| 352 | + |
| 353 | + # if there is a saved highscore |
| 354 | + if game_state.highscore is not None: |
| 355 | + # set the highscore into it's label to show on the display |
| 356 | + highscore_val.text = str(game_state.highscore) |
| 357 | + |
| 358 | + # initialze player task |
| 359 | + player_task = asyncio.create_task(player_action(game_state)) |
| 360 | + |
| 361 | + # initialize blinka task |
| 362 | + blinka_task = asyncio.create_task(blinka_action(game_state)) |
| 363 | + |
| 364 | + # start the tasks running |
| 365 | + await asyncio.gather(player_task, blinka_task) |
| 366 | + |
| 367 | +# run the main task |
| 368 | +asyncio.run(main()) |
0 commit comments