Skip to content

Commit 344d7f7

Browse files
authored
Merge pull request #2880 from FoamyGuy/blinka_says
Blinka Says Game
2 parents 44a5992 + 40a0ccb commit 344d7f7

File tree

1 file changed

+368
-0
lines changed

1 file changed

+368
-0
lines changed

Feather_TFT_Blinka_Says/code.py

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
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

Comments
 (0)