Skip to content

Commit e810d3f

Browse files
committed
initial code for Blinka Says game.
1 parent 44a5992 commit e810d3f

File tree

1 file changed

+366
-0
lines changed

1 file changed

+366
-0
lines changed

Feather_TFT_Blinka_Says/code.py

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
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+
# a global variable to hold the eventual high-score
55+
highscore = None
56+
57+
try:
58+
# read data from NVM storage
59+
read_data = nvm_helper.read_data()
60+
# if we found data check if it's a high-score value
61+
if isinstance(read_data, list) and read_data[0] == "bls_hs":
62+
# it is a high-score so populate the label with its value
63+
highscore = read_data[1]
64+
except EOFError:
65+
# no high-score data
66+
pass
67+
68+
# display setup
69+
display = board.DISPLAY
70+
main_group = Group()
71+
72+
# Label to show the "High" score label
73+
highscore_lbl = Label(terminalio.FONT, text="High ", scale=2)
74+
highscore_lbl.anchor_point = (1.0, 0.0)
75+
highscore_lbl.anchored_position = (display.width - 4, 4)
76+
main_group.append(highscore_lbl)
77+
78+
# Label to show the "Current" score label
79+
curscore_lbl = Label(terminalio.FONT, text="Current", scale=2)
80+
curscore_lbl.anchor_point = (0.0, 0.0)
81+
curscore_lbl.anchored_position = (4, 4)
82+
main_group.append(curscore_lbl)
83+
84+
# Label to show the current score numerical value
85+
curscore_val = Label(terminalio.FONT, text="0", scale=4)
86+
curscore_val.anchor_point = (0.0, 0.0)
87+
curscore_val.anchored_position = (4,
88+
curscore_lbl.bounding_box[1] +
89+
(curscore_lbl.bounding_box[3] * curscore_lbl.scale)
90+
+ 10)
91+
main_group.append(curscore_val)
92+
93+
# Label to show the high score numerical value
94+
highscore_val = Label(terminalio.FONT, text="" if highscore is None else str(highscore), scale=4)
95+
highscore_val.anchor_point = (1.0, 0.0)
96+
highscore_val.anchored_position = (display.width - 4,
97+
highscore_lbl.bounding_box[1] +
98+
highscore_lbl.bounding_box[3] * curscore_lbl.scale
99+
+ 10)
100+
main_group.append(highscore_val)
101+
102+
# Label to show the "Game Over" message.
103+
game_over_lbl = Label(terminalio.FONT, text="Game Over", scale=3)
104+
game_over_lbl.anchor_point = (0.5, 1.0)
105+
game_over_lbl.anchored_position = (display.width // 2, display.height - 4)
106+
game_over_lbl.hidden = True
107+
main_group.append(game_over_lbl)
108+
109+
# set the main_group to show on the display
110+
display.root_group = main_group
111+
112+
113+
class GameState:
114+
"""
115+
Class that stores all the information about the game state.
116+
Used for keeping track of everything and sharing it between
117+
the asyncio tasks.
118+
"""
119+
def __init__(self, difficulty: int, led_off_time: int, led_on_time: int):
120+
# how many blinks per sequence
121+
self.difficulty = difficulty
122+
123+
# how long the LED should spend off during a blink
124+
self.led_off_time = led_off_time
125+
126+
# how long the LED should spend on during a blink
127+
self.led_on_time = led_on_time
128+
129+
# the player's current score
130+
self.score = 0
131+
132+
# the current state for the state machine that controls how the game behaves.
133+
self.current_state = STATE_WAITING_TO_START
134+
135+
# list to hold the sequence of colors that have been chosen
136+
self.sequence = []
137+
138+
# the current index within the sequence
139+
self.index = 0
140+
141+
# a timestamp that will be used to ignore button presses for a short period of time
142+
# to avoid accidental double presses.
143+
self.btn_cooldown_time = -1
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+
# access the global highscore variable
157+
global highscore # pylint: disable=global-statement
158+
159+
# loop forever inside of this task
160+
while True:
161+
# get any events that have occurred from the keypad object
162+
key_event = buttons.events.get()
163+
164+
# if we're Waiting To Start
165+
if game_state.current_state == STATE_WAITING_TO_START:
166+
167+
# if the buttons aren't locked out for cool down
168+
if game_state.btn_cooldown_time < time.monotonic():
169+
170+
# if there is a released event on any key
171+
if key_event and key_event.released:
172+
173+
# hide the game over label
174+
game_over_lbl.hidden = True
175+
176+
# show the starting score
177+
curscore_val.text = str(game_state.score)
178+
print("Starting game!")
179+
# ready set go blinks
180+
for _, led_obj in leds.items():
181+
led_obj.value = True
182+
await asyncio.sleep(250 / 1000)
183+
for _, led_obj in leds.items():
184+
led_obj.value = False
185+
await asyncio.sleep(250 / 1000)
186+
for _, led_obj in leds.items():
187+
led_obj.value = True
188+
await asyncio.sleep(250 / 1000)
189+
for _, led_obj in leds.items():
190+
led_obj.value = False
191+
await asyncio.sleep(250 / 1000)
192+
for _, led_obj in leds.items():
193+
led_obj.value = True
194+
await asyncio.sleep(250 / 1000)
195+
for _, led_obj in leds.items():
196+
led_obj.value = False
197+
198+
# change the state to Blinka's Turn
199+
game_state.current_state = STATE_BLINKA_TURN
200+
201+
# if it's Blinka's Turn
202+
elif game_state.current_state == STATE_BLINKA_TURN:
203+
# ignore buttons on Blinka's turn
204+
pass
205+
206+
# if it's the Player's Turn
207+
elif game_state.current_state == STATE_PLAYER_TURN:
208+
209+
# if a button has been pressed
210+
if key_event and key_event.pressed:
211+
# light up the corresponding LED in the button
212+
leds[COLORS[key_event.key_number]].value = True
213+
214+
# if a button has been released
215+
if key_event and key_event.released:
216+
# turn off the corresponding LED in the button
217+
leds[COLORS[key_event.key_number]].value = False
218+
#print(key_event)
219+
#print(game_state.sequence)
220+
221+
# if the color of the button pressed matches the current color in the sequence
222+
if COLORS[key_event.key_number] == game_state.sequence[0]:
223+
224+
# remove the current color from the sequence
225+
game_state.sequence.pop(0)
226+
227+
# increment the score value
228+
game_state.score += 1
229+
230+
# update the score label
231+
curscore_val.text = str(game_state.score)
232+
233+
# if there are no colors left in the sequence
234+
# i.e. the level is complete
235+
if len(game_state.sequence) == 0:
236+
237+
# give a bonus point for finishing the level
238+
game_state.score += 1
239+
240+
# increase the difficulty for next level
241+
game_state.difficulty += 1
242+
243+
# update the score label
244+
curscore_val.text = str(game_state.score)
245+
246+
# change the state to Blinka's Turn
247+
game_state.current_state = STATE_BLINKA_TURN
248+
print(f"difficulty after lvl: {game_state.difficulty}")
249+
250+
# The pressed button color does not match the current color in the sequence
251+
# i.e. player pressed the wrong button
252+
else:
253+
print("player lost!")
254+
# show the game over label
255+
game_over_lbl.hidden = False
256+
257+
# if the player's current score is higher than the highscore
258+
if highscore is None or game_state.score > highscore:
259+
260+
# save new high score value to NVM storage
261+
nvm_helper.save_data(("bls_hs", game_state.score), test_run=False)
262+
263+
# update global highscore variable to the players score
264+
highscore = game_state.score
265+
266+
# update the high score label
267+
highscore_val.text = str(game_state.score)
268+
269+
# change to Waiting to Start
270+
game_state.current_state = STATE_WAITING_TO_START
271+
272+
# reset the current score to zero
273+
game_state.score = 0
274+
275+
# reset the difficulty to 1
276+
game_state.difficulty = 1
277+
278+
# enable the button cooldown timer to ignore any button presses
279+
# in the near future to avoid double presses
280+
game_state.btn_cooldown_time = time.monotonic() + 1.5
281+
282+
# reset the sequence to an empty list
283+
game_state.sequence = []
284+
285+
# sleep, allowing other asyncio tasks to take action
286+
await asyncio.sleep(0)
287+
288+
289+
async def blinka_action(game_state: GameState):
290+
"""
291+
Choose colors randomly to add to the sequence. Blink the LEDs in accordance
292+
with the sequence.
293+
294+
:param game_state: The GameState object that holds the current state of the game.
295+
:return: None
296+
"""
297+
298+
# loop forever inside of this task
299+
while True:
300+
# if it's Blinka's Turn
301+
if game_state.current_state == STATE_BLINKA_TURN:
302+
print(f"difficulty start of blinka turn: {game_state.difficulty}")
303+
304+
# if the sequence is empty
305+
if len(game_state.sequence) == 0:
306+
307+
# loop for the current difficulty
308+
for _ in range(game_state.difficulty):
309+
# append a random color to the sequence
310+
game_state.sequence.append(random.choice(COLORS))
311+
print(game_state.sequence)
312+
313+
# wait for LED_OFF amount of time
314+
await asyncio.sleep(game_state.led_off_time / 1000)
315+
316+
# turn on the LED for the current color in the sequence
317+
leds[game_state.sequence[game_state.index]].value = True
318+
319+
# wait for LED_ON amount of time
320+
await asyncio.sleep(game_state.led_on_time / 1000)
321+
322+
# turn off the LED for the current color in the sequence
323+
leds[game_state.sequence[game_state.index]].value = False
324+
325+
# wait for LED_OFF amount of time
326+
await asyncio.sleep(game_state.led_off_time / 1000)
327+
328+
# increment the index
329+
game_state.index += 1
330+
331+
# if the last index in the sequence has been passed
332+
if game_state.index >= len(game_state.sequence):
333+
334+
# reset the index to zero
335+
game_state.index = 0
336+
337+
# change to the Players Turn
338+
game_state.current_state = STATE_PLAYER_TURN
339+
print("players turn!")
340+
341+
# sleep, allowing other asyncio tasks to take action
342+
await asyncio.sleep(0)
343+
344+
345+
async def main():
346+
"""
347+
Main asyncio task that will initialize the Game State and
348+
start the other tasks running.
349+
350+
:return: None
351+
"""
352+
353+
# initialize the Game State
354+
game_state = GameState(1, 500, 500)
355+
356+
# initialze player task
357+
player_task = asyncio.create_task(player_action(game_state))
358+
359+
# initialize blinka task
360+
blinka_task = asyncio.create_task(blinka_action(game_state))
361+
362+
# start the tasks running
363+
await asyncio.gather(player_task, blinka_task)
364+
365+
# run the main task
366+
asyncio.run(main())

0 commit comments

Comments
 (0)