Skip to content

Blinka Says Game #2880

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 10, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
368 changes: 368 additions & 0 deletions Feather_TFT_Blinka_Says/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
# SPDX-FileCopyrightText: 2024 Tim Cocks
#
# SPDX-License-Identifier: MIT
"""
Blinka Says - A game inspired by Simon. Test your memory by
following along to the pattern that Blinka puts forth.

This project uses asyncio for cooperative multitasking
through tasks. There is one task for the players actions
and another for Blinka's actions.

The player action reads input from the buttons being
pressed by the player and reacts to them as appropriate.

The Blinka action blinks the randomized sequence that
the player must then try to follow and replicate.
"""
import random
import time

import asyncio
import board
from digitalio import DigitalInOut, Direction
from displayio import Group
import keypad
import terminalio

from adafruit_display_text.bitmap_label import Label
import foamyguy_nvm_helper as nvm_helper

# State Machine variables
STATE_WAITING_TO_START = 0
STATE_PLAYER_TURN = 1
STATE_BLINKA_TURN = 2

# list of color shortcut letters
COLORS = ("Y", "G", "R", "B")

# keypad initialization to read the button pins
buttons = keypad.Keys(
(board.D5, board.D6, board.D9, board.D10), value_when_pressed=False, pull=True)

# Init LED output pins
leds = {
"Y": DigitalInOut(board.A0),
"G": DigitalInOut(board.A1),
"R": DigitalInOut(board.A3),
"B": DigitalInOut(board.A2)
}

for color in COLORS:
leds[color].direction = Direction.OUTPUT

# display setup
display = board.DISPLAY
main_group = Group()

# Label to show the "High" score label
highscore_lbl = Label(terminalio.FONT, text="High ", scale=2)
highscore_lbl.anchor_point = (1.0, 0.0)
highscore_lbl.anchored_position = (display.width - 4, 4)
main_group.append(highscore_lbl)

# Label to show the "Current" score label
curscore_lbl = Label(terminalio.FONT, text="Current", scale=2)
curscore_lbl.anchor_point = (0.0, 0.0)
curscore_lbl.anchored_position = (4, 4)
main_group.append(curscore_lbl)

# Label to show the current score numerical value
curscore_val = Label(terminalio.FONT, text="0", scale=4)
curscore_val.anchor_point = (0.0, 0.0)
curscore_val.anchored_position = (4,
curscore_lbl.bounding_box[1] +
(curscore_lbl.bounding_box[3] * curscore_lbl.scale)
+ 10)
main_group.append(curscore_val)

# Label to show the high score numerical value
highscore_val = Label(terminalio.FONT, text="0", scale=4)
highscore_val.anchor_point = (1.0, 0.0)
highscore_val.anchored_position = (display.width - 4,
highscore_lbl.bounding_box[1] +
highscore_lbl.bounding_box[3] * curscore_lbl.scale
+ 10)
main_group.append(highscore_val)

