1+ """Battery Tamagotchi example using BQ27441, MCP23009E D-PAD, SSD1327 OLED and buzzer.
2+
3+ The creature ages based on the real battery percentage and periodically asks
4+ to be fed or played with via the D-PAD. A correct answer triggers a happy
5+ sprite + success sound; a wrong or missed answer triggers an angry sprite +
6+ fail sound. When the battery drops below 10 %, the game is over.
7+
8+ Controls:
9+ UP / DOWN -> navigate the action menu
10+ LEFT -> confirm selection
11+ """
12+
113import random
214from time import sleep_ms , ticks_diff , ticks_ms
315
1729)
1830from pyb import Timer
1931
20- # setup screen
32+ # --- Hardware setup ---
33+
2134spi = SPI (1 )
2235dc = Pin ("DATA_COMMAND_DISPLAY" )
2336res = Pin ("RST_DISPLAY" )
2437cs = Pin ("CS_DISPLAY" )
2538display = ssd1327 .WS_OLED_128X128_SPI (spi , dc , res , cs )
2639
27- # setup mcp23009e
2840i2c = I2C (1 )
2941reset_expander = Pin ("RST_EXPANDER" , Pin .OUT )
3042mcp = MCP23009E (i2c , address = MCP23009_I2C_ADDR , reset_pin = reset_expander )
3143
32- # setup battery
3344fg = BQ27441 (i2c )
3445
35- # sound
3646buzzer_tim = Timer (1 , freq = 1000 )
3747buzzer_ch = buzzer_tim .channel (4 , Timer .PWM , pin = Pin ("SPEAKER" ))
3848buzzer_ch .pulse_width_percent (0 )
3949
40- # D-PAD button mapping
50+ # --- Constants ---
51+
4152BUTTONS = {
4253 MCP23009_BTN_UP : "UP" ,
4354 MCP23009_BTN_DOWN : "DOWN" ,
4657}
4758
4859ACTION = ["food" , "play" ]
49- NEED = ["I'm bored" , "i'm hungry" ]
60+ NEED = ["I'm bored" , "I'm hungry" ]
61+
62+ IDLE_DISPLAY_MS = 1000
63+ RESPONSE_TIMEOUT_MS = 5000
64+ RESULT_DISPLAY_MS = 1000
5065
51- # position of button
5266X0 = 35
5367ITEM_Y = 100
5468ITEM_SPACING = 14
5569
56- # sprite
70+ # --- Sprites ---
5771
5872SPRITE_BASE = [
5973 [0 , 0 , 15 , 15 , 0 , 0 , 0 , 0 , 0 , 15 , 15 , 0 , 0 ],
127141 [0 , 0 , 0 , 0 , 15 , 0 , 0 , 0 , 15 , 0 , 0 , 0 , 0 ],
128142]
129143
130- SPRITE_HANGRY = [
144+ SPRITE_ANGRY = [
131145 [0 , 0 , 15 , 15 , 0 , 0 , 0 , 0 , 0 , 15 , 15 , 0 , 0 ],
132146 [0 , 15 , 15 , 15 , 15 , 0 , 0 , 0 , 15 , 15 , 15 , 15 , 0 ],
133147 [0 , 15 , 15 , 15 , 15 , 15 , 15 , 15 , 15 , 15 , 15 , 15 , 0 ],
163177 [0 , 0 , 0 , 0 , 0 , 0 , 0 , 15 , 15 , 15 , 15 , 0 , 0 , 0 , 0 , 0 , 0 ],
164178]
165179
180+ # --- Sounds ---
181+
166182SOUND = {
167- "start" : [
168- (523 , 120 ),
169- (659 , 120 ),
170- (784 , 120 ),
183+ "start" : [
184+ (523 , 120 ),
185+ (659 , 120 ),
186+ (784 , 120 ),
171187 (1047 , 400 ),
172- ],
173-
174- "hungry" : [
175- (400 , 150 ),
176- (350 , 150 ),
177- (300 , 300 ),
178- ],
179-
180- "bored" : [
181- (500 , 120 ),
182- (650 , 120 ),
183- (800 , 200 ),
184- ],
185-
186- "success" : [
187- (600 , 100 ),
188- (800 , 100 ),
189- (1000 , 200 ),
190- ],
191-
192- "evolution" : [
193- (600 , 120 ),
194- (750 , 120 ),
195- (900 , 120 ),
196- (1100 , 150 ),
197- (1300 , 300 ),
198- ],
199-
200- "fail" : [
201- (500 , 150 ),
202- (400 , 150 ),
203- (300 , 400 ),
204- ]
205- }
206-
207- # ------------------------------------SCREEN----------------------------------------------
188+ ],
189+ "hungry" : [
190+ (400 , 150 ),
191+ (350 , 150 ),
192+ (300 , 300 ),
193+ ],
194+ "bored" : [
195+ (500 , 120 ),
196+ (650 , 120 ),
197+ (800 , 200 ),
198+ ],
199+ "success" : [
200+ (600 , 100 ),
201+ (800 , 100 ),
202+ (1000 , 200 ),
203+ ],
204+ "fail" : [
205+ (500 , 150 ),
206+ (400 , 150 ),
207+ (300 , 400 ),
208+ ],
209+ }
210+
211+ # --- Helpers ---
212+
208213
209214def setup_buttons ():
210215 """Configure all D-PAD buttons as inputs with pull-ups."""
@@ -213,84 +218,77 @@ def setup_buttons():
213218
214219
215220def wait_for_button ():
216- """Wait for a button press and return its name."""
221+ """Poll D-PAD once and return the pressed button name, or None ."""
217222 for pin_number , name in BUTTONS .items ():
218223 if mcp .get_level (pin_number ) == MCP23009_LOGIC_LOW :
219224 while mcp .get_level (pin_number ) == MCP23009_LOGIC_LOW :
220225 sleep_ms (20 )
221226 return name
222227 return None
223228
229+
224230def draw_character (cx , cy , scale , sprite ):
225- """Draw character """
231+ """Draw a scaled pixel-art sprite on the display framebuf. """
226232 fb = display .framebuf
227233 for y , row in enumerate (sprite ):
228234 for x , color in enumerate (row ):
229235 for dy in range (scale ):
230236 for dx in range (scale ):
231- fb .pixel (cx + x * scale + dx , cy + y * scale + dy , color )
237+ fb .pixel (cx + x * scale + dx , cy + y * scale + dy , color )
232238
233239
234240def create_screen (selected_index , need , sprite , charge ):
235- """displays the screen """
241+ """Render one game frame: sprite, need text, charge and action menu. """
236242 display .fill (0 )
237243 display .text (need , 25 , 20 , 15 )
238244 display .text (str (charge ), 50 , 10 , 15 )
239245
240246 if charge >= 70 :
241- scale = 1
242- x = 55
243- y = 60
247+ scale , x , y = 1 , 55 , 60
244248 elif charge >= 40 :
245- scale = 2
246- x = 45
247- y = 50
249+ scale , x , y = 2 , 45 , 50
248250 else :
249- scale = 3
250- x = 40
251- y = 40
251+ scale , x , y = 3 , 40 , 40
252252
253253 draw_character (x , y , scale , sprite )
254254
255255 for index , label in enumerate (ACTION ):
256- y = ITEM_Y + index * ITEM_SPACING
256+ row_y = ITEM_Y + index * ITEM_SPACING
257257 prefix = ">" if index == selected_index else " "
258- display .text (prefix + label , X0 , y , 15 )
258+ display .text (prefix + label , X0 , row_y , 15 )
259259
260260 display .show ()
261261
262+
262263def create_game_over_screen ():
264+ """Display the game-over screen."""
263265 display .fill (0 )
264- display .text ("Game- Over" ,25 , 20 , 15 )
266+ display .text ("Game Over" , 25 , 20 , 15 )
265267 draw_character (35 , 45 , 3 , SPRITE_DEAD )
266268 display .show ()
267- # ------------------------------------gameplay----------------------------------------------
268269
269- def action_check (selected_index , need ,win ):
270+
271+ def action_check (selected_index , need ):
272+ """Check if the selected action matches the current need."""
270273 name = ACTION [selected_index ]
271- if need == "I'm bored" and name == "play" :
272- win = True
273- return win
274- if need == "i'm hungry" and name == "food" :
275- win = True
276- return win
277- else :
278- win = False
279- return win
274+ return (need == "I'm bored" and name == "play" ) or (need == "I'm hungry" and name == "food" )
275+
280276
281277def sound_effect (name ):
282- melody = SOUND [ name ]
283- for freq , duration_ms in melody :
278+ """Play a short melody from the SOUND dictionary."""
279+ for freq , duration_ms in SOUND [ name ] :
284280 buzzer_tim .freq (freq )
285281 buzzer_ch .pulse_width_percent (10 )
286282 sleep_ms (duration_ms )
287283 buzzer_ch .pulse_width_percent (0 )
288284 sleep_ms (30 )
289285
290286
291- # ------------------------------------main----------------------------------------------
287+ # --- Main game loop ---
288+
292289
293290def main ():
291+ """Run the Tamagotchi game."""
294292 setup_buttons ()
295293 sound_effect ("start" )
296294 is_alive = True
@@ -300,52 +298,56 @@ def main():
300298 selected_index = 0
301299 charge = fg .state_of_charge ()
302300
303- need = " "
304- create_screen (selected_index , need , SPRITE_BASE , charge )
305- sleep_ms (1000 )
301+ # Idle phase
302+ create_screen (selected_index , " " , SPRITE_BASE , charge )
303+ sleep_ms (IDLE_DISPLAY_MS )
306304
305+ # Need phase
307306 need = random .choice (NEED )
308- if need == "I'm bored" :
307+ if need == "I'm bored" :
309308 sprite = SPRITE_SAD
310309 sound_effect ("bored" )
311- else :
310+ else :
312311 sprite = SPRITE_HUNGRY
313312 sound_effect ("hungry" )
314313
315314 create_screen (selected_index , need , sprite , charge )
316315
316+ # Response phase
317317 start = ticks_ms ()
318318 win = None
319319
320320 while True :
321- timer = ticks_diff (ticks_ms (), start )
322- if timer >= 5000 :
321+ elapsed = ticks_diff (ticks_ms (), start )
322+ if elapsed >= RESPONSE_TIMEOUT_MS :
323323 break
324324
325-
326325 button = wait_for_button ()
327326 if button == "UP" :
328- selected_index = (selected_index - 1 )% len (ACTION )
327+ selected_index = (selected_index - 1 ) % len (ACTION )
329328 create_screen (selected_index , need , sprite , charge )
330329 elif button == "DOWN" :
331330 selected_index = (selected_index + 1 ) % len (ACTION )
332331 create_screen (selected_index , need , sprite , charge )
333332 elif button == "LEFT" :
334- win = action_check (selected_index , need , win )
333+ win = action_check (selected_index , need )
335334 break
336335 sleep_ms (20 )
337336
337+ # Result phase
338338 if win :
339339 create_screen (selected_index , need , SPRITE_HAPPY , charge )
340340 sound_effect ("success" )
341341 else :
342- create_screen (selected_index , need , SPRITE_HANGRY , charge )
342+ create_screen (selected_index , need , SPRITE_ANGRY , charge )
343343 sound_effect ("fail" )
344- sleep_ms (1000 )
344+ sleep_ms (RESULT_DISPLAY_MS )
345345
346346 if charge < 10 :
347347 is_alive = False
348348 create_game_over_screen ()
349349 finally :
350350 buzzer_ch .pulse_width_percent (0 )
351+
352+
351353main ()
0 commit comments