diff --git a/LCDGallery/LCDGallery.py b/LCDGallery/LCDGallery.py new file mode 100644 index 00000000..9803423b --- /dev/null +++ b/LCDGallery/LCDGallery.py @@ -0,0 +1,178 @@ +# LCD Gallery +# (c) 2024 Lauren Croney +# +# Licensed Creative Commons Attribution-ShareAlike 3.0 (CC BY-SA 3.0). +# See LICENSE.txt for details. +import gc +import time +import thumby +import Games.LCDGallery.lcd as lcd +import Games.LCDGallery.skywalk as skywalk +import Games.LCDGallery.fight as fight +import Games.LCDGallery.juggle as juggle + + +class Launcher: + VERSION = const(1.0) + + def __init__(self): + t = time.ticks_us() + self.skywalk = None + self.fight = None + self.juggle = None + + # Load high scores + self.hiscore_skywalk = 0 + self.hiscore_fight = 0 + self.hiscore_juggle = 0 + if thumby.saveData.hasItem("highscore_Skywalk"): + print("Loaded high scores") + self.hiscore_skywalk = thumby.saveData.getItem("highscore_Skywalk") + if thumby.saveData.hasItem("highscore_Juggle"): + self.hiscore_juggle = thumby.saveData.getItem("highscore_Juggle") + if thumby.saveData.hasItem("highscore_Fight"): + self.hiscore_fight = thumby.saveData.getItem("highscore_Fight") + t_diff = time.ticks_diff(time.ticks_us(), t) + print('Launcher constructor took {:6.3f}ms'.format(t_diff/1000)) + + def soundtest(self): + selections = [('tick', lcd.sound_tick), + ('move', lcd.sound_move), + ('win', lcd.sound_win), + ('score', lcd.sound_score), + ('get_item', lcd.sound_item), + ('die', lcd.sound_die), + ('tie', lcd.sound_tie), + ('life', lcd.sound_life), + ('pause', lcd.sound_pause), + ('unpause', lcd.sound_unpause)] + + selection = 0 + while True: + if thumby.buttonL.justPressed() and selection > 0: + selection -= 1 + elif thumby.buttonR.justPressed() and selection < len(selections)-1: + selection += 1 + elif thumby.buttonB.justPressed(): + self.titlescreen() + elif thumby.buttonA.justPressed(): + lcd.play(selections[selection][1]) + + thumby.display.fill(1) + + if selection > 0: # left arrow + thumby.display.blit(lcd.bmp_arrow_lr, 6, 16, 5, 5, 1, 1, 0) + if selection < len(selections)-1: # right arrow + thumby.display.blit(lcd.bmp_arrow_lr, 65, 16, 5, 5, 1, 0, 0) + thumby.display.drawText("B=Menu", 1, 1, 0) + thumby.display.drawText(f"{selections[selection][0].center(10)}", + 6, 15, 0) + thumby.display.update() + + def aboutpage(self): + while True: + thumby.display.fill(1) + thumby.display.drawText("Lauren Rad", 1, 1, 0) + thumby.display.drawText("(c) 2023", 1, 10, 0) + thumby.display.drawText(f"v{Launcher.VERSION}", 1, 18, 0) + thumby.display.update() + if thumby.inputJustPressed(): + return + + def clearscores(self): + # Clear high scores + thumby.saveData.setItem("highscore_Skywalk", 0) + thumby.saveData.setItem("highscore_Juggle", 0) + thumby.saveData.setItem("highscore_Fight", 0) + self.hiscore_skywalk = 0 + self.hiscore_juggle = 0 + self.hiscore_fight = 0 + if self.skywalk is not None: + self.skywalk.hiscore = 0 # Game object's copy + if self.fight is not None: + self.fight.hiscore = 0 + if self.juggle is not None: + self.juggle.hiscore = 0 + thumby.saveData.save() + thumby.display.drawText("Cleared".center(10), 6, 25, 0) + thumby.display.update() + time.sleep(0.5) + + def titlescreen(self): + t = time.ticks_us() + selection = 0 + SELECT_SKYWALK = const(0) + SELECT_FIGHT = const(1) + SELECT_JUGGLE = const(2) + SELECT_QUIT = const(3) + SELECT_INFO = const(4) + SELECT_SOUND_TEST = const(5) + SELECT_CLEAR = const(6) + # Duel is still called Fight internally in the code + modes = ("Sky Quest", "Duel", "Juggle", "Quit", "Info", "Sound Test", + "Clear Saves") + + t_diff = time.ticks_diff(time.ticks_us(), t) + print('Titlescreen took {:6.3f}ms to load'.format(t_diff/1000)) + while True: + thumby.display.setFont("/lib/font5x7.bin", 5, 7, 1) + thumby.display.fill(1) + thumby.display.drawText("LCD Gallery!".center(12), 1, 1, 0) + thumby.display.drawText(f"{modes[selection].center(10)}", 6, 15, 0) + + if selection > 0: + thumby.display.blit(lcd.bmp_arrow_lr, 1, 16, 5, 5, 1, 1, 0) # left arrow + if selection < len(modes)-1: + thumby.display.blit(lcd.bmp_arrow_lr, 65, 16, 5, 5, 1, 0, 0) # right arrow + if selection == 0: + thumby.display.drawText(f"Hi {self.hiscore_skywalk}", 1, 32, 0) + elif selection == 1: + thumby.display.drawText(f"Hi {self.hiscore_fight}", 1, 32, 0) + elif selection == 2: + thumby.display.drawText(f"Hi {self.hiscore_juggle}", 1, 32, 0) + + thumby.display.update() + if thumby.buttonL.justPressed() and selection > 0: + selection -= 1 + elif thumby.buttonR.justPressed() and selection < len(modes)-1: + selection += 1 + + if thumby.buttonA.justPressed(): + if selection == SELECT_SKYWALK: + if self.skywalk is None: + self.skywalk = skywalk.Skywalk() + self.skywalk.hiscore = self.hiscore_skywalk + self.skywalk.main_loop() + self.hiscore_skywalk = self.skywalk.hiscore # update hiscore + elif selection == SELECT_FIGHT: + if self.fight is None: + self.fight = fight.Fight() + self.fight.hiscore = self.hiscore_fight + self.fight.main_loop() + self.hiscore_fight = self.fight.hiscore + elif selection == SELECT_JUGGLE: + if self.juggle is None: + self.juggle = juggle.Juggle() + self.juggle.hiscore = self.hiscore_juggle + self.juggle.main_loop() + self.hiscore_juggle = self.juggle.hiscore + elif selection == SELECT_QUIT: + break + elif selection == SELECT_INFO: + self.aboutpage() + elif selection == SELECT_SOUND_TEST: + self.soundtest() + elif selection == SELECT_CLEAR: + self.clearscores() + + print("Quitting...") + thumby.reset() + + +# Begin launch +print("Launching LCD Gallery...") +gc.enable() +print(f"bytes free: {gc.mem_free()}") +thumby.saveData.setName("LCDGallery") +launcher = Launcher() +launcher.titlescreen() diff --git a/LCDGallery/arcade_description.txt b/LCDGallery/arcade_description.txt new file mode 100644 index 00000000..9ca0120e --- /dev/null +++ b/LCDGallery/arcade_description.txt @@ -0,0 +1,37 @@ +LCD Gallery +Author: Lauren Croney +LCD Gallery is a game inspired by classic LCD handheld games. It includes the following three games: + +---Sky Quest--- +You must brave the magic platforms to retrieve the Helmet of Ultimate Power and return it to solid ground. Unfortunately, you're trapped in a sisyphean nightmare and must do this over and over again. + +Objective: + Move from the left side of the screen to the right to retrieve the helmet. Make it back to the cliff without falling from one of the vanishing platforms. The magic platforms will periodically begin to vanish and then reappear. If you are jumping on or standing on a platform which has vanished, you will fall and lose a life. The middle platform is a safe zone and will not vanish. After you leave the cliff, you can't go back until you get the helmet. The helmet will hover up and down; you can only grab it when it's low enough. +Controls: + Left: Move left + A: Move right + +---Duel--- +It's a classic quick-draw shootout, with a twist! Use your special space-age blaster gun to take down your opponent! + +Objective: + After the countdown, numbers will appear above you (on the left) and the computer player (right). Whoever has the larger number must try to shoot; whoever has the smaller number must try to dodge. In event of a tie, whoever shoots first wins. Each player starts with 100 hit points. The bars on the left and right show your health and the computer player's health, respectively. The player who is shot will take damage equal to double the other player's number. The game will end when you run out of hit points. +Controls: + Left: Dodge + A: Shoot + +---Juggle--- +Look at these things! I don't know what they are, but if you drop them you're fired. + +Objective: + Move the paddle into position before a thing falls to hit it back up into the air. The height that things begin to fall back down can vary. The thing on the right will sometimes get stuck in a wandering bubble; when this happens it won't fall down until the bubble pops. + +Controls: + Left: Move left + A: Move right + +------------ +During any game, hold Down + B to enter the pause menu. + +For more complete game details, go to: + !!!!!!!!!!!!!!!!!!!!!!!!!!ADD URL!!!!!!!!!!!!!!!!!!!!!!!! diff --git a/LCDGallery/arcade_title_video.webm b/LCDGallery/arcade_title_video.webm new file mode 100644 index 00000000..c96eddde Binary files /dev/null and b/LCDGallery/arcade_title_video.webm differ diff --git a/LCDGallery/fight.py b/LCDGallery/fight.py new file mode 100644 index 00000000..e46b9ed7 --- /dev/null +++ b/LCDGallery/fight.py @@ -0,0 +1,374 @@ +# LCD Gallery +# (c) 2024 Lauren Croney +# +# Licensed Creative Commons Attribution-ShareAlike 3.0 (CC BY-SA 3.0). +# See LICENSE.txt for details. + +import thumby +import time +import random +import Games.LCDGallery.lcd as lcd +from Games.LCDGallery.lcd import Game, ScreenActor + +# Screen positioning constants +PLAYER_Y = const(20) +GUN_Y = const(23) +P_GUN_X = const(24) +C_GUN_X = const(39) +PLAYER_HP_X = const(1) +CPU_HP_X = const(67) +CPU_X = const(47) +FRAME_X = const(7) # frame x offset from edge +FRAME_Y = const(9) +ARROWS_X = const(34) +ARROWS_Y = const(2) +PLAYER_X = const(19) + + +class Player(ScreenActor): + # Actions + NONE = const(0) + DODGE = const(1) + KO = const(2) + SHOOT = const(3) + MAX_HP = const(100) + + # Other constants + POINT_VALUE = const(5) + + def __init__(self): + super().__init__() + self.num = -1 + self.action = Player.NONE + self.hp = Player.MAX_HP + + # Set player stance to dodging + def dodge(self): + self.position = Player.DODGE + + # Set player stance to KO + def ko(self): + self.position = Player.KO + + # Reset player to default stance and action + def reset(self): + self.position = Player.NONE + self.action = Player.NONE + + def resurrect(self): + self.reset() + self.hp = Player.MAX_HP + + +class Countdown(ScreenActor): + # The countdown arrows + def __init__(self, x, y): + super().__init__() + self.add(thumby.Sprite(5, 5, lcd.bmp_arrow_r, x, y, 1, 0, 0)) + self.add(thumby.Sprite(5, 5, lcd.blank_5x5, x, y, -1, 0, 0)) + + def hide(self): + self.position = 1 + + def unhide(self): + self.position = 0 + + +class Fight(Game): + # Some named constants for game behavior. + CPU_REACT_MIN = const(420) # minimum CPU reaction time; ms + CPU_REACT_MAX = const(690) # maximum CPU reaction time; ms + COUNTDOWN_MIN = const(700) + COUNTDOWN_MAX = const(1100) + POINTS_WIN = const(40) # bonus points for winning duel + BONUS_HP = const(30) + + # Game states + STATE_DEFAULT = const(-1) + STATE_COUNTDOWN = const(0) + STATE_FIGHT = const(1) + STATE_CPU_DEAD = const(2) + STATE_PLAYER_DEAD = const(3) + + # Shared bitmaps + bmp_hp = bytearray([1,1,1,1]) # noqa: E231 | 4x3 + + def __init__(self): + super().__init__() + self.player1_num = 0 + self.player2_num = 0 + self.state = Fight.STATE_DEFAULT + # Countdown arrows + self.arrows = [Countdown(ARROWS_X, ARROWS_Y), + Countdown(ARROWS_X+8, ARROWS_Y), + Countdown(ARROWS_X+16, ARROWS_Y), + Countdown(ARROWS_X+24, ARROWS_Y)] + + # The number frames + bmp_frame = \ + bytearray([0,254,254,254,254,254,254,254,0,0,3,3,3,3,3,3,3,0]) # noqa: E231 | 9x11 + self.frame1 = ScreenActor() + self.frame1.add(thumby.Sprite(9, 11, bmp_frame, FRAME_X, FRAME_Y, 1, 0, 0)) + self.frame1.blank() + + self.frame2 = ScreenActor() + self.frame2.add(thumby.Sprite(9, 11, bmp_frame, thumby.display.width-FRAME_X-9, + FRAME_Y, 1, 0, 0)) + self.frame2.blank() + + # Players + bmp_man = bytearray([251,113,48,2,48,121,15,6,16,28,0,14]) # noqa: E231 | 6x13 + bmp_man_dodge = \ + bytearray([255,255,255,255,119,35,1,229,225,243,31,15, # noqa: E231 + 7,16,12,4,16,28,31,31]) # noqa: E231 | 10x13 + bmp_ko = \ + bytearray([9,43,43,35,1,1,35,55,55,33,0,9,35]) # noqa: E231 | 13x6 + + self.player = Player() + self.player.add(thumby.Sprite(6, 13, bmp_man, PLAYER_X, PLAYER_Y, 1, 1, 0)) + self.player.add(thumby.Sprite(10, 13, bmp_man_dodge, PLAYER_X-10, PLAYER_Y, 1, 1, 0)) + self.player.add(thumby.Sprite(13, 6, bmp_ko, PLAYER_X-13, 33, 1, 1, 0)) + + self.cpu = Player() + self.cpu.add(thumby.Sprite(6, 13, bmp_man, CPU_X, PLAYER_Y, 1, 0, 0)) + self.cpu.add(thumby.Sprite(10, 13, bmp_man_dodge, CPU_X+6, PLAYER_Y, 1, 0, 0)) + self.cpu.add(thumby.Sprite(13, 6, bmp_ko, CPU_X+6, 33, 1, 0, 0)) + + # Guns + bmp_gun = bytearray([15,15,15,19,25,24,26,28,30,30]) # noqa: E231 | 10x5 + self.player_gun = ScreenActor() + self.player_gun.add(thumby.Sprite(10, 5, bmp_gun, P_GUN_X, GUN_Y, 1, 0, 0)) + self.cpu_gun = ScreenActor() + self.cpu_gun.add(thumby.Sprite(10, 5, bmp_gun, C_GUN_X, GUN_Y, 1, 1, 0)) + self.player_gun.blank() # hide by default + self.cpu_gun.blank() + self.winner = None + self.tick_speed = 1020 # tick speed override + + def reset_game(self): + super().reset_game() + self.cpu.resurrect() + self.state = Fight.STATE_DEFAULT + self.player_gun.blank() + self.cpu_gun.blank() + + # Draw countdown anim; return True if need to restart countdown, False otherwise + def countdown(self): + self.frame1.blank() # number frame 1 off + self.frame2.blank() # number frame 2 off + thumby.display.drawSprite(self.frame1.sprite()) + thumby.display.drawSprite(self.frame2.sprite()) + for arrow in self.arrows: + arrow.unhide() + thumby.display.drawSprite(arrow.sprite()) + thumby.display.update() + + count_speed = random.uniform(Fight.COUNTDOWN_MIN, Fight.COUNTDOWN_MAX) + arrow = 0 + clock = time.ticks_ms() + while arrow < 4: + if time.ticks_diff(time.ticks_ms(), clock) > count_speed: + lcd.play(lcd.sound_move) + self.arrows[arrow].hide() + thumby.display.drawSprite(self.arrows[arrow].sprite()) + thumby.display.update() + arrow += 1 + clock = time.ticks_ms() + if thumby.buttonD.pressed and thumby.buttonB.pressed(): + p = self.pause() + if p is False: + return True + else: + self.drawframe() + return True + + return False + + def drawframe(self): + thumby.display.fill(1) + # Draw score + thumby.display.setFont('/Games/LCDGallery/font_segment.bin', 5, 7, 1) + thumby.display.drawText(str(self.score)[:5], 1, 1, 0) + # Draw playfield and players + thumby.display.drawSprite(self.player.sprite()) + thumby.display.drawSprite(self.cpu.sprite()) + thumby.display.drawSprite(self.player_gun.sprite()) + thumby.display.drawSprite(self.cpu_gun.sprite()) + + # Draw BANG! text if gun is drawn + thumby.display.setFont('/lib/font3x5.bin', 3, 5, 1) + if self.player_gun.position == 0: + thumby.display.drawText("bang!", PLAYER_X-2, PLAYER_Y-7, 0) + if self.cpu_gun.position == 0: + thumby.display.drawText("bang!", CPU_X-10, PLAYER_Y-7, 0) + + # Draw HP meters + for i in range(int(self.player.hp/10)): + thumby.display.blit(Fight.bmp_hp, PLAYER_HP_X, 36-(i*3), 4, 3, -1, + 0, 0) + if self.state != Fight.STATE_DEFAULT: + for i in range(int(self.cpu.hp/10)): + thumby.display.blit(Fight.bmp_hp, CPU_HP_X, 36-(i*3), 4, 3, -1, + 0, 0) + + if self.state == Fight.STATE_CPU_DEAD: + thumby.display.setFont('/lib/font3x5.bin', 3, 5, 1) + thumby.display.drawText("RIP", 41, 34, 0) + elif self.state == Fight.STATE_PLAYER_DEAD: + thumby.display.setFont('/lib/font3x5.bin', 3, 5, 1) + thumby.display.drawText("RIP", 20, 34, 0) + elif self.state == Fight.STATE_FIGHT: + # Draw number frames + self.frame1.position = 0 # number frame 1 on + self.frame2.position = 0 # number frame 2 on + thumby.display.drawSprite(self.frame1.sprite()) + thumby.display.drawSprite(self.frame2.sprite()) + # Draw nums + thumby.display.setFont('/Games/LCDGallery/font_segment.bin', 5, 7, 1) + thumby.display.drawText(str(self.player1_num), FRAME_X+2, FRAME_Y+2, 0) + thumby.display.drawText(str(self.player2_num), thumby.display.width-FRAME_X-7, + FRAME_Y+2, 0) + thumby.display.update() + + def win(self, damage): + self.cpu.hp -= (damage * 2) + lcd.play(lcd.sound_win) + if self.cpu.hp <= 0: + self.state = Fight.STATE_CPU_DEAD + # CPU death anim + for i in range(0, 3): + self.drawframe() + lcd.play(lcd.sound_win) + self.cpu.blank() + self.drawframe() + time.sleep(0.5) + self.cpu.ko() + self.state = Fight.STATE_DEFAULT + self.cpu.blank() + self.player_gun.blank() + self.drawframe() + + self.score += Fight.POINTS_WIN + self.cpu.hp = Player.MAX_HP + + # Restore 20 HP to player on win + self.player.hp += Fight.BONUS_HP + if self.player.hp > Player.MAX_HP: + self.player.hp = Player.MAX_HP + + def lose(self, damage): + self.player.hp -= (damage * 2) + if self.player.hp <= 0: + self.state = Fight.STATE_PLAYER_DEAD + for i in range(0, 3): + self.drawframe() + lcd.play(lcd.sound_die) + self.player.blank() + self.drawframe() + time.sleep(0.5) + self.player.ko() + + self.lives = 0 + else: + self.player.ko() + lcd.play(lcd.sound_die) + + # add to player score + def player_score(self, score): + self.score += score + + # since the CPU opponent doesn't have a score, deduct from player score + def cpu_score(self, score): + self.score -= score + if self.score < 0: + self.score = 0 + + def gamelogic(self): + self.player.reset() + self.cpu.reset() + self.frame1.blank() + self.frame2.blank() + time.sleep(self.tick_speed/1000) + acted = False # Has anyone acted? + self.player1_num = random.randint(0, 9) + self.player2_num = random.randint(0, 9) + self.player_gun.blank() + self.cpu_gun.blank() + + # Do countdown; game can be paused during countdown + self.state = Fight.STATE_COUNTDOWN + self.drawframe() + restart = self.countdown() + if restart is True: + return # After unpausing, start over + self.state = Fight.STATE_FIGHT + self.drawframe() + + if self.lives == 0: + return # Exit if quit while paused + + cpu_reaction = random.randint(Fight.CPU_REACT_MIN, Fight.CPU_REACT_MAX) + clock = time.ticks_ms() + while not acted: + # Special case: if both players drew a 0 then roll again + if self.player1_num == 0 and self.player2_num == 0: + lcd.play(lcd.sound_tie) + time.sleep(0.5) + return + + if thumby.buttonA.justPressed(): + self.player_gun.position = 0 # show player gun + self.player.action = Player.SHOOT + acted = True + # Check if the hit was effective + if self.player1_num >= self.player2_num: + self.cpu.ko() + else: + self.cpu_gun.position = 0 # cpu will shoot back + self.drawframe() + self.player_gun.blank() + self.player.ko() + elif thumby.buttonL.justPressed(): + self.player.dodge() + self.player.action = Player.DODGE + acted = True + else: + # If the CPU is done waiting, react + if time.ticks_diff(time.ticks_ms(), clock) > cpu_reaction: + if self.player1_num > self.player2_num: + self.cpu.action = Player.DODGE + self.cpu.dodge() + # CPU will hit on equal numbers + elif self.player2_num >= self.player1_num: + self.cpu.action = Player.SHOOT + self.player.ko() + self.cpu_gun.position = 0 # show CPU gun + else: + self.cpu.action = Player.NONE + acted = True + + # Make sound for action + lcd.play(lcd.sound_move) + + # Determine who won this matchup + if self.player.action == Player.DODGE: + # If the player dodged, the only thing that mattered is + # if they needed to + if self.player1_num < self.player2_num: + self.win(0) + self.score += 2 # successful player dodge + elif self.player1_num == self.player2_num: + # player dodged on a tie, no action + lcd.play(lcd.sound_tie) + else: + self.player.ko() # erroneous dodge, fall over and get hurt + self.lose(8) + elif self.player.action == Player.SHOOT: + if not self.cpu.action == Player.DODGE: + if self.player1_num >= self.player2_num: + self.win(self.player1_num) + self.score += 3 # successful player hit + else: + self.lose(self.player2_num) # unsuccessful player hit + elif self.cpu.action == Player.SHOOT: + self.lose(self.player2_num) # successful CPU hit diff --git a/LCDGallery/font_segment.bin b/LCDGallery/font_segment.bin new file mode 100644 index 00000000..7e0d0d1c Binary files /dev/null and b/LCDGallery/font_segment.bin differ diff --git a/LCDGallery/juggle.py b/LCDGallery/juggle.py new file mode 100644 index 00000000..679f0616 --- /dev/null +++ b/LCDGallery/juggle.py @@ -0,0 +1,292 @@ +# LCD Gallery +# (c) 2024 Lauren Croney +# +# Licensed Creative Commons Attribution-ShareAlike 3.0 (CC BY-SA 3.0). +# See LICENSE.txt for details. +import gc +import time +import random +import thumby +import Games.LCDGallery.lcd as lcd +from Games.LCDGallery.lcd import Game, ScreenActor + +# Screen positioning constants +Y_T_1 = const(9) # top thing position +Y_T_2 = const(17) # ... +Y_T_3 = const(23) # ... +Y_T_4 = const(30) # bottom thing position +X_T_1 = const(8) # thing / paddle slot 1 +X_T_2 = const(23) # thing / paddle slot 2 +X_T_3 = const(38) # thing / paddle slot 3 +X_T_4 = const(53) # thing / paddle slot 4 +X_BUBBLE = const(51) # bubble x +Y_BUBBLE = const(7) # bubble y +Y_PADDLE = const(32) # paddle y + + +class Player(ScreenActor): + DEFAULT_LIVES = 3 # Default number of lives for the player. + MAX_POSITION = 3 # The maximum number of position slots + + def __init__(self): + super().__init__() + self.dead = False + # Add screen elements + bmp_paddleup = bytearray([247,247,247,247,247,247,247,247,247]) # noqa: E231 | 9x8 + bmp_paddledown = bytearray([239,223,191,191,191,191,191,223,239]) # noqa: E231 | 9x8 + bmp_paddle = bmp_paddledown + bmp_paddleup + self.add(thumby.Sprite(9, 8, bmp_paddle, X_T_1-2, Y_PADDLE, 1, 0, 0)) + self.add(thumby.Sprite(9, 8, bmp_paddle, X_T_2-2, Y_PADDLE, 1, 0, 0)) + self.add(thumby.Sprite(9, 8, bmp_paddle, X_T_3-2, Y_PADDLE, 1, 0, 0)) + self.add(thumby.Sprite(9, 8, bmp_paddle, X_T_4-2, Y_PADDLE, 1, 0, 0)) + + # Reset the player character for a new game after all lives are lost + def resurrect(self): + self.position = 0 + self.dead = False + + # Move the player left one position. + # Return False if the player can't move more, True otherwise. + def left(self): + if self.position > 0: + self.position -= 1 + else: + return False + + return True + + # Return False if the player can't move more, True otherwise. + def right(self): + if self.position < Player.MAX_POSITION: + self.position += 1 + else: + return False + + return True + + +# The things the player is juggling. +class Thing(ScreenActor): + # Constants + DEF_FALLRATE = 33 + TIMER_MAX = 4 # Miss timer + + # Takes a list of screen objects, start position, and start dir. + # Position 0 is the dropped, broken thing. After that each pos is higher + def __init__(self, pos, dir, x, bmps, num=1): + super().__init__() + self._pos = pos # Override default start position of 0 + self._pos_default = pos # Save default position + self._dir = dir # direction: 1 = rising, -1 = falling + self._dir_default = dir # Save default direction + self.frozen = False + self.fallrate = Thing.DEF_FALLRATE + self.num = num + self.timer = Thing.TIMER_MAX + # Create screen elements given list of bitmaps and x position + self.add(thumby.Sprite(5, 5, bmps[0], x, Y_T_4, 1, 0, 0)) + self.add(thumby.Sprite(5, 5, bmps[1], x, Y_T_3, 1, 0, 0)) + self.add(thumby.Sprite(5, 5, bmps[2], x, Y_T_2, 1, 0, 0)) + self.add(thumby.Sprite(5, 5, bmps[3], x, Y_T_1, 1, 0, 0)) + + # Go back up when hit + def hit(self): + self._dir = 1 + self.position += 1 + self.timer = Thing.TIMER_MAX + + # Reset the default position and dir for this thing. + def reset(self): + self.position = self._pos_default + self._dir = self._dir_default + self.timer = Thing.TIMER_MAX + self.frozen = False + + def move(self): + if self.frozen: + return # Don't move if item is frozen + + # If the peak has been reached, drop back down + if self._dir == 1 and self.position == len(self._sprites)-1: + self._dir = -1 + # Otherwise if it's in the air, there is a chance that it will fall anyway + elif self._dir == 1 and self.position > 1 and random.randint(0, 100) < self.fallrate: + self._dir = -1 + elif self.position == 0: + self._dir = 0 # let the miss timer take care of this + try: + self.position += self._dir + except Exception as e: + print(f"exception {e} pos={self.position} dir={self._dir}") + + @property + def about_to_fall(self): + return self._dir == -1 and self.position == 0 + + +# Class representing the bubble that holds thing 4 +class Bubble(ScreenActor): + def __init__(self): + super().__init__() + bmp_bubble = \ + bytearray([131,125,254,254,254,254,254,125,131,1,1,0,0,0,0,0,1,1]) # noqa: E231 | 9x9 + self.add(thumby.Sprite(9, 9, bmp_bubble, X_BUBBLE, Y_BUBBLE, 1, 0, 0)) + + +# Game class +class Juggle(Game): + # Constants + NEW_LIVES = [100, 200, 400, 600, 1000] # values to be tweaked + SPEED_UP_MOD = const(5) # When to speed up (modulo) + SPEED_DOWN_MOD = const(100) # When to speed down (modulo) + FREEZE_RATE = const(25) # How often bubble activates + ACTION_LEFT = const(-1) + ACTION_RIGHT = const(1) + ACTION_NONE = const(0) + CYCLE_LENGTH = const(16) # ticks per cycle + TICK_DEFAULT = const(145) # default tick speed + + def __init__(self): + super().__init__() + + self.action = Juggle.ACTION_NONE # Set default player action + + # Thing bitmaps (5x5) + bmps_thing1 = [bytearray([25, 4, 16, 4, 25]), + bytearray([21, 17, 10, 0, 17]), + bytearray([19, 4, 1, 4, 19]), + bytearray([17, 0, 10, 17, 21])] + bmps_thing2 = [bytearray([14, 16, 30, 0, 14]), + bytearray([6, 21, 21, 21, 0]), + bytearray([14, 0, 15, 1, 14]), + bytearray([0, 21, 21, 21, 12])] + bmps_thing3 = [bytearray([27, 1, 8, 1, 27]), + bytearray([17, 21, 0, 17, 27]), + bytearray([27, 16, 2, 16, 27]), + bytearray([27, 17, 0, 21, 17])] + bmps_thing4 = [bytearray([7, 0, 30, 6, 0]), + bytearray([4, 4, 13, 13, 1]), + bytearray([0, 12, 15, 0, 28]), + bytearray([16, 22, 22, 4, 4])] + + self.things = [Thing(1, 1, X_T_1, bmps_thing1, 0), + Thing(1, 1, X_T_2, bmps_thing2, 1), + Thing(1, 1, X_T_3, bmps_thing3, 2), + Thing(1, 1, X_T_4, bmps_thing4, 3)] + self.player = Player() + self.bubble = Bubble() + self.move_order = [1, 3, 2, 0] # Order to move things + self.tick_speed = Juggle.TICK_DEFAULT + + # Add speed reset to game reset + def reset_game(self): + super().reset_game() + self.tick_speed = Juggle.TICK_DEFAULT + + # Handle the player dropping a thing + def die(self): + self.lives -= 1 + old_pos = self.player.position + for i in range(4): + if i % 2 == 0: + self.player.blank() # flash player off + else: + self.player.position = old_pos # flash on + self.drawframe() + lcd.play(lcd.sound_die) + time.sleep(0.25) + + for thing in self.things: + thing.reset() # Reset the positions + + def drawframe(self): + thumby.display.fill(1) + + # Draw health + for i in range(0, self.lives): + thumby.display.blit(lcd.bmp_heart, 66, 1+(7*i), 5, 5, -1, 0, 0) + # Draw player sprite + thumby.display.drawSprite(self.player.sprite()) + # Draw thing sprites + for i in range(4): + thumby.display.drawSprite(self.things[i].sprite()) + # Draw bubble if first thing is frozen + if self.things[3].frozen: + thumby.display.drawSprite(self.bubble.sprite()) + + # Draw score + thumby.display.drawText(str(self.score)[:7], 1, 1, 0) + thumby.display.update() + + def gamelogic(self): + moved = False # Player has moved + + # Set actions for next tick based on input + if thumby.buttonL.justPressed(): + self.action = Juggle.ACTION_LEFT + elif thumby.buttonA.justPressed(): + self.action = Juggle.ACTION_RIGHT + elif thumby.buttonD.pressed() and thumby.buttonB.pressed(): + self.pause() + + if (self.score in self.NEW_LIVES) and (self.lives < lcd.MAX_LIVES): + self.lives += 1 # Grant a new life at certain scores + self.score += 10 # Advance score so only one life is granted + lcd.play(lcd.sound_life) + + if self.action == Juggle.ACTION_LEFT: + lcd.play(lcd.sound_move) + self.player.left() + moved = True + elif self.action == Juggle.ACTION_RIGHT: + lcd.play(lcd.sound_move) + self.player.right() + moved = True + + if moved is True: + self.player.sprite().setFrame(0) # Reset player sprite when moving + moved = False + self.action = Juggle.ACTION_NONE + + if time.ticks_diff(time.ticks_ms(), self.clock) > self.tick_speed: + if self.gametick % 4 == 0: + self.player.sprite().setFrame(0) # Reset player sprite + # Collision detection + for i in range(4): + if self.things[i].about_to_fall and i == self.player.position: + # Collision + self.player.sprite().setFrame(1) # Set sprite to hit frame + self.score += 1 + self.things[i].hit() # Bounce it back + lcd.play(lcd.sound_score) + # Adjust speed at different scores + if (self.score >= Juggle.SPEED_DOWN_MOD and + self.score % Juggle.SPEED_DOWN_MOD == 0): + self.tick_speed = Juggle.TICK_DEFAULT + elif (self.score >= Juggle.SPEED_UP_MOD and + self.score % Juggle.SPEED_UP_MOD == 0): + self.tick_speed -= 5 + elif self.gametick % 2 == 0: + lcd.play(lcd.sound_tick) + # Move things in a set order + t = int((self.gametick % Juggle.CYCLE_LENGTH) / 4) + self.things[self.move_order[t]].move() + + # Random chance to freeze or unfreeze last item + if (random.randint(0, 100) < Juggle.FREEZE_RATE + and self.things[3].position == 3): + self.things[3].frozen = not self.things[3].frozen + else: + for i in range(4): + if self.things[i].position == 0: + self.things[i].timer -= 1 + + # Check for miss + for i in range(4): + if (self.things[i].timer == 0 and i != self.player.position): + self.things[i].blank() + self.die() # Miss + + self.gametick += 1 + self.reset_clock() + gc.collect() diff --git a/LCDGallery/lcd.py b/LCDGallery/lcd.py new file mode 100644 index 00000000..4ee2242f --- /dev/null +++ b/LCDGallery/lcd.py @@ -0,0 +1,175 @@ +# LCD Gallery +# (c) 2024 Lauren Croney +# +# Licensed Creative Commons Attribution-ShareAlike 3.0 (CC BY-SA 3.0). +# See LICENSE.txt for details. +import time +import thumby + +# Constants +MAX_LIVES = const(3) + +# Shared bitmap assets +bmp_arrow_r = bytearray([31,31,0,17,27]) # noqa: E231 | 5x5 +bmp_arrow_l = bytearray([27,17,0,31,31]) # noqa: E231 | 5x5 +bmp_arrow_u = bytearray([27,25,24,25,27]) # noqa: E231 | 5x5 +bmp_arrow_d = bytearray([27,19,3,19,27]) # noqa: E231 | 5x5 +blank_5x5 = bytearray([31,31,31,31,31]) # noqa: E231 | 5x5 +bmp_arrow_lr = bytearray([31,31,0,17,27]) # noqa: E231 | 5x5 (for mirroring) +bmp_arrow_ud = bytearray([27,25,24,25,27]) # noqa: E231 | 5x5 (for mirroring) +bmp_heart = bytearray([25,16,1,16,25]) # noqa: E231 | 5x5 + + +# Sound engine +class Note: + def __init__(self, pitch, duration): + self.pitch = pitch + self.duration = duration + + @property + def value(self): + return self.pitch, self.duration + + +class Sound: + def __init__(self, notes, blocking=False, repetitions=1): + self.notes = notes + self.blocking = blocking + self.repetitions = repetitions + + +def play(sound): + for i in range(sound.repetitions): + for note in sound.notes: + if sound.blocking is True: + thumby.audio.playBlocking(*note.value) + else: + thumby.audio.play(*note.value) + + +sound_tie = Sound([Note(1020, 30)]) +sound_tick = Sound([Note(1000, 20)]) +sound_move = Sound([Note(600, 30)]) +sound_win = Sound([Note(1000, 20), Note(1500, 20), Note(1000, 20)], True, 3) +sound_score = Sound([Note(1600, 50)]) +sound_item = Sound([Note(800, 30), Note(1000, 30)], True) +sound_die = Sound([Note(300, 500)], True) +sound_life = Sound([Note(700, 500), Note(800, 500), Note(1048, 500), Note(1396, 500)], True) +sound_pause = Sound([Note(523, 200), Note(1046, 200), Note(2093, 200)], True) +sound_unpause = Sound([Note(2093, 200), Note(1046, 200), Note(523, 200)], True) + + +class ScreenActor: + bmp_blank = bytearray([1]) + + def __init__(self): + self._pos = 0 + self._sprites = [] + self._blank = thumby.Sprite(1, 1, bytearray([1]), 80, 80, 1, 0, 0) + + def add(self, sprite): + self._sprites.append(sprite) + + @property + def position(self): + return self._pos + + @position.setter + def position(self, value): + if value > len(self._sprites): + raise Exception("invalid actor position") + self._pos = int(value) + + def blank(self): + self._pos = -1 + + def sprite(self): + try: + if self._pos >= 0: + return self._sprites[self._pos] + except Exception as e: + print(f"Exception in ScreenActor.sprite(): {e}") + print(f"pos={self._pos} class={type(self)}") + + return self._blank # return blank by default + + +class Game: + # Parent class for games + def __init__(self): + thumby.display.setFPS(30) + self.hiscore = 0 + self.score = 0 + self.lives = MAX_LIVES + self.tick_speed = 500 # ms + self.cycle_length = 1 # number of ticks per cycle + self.clock = time.ticks_ms() # start clock + self.gametick = 1 + + def reset_clock(self): + self.clock = time.ticks_ms() # reset clock + + def main_loop(self): + thumby.display.setFont("/Games/LCDGallery/font_segment.bin", 5, 7, 1) + while self.lives > 0: + self.drawframe() + self.gamelogic() + self.game_over() + + self.reset_game() + + # Pause game and return True for resume, False for quit + def pause(self): + play(sound_pause) + selection = 0 + thumby.display.setFont("/lib/font5x7.bin", 5, 7, 1) + while True: + thumby.display.fill(1) + thumby.display.drawText("*PAUSED*", 13, 1, 0) + thumby.display.drawText(" QUIT", 10, 14, 0) + thumby.display.drawText(" RESUME", 10, 23, 0) + + if selection == 0: + thumby.display.blit(bmp_arrow_lr, 1, 15, 5, 5, 1, 0, 0) + if thumby.buttonA.justPressed(): + self.lives = 0 # quit + return False + elif selection == 1: + thumby.display.blit(bmp_arrow_lr, 1, 24, 5, 5, 1, 0, 0) + if thumby.buttonA.justPressed(): + play(sound_unpause) + return True + thumby.display.update() + + if thumby.buttonU.justPressed(): + selection = 0 + elif thumby.buttonD.justPressed(): + selection = 1 + + def save_score(self): + name = type(self).__name__ + thumby.saveData.setItem("highscore_"+name, self.hiscore) + thumby.saveData.save() + + def game_over(self): + thumby.display.setFont("/lib/font5x7.bin", 5, 7, 1) + if self.score > self.hiscore: + self.hiscore = self.score + self.save_score() + + thumby.display.fill(1) + thumby.display.drawText("GAME OVER", 1, 1, 0) + thumby.display.drawText(f"SCORE={self.score}", 1, 9, 0) + thumby.display.drawText(f"HI={self.hiscore}", 1, 17, 0) + thumby.display.update() + clock = time.ticks_ms() + # Return to menu after a pause + while time.ticks_diff(time.ticks_ms(), clock) < 2000: + pass + clock = time.ticks_ms() + + # Individual games will augment this + def reset_game(self): + self.score = 0 + self.player.resurrect() + self.lives = MAX_LIVES diff --git a/LCDGallery/skywalk.py b/LCDGallery/skywalk.py new file mode 100644 index 00000000..bed44827 --- /dev/null +++ b/LCDGallery/skywalk.py @@ -0,0 +1,379 @@ +# LCD Gallery +# (c) 2024 Lauren Croney +# +# Licensed Creative Commons Attribution-ShareAlike 3.0 (CC BY-SA 3.0). +# See LICENSE.txt for details. +import time +import random +import gc +import thumby +import Games.LCDGallery.lcd as lcd +from Games.LCDGallery.lcd import Game, ScreenActor + +# Screen position constants +PLATFORM_Y = const(24) +Y_JUMP = const(9) +Y_PLAYER = const(15) +Y_DEAD = const(34) +Y_CLIFF = const(24) +X1_1 = const(0) +X1_2 = const(6) +X2_1 = const(11) +X2_2 = const(16) +X3_1 = const(22) +X3_2 = const(28) +X4_1 = const(33) +X4_2 = const(38) +X5_1 = const(44) +X5_2 = const(50) +X6_1 = const(55) +X6_2 = const(60) +X7_1 = const(65) +X_HAT = const(55) +Y1_HAT = const(7) +Y2_HAT = const(15) +X_HP = const(50) + + +# Class representing the player +class Player(ScreenActor): + # Constants + POS_ALIVE_FIRST = const(0) # Position: On left cliff + POS_HAT = const(10) # Platform with hat + POS_ALIVE_LAST = const(12) # On right cliff + POS_DEAD_FIRST = const(13) # First death position + POS_DEAD_LAST = const(11) # Last death position + + def __init__(self): + super().__init__() + self.has_item = False + self.dead = False + self.slot = 0 # Unlike position, this is to compare against platforms + # Add screen elements + bmp_man1_hat = \ + bytearray([204,237,72,128,72,237,204,1,0,0,1,0,0,1]) # noqa: E231 7x9 + bmp_man2_hat = \ + bytearray([228, 237, 72, 128, 72, 237, 204, 1, 0, 0, 1,0,0,1]) # noqa: E231 | 7x9 + bmp_man3_hat = \ + bytearray([236,237,72,128,72,237,236,1,0,0,1,0,0,1]) # noqa: E231 | 7x9 + bmp_man1 = \ + bytearray([207,239,72,128,72,239,207,1,0,0,1,0,0,1]) + bmp_man1_hat # noqa: E231 + bmp_man2 = \ + bytearray([231,239,72,128,72,239,207,1,0,0,1,0,0,1]) + bmp_man2_hat # noqa: E231 + bmp_man3 = \ + bytearray([239,239,72,128,72,239,239,1,0,0,1,0,0,1]) + bmp_man3_hat # noqa: E231 + bmp_dead = bytearray([9,43,55,35,0,55,35,35,35]) # noqa: E231 9x6 + self.add(thumby.Sprite(7, 9, bmp_man1, X1_1, Y_PLAYER, 1, 0, 0)) + self.add(thumby.Sprite(7, 9, bmp_man2, X1_2, Y_JUMP, 1, 0, 0)) + self.add(thumby.Sprite(7, 9, bmp_man3, X2_1, Y_PLAYER, 1, 0, 0)) + self.add(thumby.Sprite(7, 9, bmp_man2, X2_2, Y_JUMP, 1, 1, 0)) + self.add(thumby.Sprite(7, 9, bmp_man1, X3_1, Y_PLAYER, 1, 0, 0)) + self.add(thumby.Sprite(7, 9, bmp_man2, X3_2, Y_JUMP, 1, 0, 0)) + self.add(thumby.Sprite(7, 9, bmp_man3, X4_1, Y_PLAYER, 1, 0, 0)) + self.add(thumby.Sprite(7, 9, bmp_man2, X4_2, Y_JUMP, 1, 1, 0)) + self.add(thumby.Sprite(7, 9, bmp_man1, X5_1, Y_PLAYER, 1, 0, 0)) + self.add(thumby.Sprite(7, 9, bmp_man2, X5_2, Y_JUMP, 1, 0, 0)) + self.add(thumby.Sprite(7, 9, bmp_man3, X6_1, Y_PLAYER, 1, 0, 0)) + self.add(thumby.Sprite(7, 9, bmp_man2, X6_2, Y_JUMP, 1, 1, 0)) + self.add(thumby.Sprite(7, 9, bmp_man1, X7_1, Y_PLAYER, 1, 0, 0)) + self.add(thumby.Sprite(9, 6, bmp_dead, X2_1, Y_DEAD, 1, 0, 0)) + self.add(thumby.Sprite(9, 6, bmp_dead, X3_1, Y_DEAD, 1, 0, 0)) + self.add(thumby.Sprite(9, 6, bmp_dead, X4_1, Y_DEAD, 1, 0, 0)) + self.add(thumby.Sprite(9, 6, bmp_dead, X5_1, Y_DEAD, 1, 0, 0)) + self.add(thumby.Sprite(9, 6, bmp_dead, X6_1, Y_DEAD, 1, 0, 0)) + + # Set player state to dead + def die(self): + self.dead = True + self.position = self.POS_ALIVE_LAST + self.slot # Move to death pos + + # Reset the player character after a single life is lost + def revive(self): + self.position = 0 + self.slot = 0 + self.dead = False + self.has_item = self.give_item() + + # Reset the player character for a new game after all lives are lost + def resurrect(self): + self.position = 0 + self.slot = 0 + self.dead = False + self.give_item() + + # Mark player as carrying item and update frame + def get_item(self): + self.has_item = True + for sprite in self._sprites: + sprite.setFrame(1) + + # Mark player as not carrying item and update frame + def give_item(self): + self.has_item = False + for sprite in self._sprites: + sprite.setFrame(0) + + # Move player left. Return False if player can't move, True otherwise + def left(self): + if self.position == 2 and not self.has_item: + return False # can't return to left cliff without item + if not self.dead and self.position > self.POS_ALIVE_FIRST: + self.position -= 1 + if self.position % 2 == 0: + self.slot -= 1 # only change slot on even positions + return True + return False + + # Move player right. Return False if player can't move, True otherwise + def right(self): + if not self.dead and self.position < 10: + self.position += 1 + if self.position % 2 == 0: + self.slot += 1 + return True + return False + + @property + def jumping(self): + if self.position in [1, 3, 5, 7, 9, 11]: + return True + else: + return False + + +# Class representing one of the platforms the player jumps on +class Platform(ScreenActor): + def __init__(self, slot, x, enabled=True): + super().__init__() + self.slot = slot # Associate with a player slot + self.level = 0 + self.eroding = False # Has the battery started eroding? + self.enabled = enabled # if false, the platform will not act + + # Create screen elements + bmp_p1 = bytearray([15,15,15,15,15,15,15,15,15]) # noqa: E231 | 9x4 + bmp_p2 = bytearray([15,15,14,12,12,12,14,15,15]) # noqa: E231 | 9x4 + bmp_p3 = bytearray([15,14,12,8,8,8,12,14,15]) # noqa: E231 | 9x4 + bmp_p4 = bytearray([14,12,8,8,8,8,8,12,14]) # noqa: E231 | 9x4 + bmp_platform = bmp_p4 + bmp_p3 + bmp_p2 + bmp_p1 + self.add(thumby.Sprite(9, 4, bmp_platform, x, PLATFORM_Y, 1, 0, 0)) + + # Erode platform or reset + def tick(self): + if self.enabled: + if self.level == 3: + self.level = 0 + self.eroding = False + elif self.eroding is True: + self.level += 1 + self._sprites[0].setFrame(self.level) # Reflect lvl in frame + + # Set platform to eroding + def erode(self): + self.eroding = True + + +# Class representing the hat item the player gets +class Hat(ScreenActor): + RAISED = const(1) + LOWERED = const(0) + + def __init__(self): + super().__init__() + + # Create screen elements + bmp_hat = bytearray([4,5,4,6,4,5,4,7]) # noqa: E231 | 8x3 + self.add(thumby.Sprite(8, 3, bmp_hat, X_HAT, Y2_HAT, 1, 0, 0)) + self.add(thumby.Sprite(8, 3, bmp_hat, X_HAT, Y1_HAT, 1, 0, 0)) + + # Toggle raised or lowered state of hat + def toggle(self): + if self._pos == self.RAISED: + self._pos = self.LOWERED + else: + self._pos = self.RAISED + + +# Game class +class Skywalk(Game): + # Constants + ACTION_LEFT = const(-1) + ACTION_JUMP_LEFT = const(-2) + ACTION_RIGHT = const(1) + ACTION_JUMP_RIGHT = const(2) + ACTION_NONE = const(0) + HAT_RATE = const(40) # rate of hat movement + SCORE_HAT = const(3) # points given for getting hat + SCORE_BONUS_MAX = const(15) # max bonus points for returning to cliff + SCORE_BONUS_MIN = const(4) # min bonus points for returning to cliff + TIMEOUT_HAT = const(20) + TIMEOUT_HAT_MIN = const(14) # minimum time before hat can toggle + TIMEOUT_CLIFF = const(20) + SPEED_UP_MOD = const(40) + SPEED_DOWN_MOD = const(200) + EROSION_DEFAULT = const(72) # initial erosion rate + + # Bitmaps + bmp_cliff = bytearray([0,0,0,0,192,252,254,0,0,224,255,255,255,255]) # noqa: E231 | 7x16 + + def __init__(self): + super().__init__() + self.action = self.ACTION_NONE + self.platforms = [Platform(1, X2_1-1), Platform(2, X3_1-1), + Platform(3, X4_1-1, False), Platform(4, X5_1-1), + Platform(5, X6_1-1)] + self.player = Player() + self.hat = Hat() + self.reset_game() + + def reset_game(self): + super().reset_game() + self.new_lives = [100, 200, 300, 600, 1000] # scores at which new lives are granted + self.erosion_rate = Skywalk.EROSION_DEFAULT # rate of platform erosion + self.bonus_points = 12 # bonus points for returning to cliff + self.hat_timeout = Skywalk.TIMEOUT_HAT # maximum ticks before hat moved + self.cliff_timeout = Skywalk.TIMEOUT_CLIFF # maximum ticks on cliff + self.tick_speed = 460 + + def death_anim(self): + pos_temp = self.player.position + self.drawframe() + for i in range(0, 3): + self.drawframe() + lcd.play(lcd.sound_die) + self.player.blank() + self.drawframe() + time.sleep(0.25) + self.player.position = pos_temp + + def drawframe(self): + thumby.display.fill(1) + # Score + thumby.display.drawText(str(self.score)[:5], 1, 1, 0) + # Lives + for i in range(0, self.lives): + thumby.display.blit(lcd.bmp_heart, X_HP+(7*i), 1, 5, 5, -1, 0, 0) + # Cliffs + thumby.display.blit(Skywalk.bmp_cliff, 0, Y_CLIFF, 7, 16, -1, 0, 0) + # Player + thumby.display.drawSprite(self.player.sprite()) + # Hat + if self.player.has_item is False: + thumby.display.drawSprite(self.hat.sprite()) + # Platforms + for p in self.platforms: + thumby.display.drawSprite(p.sprite()) + + thumby.display.update() + + # Update score and apply any effects + def update_score(self, points): + self.score += points + rounded = int(self.score / 10) * 10 # round score to nearest 10 + # Update speed by score + if self.score >= Skywalk.SPEED_DOWN_MOD and rounded % Skywalk.SPEED_DOWN_MOD == 0: + self.erosion_rate = Skywalk.EROSION_DEFAULT + elif self.score >= Skywalk.SPEED_UP_MOD and rounded % Skywalk.SPEED_UP_MOD == 0: + self.erosion_rate += 10 + + def gamelogic(self): + # Reduce bonus the longer it takes to return to cliff + if (self.gametick >= 10 and self.gametick % 10 == 0 and + self.bonus_points > self.SCORE_BONUS_MIN and + self.player.position > 0): + self.bonus_points -= 1 + + while time.ticks_diff(time.ticks_ms(), self.clock) < self.tick_speed: + # While waiting for the next tick, scan for input + # and do collision detection so the game stays responsive + # Make sure player isn't jumping before taking input + if (self.action != self.ACTION_JUMP_LEFT and + self.action != self.ACTION_JUMP_RIGHT): + if thumby.buttonL.justPressed(): + self.action = self.ACTION_LEFT + elif thumby.buttonA.justPressed(): + self.action = self.ACTION_RIGHT + elif thumby.buttonD.pressed() and thumby.buttonB.pressed(): + self.pause() + self.action = self.ACTION_NONE # prevent movement after unpuase + + for p in self.platforms: + # Player must be on a dropped platform AND not jumping + if (self.player.slot == p.slot and p.level == 3 and + not self.player.jumping): + self.player.die() + self.death_anim() + self.lives -= 1 + self.player.resurrect() + break + + # Give or take player item + if (self.player.position == Player.POS_HAT and + self.player.has_item is False and + self.hat.position == Hat.LOWERED): + self.player.get_item() + self.update_score(Skywalk.SCORE_HAT) + lcd.play(lcd.sound_item) + elif self.player.position == 0 and self.player.has_item is True: + self.player.give_item() + self.update_score(self.bonus_points) + self.bonus_points = Skywalk.SCORE_BONUS_MAX + self.cliff_timeout = Skywalk.TIMEOUT_CLIFF + for s in self.new_lives: + if self.score > s and self.lives < lcd.MAX_LIVES: + self.lives += 1 + self.new_lives.remove(s) + lcd.play(lcd.sound_life) + lcd.play(lcd.sound_win) + + # Platforms and hat update every other tick + if self.gametick % 2 == 0: + if random.randint(0, 100) < self.erosion_rate: + self.platforms[random.choice([0, 1, 3, 4])].erode() + for p in self.platforms: + p.tick() + if (self.hat_timeout <= Skywalk.TIMEOUT_HAT_MIN and + random.randint(0, 100) < Skywalk.HAT_RATE) or self.hat_timeout == 0: + self.hat.toggle() + self.hat_timeout = Skywalk.TIMEOUT_HAT + + # The player can move on every tick + if self.action == self.ACTION_RIGHT: + moved = self.player.right() + if moved: + self.action = self.ACTION_JUMP_RIGHT + lcd.play(lcd.sound_move) + else: + self.action = self.ACTION_NONE + elif self.action == self.ACTION_LEFT: + moved = self.player.left() + if moved: + self.action = self.ACTION_JUMP_LEFT + lcd.play(lcd.sound_move) + else: + self.action = self.ACTION_NONE + elif self.action == self.ACTION_JUMP_RIGHT: + self.player.right() + self.action = self.ACTION_NONE + lcd.play(lcd.sound_tick) + elif self.action == self.ACTION_JUMP_LEFT: + self.player.left() + self.action = self.ACTION_NONE + lcd.play(lcd.sound_tick) + elif (self.action == self.ACTION_NONE and self.player.position == 0 + and self.cliff_timeout == 0): + self.action = self.ACTION_RIGHT + else: + lcd.play(lcd.sound_tick) + + # Update timing + self.reset_clock() + self.gametick += 1 + self.hat_timeout -= 1 + self.cliff_timeout -= 1 + if self.hat_timeout < 0: + self.hat_timeout = Skywalk.TIMEOUT_HAT + if self.cliff_timeout < 0: + self.cliff_timeout = Skywalk.TIMEOUT_CLIFF + + gc.collect()