From 3e9b243e5759843b605d2300dd249572c51fda27 Mon Sep 17 00:00:00 2001 From: Brennon Church Date: Wed, 29 Oct 2025 14:42:32 -0700 Subject: [PATCH 1/6] Jungle, a tiny game for the Github Badge --- badge/apps/jungle/__init__.py | 261 +++++++++++++++++++++++++ badge/apps/jungle/icon.png | Bin 0 -> 186 bytes badge/apps/jungle/sprites/branch.png | Bin 0 -> 345 bytes badge/apps/jungle/sprites/creature.png | Bin 0 -> 219 bytes badge/apps/jungle/sprites/log.png | Bin 0 -> 212 bytes badge/apps/jungle/sprites/player.png | Bin 0 -> 249 bytes 6 files changed, 261 insertions(+) create mode 100644 badge/apps/jungle/__init__.py create mode 100644 badge/apps/jungle/icon.png create mode 100644 badge/apps/jungle/sprites/branch.png create mode 100644 badge/apps/jungle/sprites/creature.png create mode 100644 badge/apps/jungle/sprites/log.png create mode 100644 badge/apps/jungle/sprites/player.png diff --git a/badge/apps/jungle/__init__.py b/badge/apps/jungle/__init__.py new file mode 100644 index 0000000..787299c --- /dev/null +++ b/badge/apps/jungle/__init__.py @@ -0,0 +1,261 @@ +from badgeware import screen, Image, PixelFont, brushes, shapes, io, run +import random + +# Constants +WIDTH = 160 +HEIGHT = 120 +GROUND_Y = 90 +PLAYER_X = 30 +SCROLL_SPEED = 2 +OBSTACLE_SPAWN_MIN = 70 +OBSTACLE_SPAWN_MAX = 120 +OBSTACLE_REMOVAL_THRESHOLD = -50 # X position at which off-screen obstacles are removed +AIR_GAP = 8 # Vertical gap above ground that the branch bottom sits at (duck to clear) + +# Collision box adjustments for forgiving gameplay +COLLISION_MARGIN = 2 # Pixels reduced from each side of collision boxes +COLLISION_Y_MARGIN = 2 # Y-offset for standing player collision box +DUCKING_Y_OFFSET = 10 # Y-offset for ducking player collision box +DUCKING_SPRITE_Y_OFFSET = 8 # Y-offset for rendering ducking sprite (visual position) +PLAYER_COLLISION_WIDTH = 16 - (COLLISION_MARGIN * 2) # Player collision box width (12) +PLAYER_COLLISION_HEIGHT = 16 - (COLLISION_MARGIN * 2) # Standing player collision box height (12) +PLAYER_DUCKING_COLLISION_HEIGHT = 8 - (COLLISION_MARGIN * 2) # Ducking player collision box height (4) + +# Jungle theme colors +SKY_COLOR = brushes.color(135, 206, 235) # Sky blue +GROUND_COLOR = brushes.color(34, 139, 34) # Forest green +GRASS_COLOR = brushes.color(50, 205, 50) # Lime green +TEXT_COLOR = brushes.color(255, 255, 255) # White +GAMEOVER_BG = brushes.color(0, 0, 0, 200) # Semi-transparent black + +# Set up font +screen.font = PixelFont.load("/system/assets/fonts/ark.ppf") + +# Load sprites +player_img = Image.load("/system/apps/jungle/sprites/player.png") +log_img = Image.load("/system/apps/jungle/sprites/log.png") +creature_img = Image.load("/system/apps/jungle/sprites/creature.png") +branch_img = Image.load("/system/apps/jungle/sprites/branch.png") + +# Game state +state = { + "game_state": "playing", # "playing" or "gameover" + "score": 0, + "player_y": GROUND_Y - 16, # Player standing on ground + "player_vel_y": 0, + "is_jumping": False, + "is_ducking": False, + "obstacles": [], + "next_spawn": OBSTACLE_SPAWN_MIN, + "scroll_offset": 0 +} + +# Obstacle types: (image, type, ground_height) +# type: "ground" (jump over) or "air" (duck under) +# ground_height: for ground obstacles, this should equal sprite height so bottom rests at GROUND_Y +# (calculation: y_pos = GROUND_Y - ground_height, so bottom = y_pos + height = GROUND_Y) +# Coordinate system: y = top of sprite. Collision boxes use margins and offsets. +# Air obstacles: branch placed with bottom at GROUND_Y - AIR_GAP (90 - 8 = 82) +# Standing player: player_y = 74 (sprite top), collision box top = 74 + COLLISION_Y_MARGIN = 76, +# collision box height = 12, so collision box bottom = 88 (HITS branch at 82) +# Ducking player: player_y = 74 (sprite top), collision box top = 74 + DUCKING_Y_OFFSET = 84, +# collision box height = 4, so collision box bottom = 88 (CLEARS branch at 82) +OBSTACLE_TYPES = [ + (log_img, "ground", 12), + (creature_img, "ground", 12), + (branch_img, "air", 0) # ground_height not used for air obstacles +] + +def reset_game(): + """Reset game to initial state""" + state["game_state"] = "playing" + state["score"] = 0 + state["player_y"] = GROUND_Y - 16 + state["player_vel_y"] = 0 + state["is_jumping"] = False + state["is_ducking"] = False + state["obstacles"] = [] + state["next_spawn"] = OBSTACLE_SPAWN_MIN + state["scroll_offset"] = 0 + +def spawn_obstacle(): + """Spawn a new obstacle, ensuring it's passable""" + # Pick random obstacle type + img, obs_type, ground_height = random.choice(OBSTACLE_TYPES) + + # Check if last obstacle was an air obstacle + last_was_air = False + if state["obstacles"]: + last_obs = state["obstacles"][-1] + if last_obs["type"] == "air": + last_was_air = True + + # Ensure we don't spawn two air obstacles in a row (would be impossible) + if last_was_air and obs_type == "air": + # Force ground obstacle instead + ground_obstacles = [(img, t, h) for img, t, h in OBSTACLE_TYPES if t == "ground"] + img, obs_type, ground_height = random.choice(ground_obstacles) + + # Compute spawn Y: ground obstacles rest on ground; air obstacles have fixed bottom at GROUND_Y - AIR_GAP + if obs_type == "air": + y_pos = GROUND_Y - AIR_GAP - img.height + else: + y_pos = GROUND_Y - ground_height + + obstacle = { + "x": WIDTH, + "y": y_pos, + "img": img, + "type": obs_type, + "scored": False + } + state["obstacles"].append(obstacle) + state["next_spawn"] = random.randint(OBSTACLE_SPAWN_MIN, OBSTACLE_SPAWN_MAX) + +def check_collision(): + """Check if player collides with any obstacle""" + # Reduced collision box for more forgiving gameplay + # Calculate Y position based on ducking state + if state["is_ducking"]: + collision_y = state["player_y"] + DUCKING_Y_OFFSET + collision_h = PLAYER_DUCKING_COLLISION_HEIGHT + else: + collision_y = state["player_y"] + COLLISION_Y_MARGIN + collision_h = PLAYER_COLLISION_HEIGHT + + player_rect = { + "x": PLAYER_X + COLLISION_MARGIN, + "y": collision_y, + "w": PLAYER_COLLISION_WIDTH, + "h": collision_h + } + + for obs in state["obstacles"]: + obs_w = obs["img"].width + obs_h = obs["img"].height + + # Reduced obstacle collision box + obs_rect = { + "x": obs["x"] + COLLISION_MARGIN, + "y": obs["y"] + COLLISION_MARGIN, + "w": obs_w - (COLLISION_MARGIN * 2), + "h": obs_h - (COLLISION_MARGIN * 2) + } + + # Check rectangle collision + if (player_rect["x"] < obs_rect["x"] + obs_rect["w"] and + player_rect["x"] + player_rect["w"] > obs_rect["x"] and + player_rect["y"] < obs_rect["y"] + obs_rect["h"] and + player_rect["y"] + player_rect["h"] > obs_rect["y"]): + return True + + return False + +def update(): + if state["game_state"] == "playing": + # Handle jumping + if io.BUTTON_UP in io.pressed and not state["is_jumping"]: + state["is_jumping"] = True + state["player_vel_y"] = -4.5 + + # Handle ducking + if io.BUTTON_DOWN in io.held and not state["is_jumping"]: + state["is_ducking"] = True + else: + state["is_ducking"] = False + + # Apply gravity + if state["is_jumping"]: + state["player_vel_y"] += 0.3 + state["player_y"] += state["player_vel_y"] + + # Check if landed + if state["player_y"] >= GROUND_Y - 16: + state["player_y"] = GROUND_Y - 16 + state["is_jumping"] = False + state["player_vel_y"] = 0 + + # Update obstacles + state["scroll_offset"] += SCROLL_SPEED + state["next_spawn"] -= SCROLL_SPEED + + # Spawn new obstacle if needed + if state["next_spawn"] <= 0: + spawn_obstacle() + + # Move obstacles and check scoring + for obs in state["obstacles"]: + obs["x"] -= SCROLL_SPEED + + # Score point when obstacle passes player + if not obs["scored"] and obs["x"] + obs["img"].width < PLAYER_X: + obs["scored"] = True + state["score"] += 1 + + # Remove off-screen obstacles + state["obstacles"] = [obs for obs in state["obstacles"] if obs["x"] > OBSTACLE_REMOVAL_THRESHOLD] + + # Check collision + if check_collision(): + state["game_state"] = "gameover" + + # Draw game + draw_game() + + elif state["game_state"] == "gameover": + # Draw game over screen + draw_game() + draw_gameover() + + # Press A to restart + if io.BUTTON_A in io.pressed: + reset_game() + +def draw_game(): + # Draw sky + screen.brush = SKY_COLOR + screen.clear() + + # Draw ground + screen.brush = GROUND_COLOR + screen.draw(shapes.rectangle(0, GROUND_Y, WIDTH, HEIGHT - GROUND_Y)) + + # Draw grass pattern on ground + screen.brush = GRASS_COLOR + for i in range(0, WIDTH, 8): + offset = (state["scroll_offset"] + i) % 8 + screen.draw(shapes.rectangle(i - offset, GROUND_Y, 4, 2)) + + # Draw obstacles + for obs in state["obstacles"]: + x_pos = int(obs["x"]) + y_pos = int(obs["y"]) + # Ensure coordinates are integers to prevent rendering artifacts + screen.blit(obs["img"], x_pos, y_pos) + + # Draw player + player_y = int(state["player_y"]) + if state["is_ducking"]: + # Draw ducking (squashed sprite) + # DUCKING_SPRITE_Y_OFFSET (8) positions the squashed sprite (16px->8px) visually on ground. + # DUCKING_Y_OFFSET (10) positions collision box top for reduced hit area during ducking. + screen.scale_blit(player_img, PLAYER_X, int(player_y + DUCKING_SPRITE_Y_OFFSET), 16, 8) + else: + screen.blit(player_img, PLAYER_X, player_y) + + # Draw score + screen.brush = TEXT_COLOR + screen.text(f"Score: {state['score']}", 5, 5) + +def draw_gameover(): + # Semi-transparent overlay + screen.brush = GAMEOVER_BG + screen.draw(shapes.rectangle(20, 35, 120, 50)) + + # Game over text + screen.brush = TEXT_COLOR + screen.text("GAME OVER!", 45, 45) + screen.text(f"Score: {state['score']}", 52, 60) + screen.text("Press A to restart", 25, 75) + +run(update) diff --git a/badge/apps/jungle/icon.png b/badge/apps/jungle/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4f553de763dc36ae0df9dd74a06cdd1305b30d7b GIT binary patch literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaP3?%1DUd;wl3dtTpz6=aiY77hwEes65fIY3CLkD@$_|Nf5{~)pk_Kpd~zL7NY>NEF@)oKa>4;_Cui9SD>f`WArd0M72(Vg cG=+hILkk1LFQ8Dv z3kHT#0|tgy2@DKYGZ+}e3+C(!v;j)6C3(BM0BIoj>AbrhNU@iA`ns~eU73uYur|zHBO-dC literal 0 HcmV?d00001 diff --git a/badge/apps/jungle/sprites/creature.png b/badge/apps/jungle/sprites/creature.png new file mode 100644 index 0000000000000000000000000000000000000000..76e139cb84a4d53864382e73a1ee498b59157434 GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~e!3HF=pW8M9DTQQ@AYTTCDm4a%h86~fUqGRT z7Yq!g1`G_Z5*Qe)W-u^_7tGleXakgBOY(Mi0n$M5(|LD2kYX?K^mS!_$t5djDDBcv z05Uz$)5S4F<9u?01S>PY#70i3xu(DRy$@SHF4m};@j>IrY4wbTi;>EEY9ta4KHtAN zh9kOnf1)|VYVLEA^OOHF|_BsdruRBnnrZLK_*0y#@vA{_=H<6qf8?hhx^y>RQc^-fBI1H--rvWE1J)e-!g((3U|Q6= zTk#gvpLEkiEY*VkI#lyU{B3xbw?}G%GmA6>gE}YAA`j)8AlEW@y85}Sb4q9e0Mi^k AkN^Mx literal 0 HcmV?d00001 diff --git a/badge/apps/jungle/sprites/player.png b/badge/apps/jungle/sprites/player.png new file mode 100644 index 0000000000000000000000000000000000000000..019883610493205d9876e1a06d80cb2fe17562d8 GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`3dtTpz6=aiY77hwEes65fI&pI;OIFZO;cNFy z6QEGOr;B5V#p&b(iHrjs-jC`x{jTRZT3nvABK=@?O@V0B-fg_Ah1B+(`@G40p{Ln{ z|J#~1OfDuioc+J!5M#N7!f$zhiw9qs3>%V|3#t=yrJc_&HZI8PoWuFe;9z>y_SFV9 n2HxfSkFF|va2TjWgN@gTe~DWM4fNV`=z literal 0 HcmV?d00001 From e770910f7ea43a8ce43d19625248b5e1c3bf3375 Mon Sep 17 00:00:00 2001 From: Atarukun Date: Wed, 29 Oct 2025 14:59:41 -0700 Subject: [PATCH 2/6] Update badge/apps/jungle/__init__.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- badge/apps/jungle/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/badge/apps/jungle/__init__.py b/badge/apps/jungle/__init__.py index 787299c..5a0c648 100644 --- a/badge/apps/jungle/__init__.py +++ b/badge/apps/jungle/__init__.py @@ -87,7 +87,7 @@ def spawn_obstacle(): last_was_air = False if state["obstacles"]: last_obs = state["obstacles"][-1] - if last_obs["type"] == "air": + if last_obs.get("type") == "air": last_was_air = True # Ensure we don't spawn two air obstacles in a row (would be impossible) From 16f4d1694c45993340faf209582136d90fb0a6f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:00:05 +0000 Subject: [PATCH 3/6] Initial plan From ffb852e5f1a6e45d4bacdd1d7e449e489a23b851 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:02:22 +0000 Subject: [PATCH 4/6] Define GRASS_PATTERN_WIDTH constant to replace magic number Co-authored-by: atarukun <6511495+atarukun@users.noreply.github.com> --- badge/apps/jungle/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/badge/apps/jungle/__init__.py b/badge/apps/jungle/__init__.py index 5a0c648..9eed944 100644 --- a/badge/apps/jungle/__init__.py +++ b/badge/apps/jungle/__init__.py @@ -11,6 +11,7 @@ OBSTACLE_SPAWN_MAX = 120 OBSTACLE_REMOVAL_THRESHOLD = -50 # X position at which off-screen obstacles are removed AIR_GAP = 8 # Vertical gap above ground that the branch bottom sits at (duck to clear) +GRASS_PATTERN_WIDTH = 8 # Width of the repeating grass pattern # Collision box adjustments for forgiving gameplay COLLISION_MARGIN = 2 # Pixels reduced from each side of collision boxes @@ -222,8 +223,8 @@ def draw_game(): # Draw grass pattern on ground screen.brush = GRASS_COLOR - for i in range(0, WIDTH, 8): - offset = (state["scroll_offset"] + i) % 8 + for i in range(0, WIDTH, GRASS_PATTERN_WIDTH): + offset = (state["scroll_offset"] + i) % GRASS_PATTERN_WIDTH screen.draw(shapes.rectangle(i - offset, GROUND_Y, 4, 2)) # Draw obstacles From 7722ac468522d2833b971363ecb4810aadad2246 Mon Sep 17 00:00:00 2001 From: Atarukun Date: Wed, 29 Oct 2025 15:12:58 -0700 Subject: [PATCH 5/6] Update badge/apps/jungle/__init__.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- badge/apps/jungle/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/badge/apps/jungle/__init__.py b/badge/apps/jungle/__init__.py index 9eed944..d39501b 100644 --- a/badge/apps/jungle/__init__.py +++ b/badge/apps/jungle/__init__.py @@ -259,4 +259,5 @@ def draw_gameover(): screen.text(f"Score: {state['score']}", 52, 60) screen.text("Press A to restart", 25, 75) -run(update) +if __name__ == "__main__": + run(update) From 823ae10643630d6a929c1afad012a2775201253f Mon Sep 17 00:00:00 2001 From: Atarukun Date: Wed, 29 Oct 2025 15:19:54 -0700 Subject: [PATCH 6/6] Update badge/apps/jungle/__init__.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- badge/apps/jungle/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/badge/apps/jungle/__init__.py b/badge/apps/jungle/__init__.py index d39501b..f237448 100644 --- a/badge/apps/jungle/__init__.py +++ b/badge/apps/jungle/__init__.py @@ -223,8 +223,8 @@ def draw_game(): # Draw grass pattern on ground screen.brush = GRASS_COLOR + offset = state["scroll_offset"] % GRASS_PATTERN_WIDTH for i in range(0, WIDTH, GRASS_PATTERN_WIDTH): - offset = (state["scroll_offset"] + i) % GRASS_PATTERN_WIDTH screen.draw(shapes.rectangle(i - offset, GROUND_Y, 4, 2)) # Draw obstacles