# Label to show the "Game Over" message.
game_over_lbl = Label(terminalio.FONT, text="Game Over", scale=3)
game_over_lbl.anchor_point = (0.5, 1.0)
game_over_lbl.anchored_position = (display.width // 2, display.height - 4)
game_over_lbl.hidden = True
main_group.append(game_over_lbl)

# set the main_group to show on the display
display.root_group = main_group


class GameState:
"""
Class that stores all the information about the game state.
Used for keeping track of everything and sharing it between
the asyncio tasks.
"""
def __init__(self, difficulty: int, led_off_time: int, led_on_time: int):
# how many blinks per sequence
self.difficulty = difficulty

# how long the LED should spend off during a blink
self.led_off_time = led_off_time

# how long the LED should spend on during a blink
self.led_on_time = led_on_time

# the player's current score
self.score = 0

# the current state for the state machine that controls how the game behaves.
self.current_state = STATE_WAITING_TO_START

# list to hold the sequence of colors that have been chosen
self.sequence = []

# the current index within the sequence
self.index = 0

# a timestamp that will be used to ignore button presses for a short period of time
# to avoid accidental double presses.
self.btn_cooldown_time = -1

# a variable to hold the eventual high-score
self.highscore = None

try:
# read data from NVM storage
read_data = nvm_helper.read_data()
# if we found data check if it's a high-score value
if isinstance(read_data, list) and read_data[0] == "bls_hs":
# it is a high-score so populate the label with its value
self.highscore = read_data[1]
except EOFError:
# no high-score data
pass


async def player_action(game_state: GameState):
"""
Read the buttons to determine if the player has pressed any of them, and react
appropriately if so.

:param game_state: The GameState object that holds the current state of the game.
:return: None
"""
# pylint: disable=too-many-branches, too-many-statements

# loop forever inside of this task
while True:
# get any events that have occurred from the keypad object
key_event = buttons.events.get()

# if we're Waiting To Start
if game_state.current_state == STATE_WAITING_TO_START:

# if the buttons aren't locked out for cool down
if game_state.btn_cooldown_time < time.monotonic():

# if there is a released event on any key
if key_event and key_event.released:

# hide the game over label
game_over_lbl.hidden = True

# show the starting score
curscore_val.text = str(game_state.score)
print("Starting game!")
# ready set go blinks
for _, led_obj in leds.items():
led_obj.value = True
await asyncio.sleep(250 / 1000)
for _, led_obj in leds.items():
led_obj.value = False
await asyncio.sleep(250 / 1000)
for _, led_obj in leds.items():
led_obj.value = True
await asyncio.sleep(250 / 1000)
for _, led_obj in leds.items():
led_obj.value = False
await asyncio.sleep(250 / 1000)
for _, led_obj in leds.items():
led_obj.value = True
await asyncio.sleep(250 / 1000)
for _, led_obj in leds.items():
led_obj.value = False

# change the state to Blinka's Turn
game_state.current_state = STATE_BLINKA_TURN

# if it's Blinka's Turn
elif game_state.current_state == STATE_BLINKA_TURN:
# ignore buttons on Blinka's turn
pass

# if it's the Player's Turn
elif game_state.current_state == STATE_PLAYER_TURN:

# if a button has been pressed
if key_event and key_event.pressed:
# light up the corresponding LED in the button
leds[COLORS[key_event.key_number]].value = True

# if a button has been released
if key_event and key_event.released:
# turn off the corresponding LED in the button
leds[COLORS[key_event.key_number]].value = False
#print(key_event)
#print(game_state.sequence)

# if the color of the button pressed matches the current color in the sequence
if COLORS[key_event.key_number] == game_state.sequence[0]:

# remove the current color from the sequence
game_state.sequence.pop(0)

# increment the score value
game_state.score += 1

# update the score label
curscore_val.text = str(game_state.score)

# if there are no colors left in the sequence
# i.e. the level is complete
if len(game_state.sequence) == 0:

# give a bonus point for finishing the level
game_state.score += 1

# increase the difficulty for next level
game_state.difficulty += 1

# update the score label
curscore_val.text = str(game_state.score)

# change the state to Blinka's Turn
game_state.current_state = STATE_BLINKA_TURN
print(f"difficulty after lvl: {game_state.difficulty}")

# The pressed button color does not match the current color in the sequence
# i.e. player pressed the wrong button
else:
print("player lost!")
# show the game over label
game_over_lbl.hidden = False

# if the player's current score is higher than the highscore
if game_state.highscore is None or game_state.score > game_state.highscore:

# save new high score value to NVM storage
nvm_helper.save_data(("bls_hs", game_state.score), test_run=False)

# update highscore variable to the players score
game_state.highscore = game_state.score

# update the high score label
highscore_val.text = str(game_state.score)

# change to Waiting to Start
game_state.current_state = STATE_WAITING_TO_START

# reset the current score to zero
game_state.score = 0

# reset the difficulty to 1
game_state.difficulty = 1

# enable the button cooldown timer to ignore any button presses
# in the near future to avoid double presses
game_state.btn_cooldown_time = time.monotonic() + 1.5

# reset the sequence to an empty list
game_state.sequence = []

# sleep, allowing other asyncio tasks to take action
await asyncio.sleep(0)


async def blinka_action(game_state: GameState):
"""
Choose colors randomly to add to the sequence. Blink the LEDs in accordance
with the sequence.

:param game_state: The GameState object that holds the current state of the game.
:return: None
"""

# loop forever inside of this task
while True:
# if it's Blinka's Turn
if game_state.current_state == STATE_BLINKA_TURN:
print(f"difficulty start of blinka turn: {game_state.difficulty}")

# if the sequence is empty
if len(game_state.sequence) == 0:

# loop for the current difficulty
for _ in range(game_state.difficulty):
# append a random color to the sequence
game_state.sequence.append(random.choice(COLORS))
print(game_state.sequence)

# wait for LED_OFF amount of time
await asyncio.sleep(game_state.led_off_time / 1000)

# turn on the LED for the current color in the sequence
leds[game_state.sequence[game_state.index]].value = True

# wait for LED_ON amount of time
await asyncio.sleep(game_state.led_on_time / 1000)

# turn off the LED for the current color in the sequence
leds[game_state.sequence[game_state.index]].value = False

# wait for LED_OFF amount of time
await asyncio.sleep(game_state.led_off_time / 1000)

# increment the index
game_state.index += 1

# if the last index in the sequence has been passed
if game_state.index >= len(game_state.sequence):

# reset the index to zero
game_state.index = 0

# change to the Players Turn
game_state.current_state = STATE_PLAYER_TURN
print("players turn!")

# sleep, allowing other asyncio tasks to take action
await asyncio.sleep(0)


async def main():
"""
Main asyncio task that will initialize the Game State and
start the other tasks running.

:return: None
"""

# initialize the Game State
game_state = GameState(1, 500, 500)

# if there is a saved highscore
if game_state.highscore is not None:
# set the highscore into it's label to show on the display
highscore_val.text = str(game_state.highscore)

# initialze player task
player_task = asyncio.create_task(player_action(game_state))

# initialize blinka task
blinka_task = asyncio.create_task(blinka_action(game_state))

# start the tasks running
await asyncio.gather(player_task, blinka_task)

# run the main task
asyncio.run(main())