forked from badger/home
-
Notifications
You must be signed in to change notification settings - Fork 0
Jungle, a tiny game for the Github Badge #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
atarukun
wants to merge
7
commits into
main
Choose a base branch
from
jungle
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
3e9b243
Jungle, a tiny game for the Github Badge
atarukun e770910
Update badge/apps/jungle/__init__.py
atarukun 16f4d16
Initial plan
Copilot ffb852e
Define GRASS_PATTERN_WIDTH constant to replace magic number
Copilot eb485e1
Merge pull request #2 from atarukun/copilot/sub-pr-1
atarukun 7722ac4
Update badge/apps/jungle/__init__.py
atarukun 823ae10
Update badge/apps/jungle/__init__.py
atarukun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,263 @@ | ||
| 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) | ||
| 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 | ||
| 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.get("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""" | ||
|
atarukun marked this conversation as resolved.
|
||
| # 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(): | ||
|
atarukun marked this conversation as resolved.
|
||
| if state["game_state"] == "playing": | ||
| # Handle jumping | ||
| if io.BUTTON_UP in io.pressed and not state["is_jumping"]: | ||
|
atarukun marked this conversation as resolved.
|
||
| state["is_jumping"] = True | ||
| state["player_vel_y"] = -4.5 | ||
|
|
||
| # Handle ducking | ||
| if io.BUTTON_DOWN in io.held and not state["is_jumping"]: | ||
|
atarukun marked this conversation as resolved.
|
||
| 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(): | ||
|
atarukun marked this conversation as resolved.
|
||
| # 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 | ||
| offset = state["scroll_offset"] % GRASS_PATTERN_WIDTH | ||
| for i in range(0, WIDTH, GRASS_PATTERN_WIDTH): | ||
| 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(): | ||
|
atarukun marked this conversation as resolved.
|
||
| # 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) | ||
|
|
||
| if __name__ == "__main__": | ||
| run(update) | ||
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.