---
name: badger-app-creator
description: Create MicroPython applications for Universe 2025 (Tufty) Badge including display graphics, button handling, and MonaOS app structure. Use when building badge apps, creating interactive displays, or developing MicroPython programs.
---

# Universe 2025 Badge App Creator

Create well-structured MicroPython applications for the **Universe 2025 (Tufty) Badge** with MonaOS integration, display graphics, button handling, and proper app architecture.

## Important: MonaOS App Structure

**Critical**: MonaOS apps follow a specific structure. Each app is a directory in `/system/apps/` containing:

```
/system/apps/my_app/
├── icon.png          # 24x24 PNG icon
├── __init__.py       # Entry point with update() function
└── assets/           # Optional: app assets (auto-added to path)
    └── ...
```

### Required Functions

Your `__init__.py` must implement:

**`update()`** - Required, called every frame by MonaOS:
```python
def update():
    # Called every frame
    # Draw your UI, handle input, update state
    pass
```

**`init()`** - Optional, called once when app launches:
```python
def init():
    # Initialize app state, load resources
    pass
```

**`on_exit()`** - Optional, called when HOME button pressed:
```python
def on_exit():
    # Save state, cleanup resources
    pass
```

## MonaOS App Template

```python
# __init__.py - MonaOS app template
from badgeware import screen, brushes, shapes, io, PixelFont, Image

# App state
app_state = {
    "counter": 0,
    "color": (255, 255, 255)
}

def init():
    """Called once when app launches"""
    # Load font
    screen.font = PixelFont.load("nope.ppf")

    # Load saved state if exists
    try:
        with open("/storage/myapp_state.txt", "r") as f:
            app_state["counter"] = int(f.read())
    except:
        pass

    print("App initialized!")

def update():
    """Called every frame by MonaOS"""
    # Clear screen
    screen.brush = brushes.color(20, 40, 60)
    screen.clear()

    # Draw UI
    screen.brush = brushes.color(255, 255, 255)
    screen.text("My App", 10, 10)
    screen.text(f"Count: {app_state['counter']}", 10, 30)

    # Handle buttons (checked every frame)
    if io.BUTTON_A in io.pressed:
        app_state["counter"] += 1

    if io.BUTTON_B in io.pressed:
        app_state["counter"] = 0

    # HOME button exits automatically

def on_exit():
    """Called when returning to MonaOS menu"""
    # Save state
    with open("/storage/myapp_state.txt", "w") as f:
        f.write(str(app_state["counter"]))

    print("App exiting!")
```

## Display API (badgeware)

### Import Modules

```python
from badgeware import screen, brushes, shapes, Image, PixelFont, Matrix, io
```

### Screen Drawing (160x120 framebuffer)

The screen is a 160×120 RGB framebuffer that MonaOS automatically pixel-doubles to 320×240.

**Basic Drawing**:
```python
# Set brush color (RGB 0-255)
screen.brush = brushes.color(r, g, b)

# Clear screen
screen.clear()

# Draw text
screen.text("Hello", x, y)

# Draw shapes
screen.draw(shapes.rectangle(x, y, width, height))
screen.draw(shapes.circle(x, y, radius))
screen.draw(shapes.line(x1, y1, x2, y2))
screen.draw(shapes.arc(x, y, radius, start_angle, end_angle))
screen.draw(shapes.pie(x, y, radius, start_angle, end_angle))
```

**Antialiasing** (smooth edges):
```python
screen.antialias = Image.X4  # Enable 4x antialiasing
screen.antialias = Image.NONE  # Disable
```

**No Manual Update Needed**: MonaOS automatically updates the display after each `update()` call.

### Shapes Module

Full documentation: https://github.com/badger/home/blob/main/badgerware/shapes.md

**Available Shapes**:
```python
from badgeware import shapes

# Rectangle
shapes.rectangle(x, y, width, height)

# Circle
shapes.circle(x, y, radius)

# Line
shapes.line(x1, y1, x2, y2)

# Arc (portion of circle outline)
shapes.arc(x, y, radius, start_angle, end_angle)

# Pie (filled circle segment)
shapes.pie(x, y, radius, start_angle, end_angle)

# Rounded rectangle
shapes.rounded_rectangle(x, y, width, height, radius)

# Regular polygon (pentagon, hexagon, etc.)
shapes.regular_polygon(x, y, sides, radius)

# Squircle (smooth rectangle-circle hybrid)
shapes.squircle(x, y, width, height)
```

**Transformations**:
```python
from badgeware import Matrix

# Create shape
rect = shapes.rectangle(-1, -1, 2, 2)

# Apply transformation
rect.transform = Matrix() \
    .translate(80, 60) \  # Move to center
    .scale(20, 20) \      # Scale up
    .rotate(io.ticks / 100)  # Animated rotation

screen.draw(rect)
```

### Brushes Module

Full documentation: https://github.com/badger/home/blob/main/badgerware/brushes.md

**Solid Colors**:
```python
from badgeware import brushes

# RGB color (0-255 per channel)
screen.brush = brushes.color(r, g, b)

# Examples
screen.brush = brushes.color(255, 0, 0)     # Red
screen.brush = brushes.color(0, 255, 0)     # Green
screen.brush = brushes.color(0, 0, 255)     # Blue
screen.brush = brushes.color(255, 255, 255) # White
screen.brush = brushes.color(0, 0, 0)       # Black
```

### Fonts

Full documentation: https://github.com/badger/home/blob/main/PixelFont.md

**30 Licensed Pixel Fonts Included**:
```python
from badgeware import PixelFont

# Load font
screen.font = PixelFont.load("nope.ppf")

# Draw text with loaded font
screen.text("Styled text", x, y)

# Measure text width
width = screen.font.measure("text to measure")

# Reset to default font
screen.font = None
```

### Images & Sprites

Full documentation: https://github.com/badger/home/blob/main/badgerware/Image.md

**Loading Images**:
```python
from badgeware import Image

# Load PNG image
img = Image.load("sprite.png")

# Blit to screen
screen.blit(img, x, y)

# Scaled blit
screen.scale_blit(img, x, y, width, height)
```

**Sprite Sheets**:
```python
# Using SpriteSheet helper (from examples)
from lib import SpriteSheet

# Load sprite sheet (7 columns, 1 row)
sprites = SpriteSheet("assets/mona-sprites.png", 7, 1)

# Blit specific sprite (column 0, row 0)
screen.blit(sprites.sprite(0, 0), x, y)

# Scaled sprite
screen.scale_blit(sprites.sprite(3, 0), x, y, 30, 30)
```

## Button Handling (io module)

Full documentation: https://github.com/badger/home/blob/main/badgerware/io.md

### Button Constants

```python
from badgeware import io

# Available buttons
io.BUTTON_A       # Left button
io.BUTTON_B       # Middle button
io.BUTTON_C       # Right button
io.BUTTON_UP      # Up button
io.BUTTON_DOWN    # Down button
io.BUTTON_HOME    # HOME button (exits to MonaOS)
```

### Button States

Check button states within your `update()` function:

```python
def update():
    # Button just pressed this frame
    if io.BUTTON_A in io.pressed:
        print("A was just pressed")

    # Button just released this frame
    if io.BUTTON_B in io.released:
        print("B was just released")

    # Button currently held down
    if io.BUTTON_C in io.held:
        print("C is being held")

    # Button state changed this frame (pressed or released)
    if io.BUTTON_UP in io.changed:
        print("UP state changed")
```

**No Debouncing Needed**: The io module handles button debouncing automatically.

### Menu Navigation Example

```python
menu_items = ["Option 1", "Option 2", "Option 3", "Option 4"]
selected = 0

def update():
    global selected

    # Clear screen
    screen.brush = brushes.color(20, 40, 60)
    screen.clear()

    # Draw title
    screen.brush = brushes.color(255, 255, 255)
    screen.text("Menu", 10, 5)

    # Draw menu items
    y = 30
    for i, item in enumerate(menu_items):
        if i == selected:
            # Highlight selected item
            screen.brush = brushes.color(255, 255, 0)
            screen.text("> " + item, 10, y)
        else:
            screen.brush = brushes.color(200, 200, 200)
            screen.text("  " + item, 10, y)
        y += 20

    # Handle navigation
    if io.BUTTON_UP in io.pressed:
        selected = (selected - 1) % len(menu_items)

    if io.BUTTON_DOWN in io.pressed:
        selected = (selected + 1) % len(menu_items)

    if io.BUTTON_A in io.pressed:
        print(f"Selected: {menu_items[selected]}")
```

## Animation & Timing

### Using io.ticks

```python
from badgeware import io
import math

def update():
    # io.ticks increments every frame
    # Use for smooth animations

    # Oscillating value
    y = (math.sin(io.ticks / 100) * 30) + 60

    # Rotating shape
    angle = io.ticks / 50
    rect = shapes.rectangle(-1, -1, 2, 2)
    rect.transform = Matrix().translate(80, 60).rotate(angle)
    screen.draw(rect)

    # Pulsing size
    scale = (math.sin(io.ticks / 60) * 10) + 20
    circle = shapes.circle(80, 60, scale)
    screen.draw(circle)
```

## State Management

### Persistent Storage

Store app data in the writable LittleFS partition at `/storage/`:

```python
import json

CONFIG_FILE = "/storage/myapp_config.json"

def save_config(data):
    """Save configuration to persistent storage"""
    try:
        with open(CONFIG_FILE, "w") as f:
            json.dump(data, f)
        print("Config saved!")
    except Exception as e:
        print(f"Save failed: {e}")

def load_config():
    """Load configuration from persistent storage"""
    try:
        with open(CONFIG_FILE, "r") as f:
            return json.load(f)
    except:
        # Return defaults if file doesn't exist
        return {
            "name": "Badge User",
            "theme": "light",
            "counter": 0
        }

# Usage in app
config = {}

def init():
    global config
    config = load_config()
    print(f"Loaded: {config}")

def on_exit():
    save_config(config)
```

### State Machine Pattern

```python
class AppState:
    MENU = 0
    GAME = 1
    SETTINGS = 2
    GAME_OVER = 3

state = AppState.MENU
game_data = {"score": 0, "level": 1}

def update():
    global state

    if state == AppState.MENU:
        draw_menu()
        if io.BUTTON_A in io.pressed:
            state = AppState.GAME

    elif state == AppState.GAME:
        update_game()
        draw_game()
        if game_data["score"] < 0:
            state = AppState.GAME_OVER

    elif state == AppState.SETTINGS:
        draw_settings()
        if io.BUTTON_B in io.pressed:
            state = AppState.MENU

    elif state == AppState.GAME_OVER:
        draw_game_over()
        if io.BUTTON_A in io.pressed:
            state = AppState.MENU
            game_data = {"score": 0, "level": 1}

def draw_menu():
    screen.brush = brushes.color(0, 0, 0)
    screen.clear()
    screen.brush = brushes.color(255, 255, 255)
    screen.text("MAIN MENU", 40, 50)
    screen.text("Press A to start", 30, 70)

def update_game():
    # Game logic
    game_data["score"] += 1

def draw_game():
    screen.brush = brushes.color(0, 0, 0)
    screen.clear()
    screen.brush = brushes.color(255, 255, 255)
    screen.text(f"Score: {game_data['score']}", 10, 10)

def draw_settings():
    # Settings UI
    pass

def draw_game_over():
    screen.brush = brushes.color(0, 0, 0)
    screen.clear()
    screen.brush = brushes.color(255, 0, 0)
    screen.text("GAME OVER", 40, 50)
    screen.text(f"Score: {game_data['score']}", 40, 70)
```

## WiFi Integration

Use standard MicroPython network module:

```python
import network
import time

def connect_wifi(ssid, password):
    """Connect to WiFi network"""
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)

    if wlan.isconnected():
        print("Already connected:", wlan.ifconfig()[0])
        return True

    print(f"Connecting to {ssid}...")
    wlan.connect(ssid, password)

    # Wait for connection (with timeout)
    timeout = 10
    while not wlan.isconnected() and timeout > 0:
        time.sleep(1)
        timeout -= 1

    if wlan.isconnected():
        print("Connected:", wlan.ifconfig()[0])
        return True
    else:
        print("Connection failed")
        return False

def fetch_data(url):
    """Fetch data from URL"""
    try:
        import urequests
        response = urequests.get(url)
        data = response.json()
        response.close()
        return data
    except Exception as e:
        print(f"Error fetching data: {e}")
        return None

# Usage in app
def init():
    if connect_wifi("MyWiFi", "password123"):
        data = fetch_data("https://api.example.com/data")
        if data:
            print("Got data:", data)
```

Full WiFi docs: https://docs.micropython.org/en/latest/rp2/quickref.html#wlan

## Performance Optimization

### Reduce Draw Calls

```python
# Bad - many individual draws
def update():
    for i in range(100):
        screen.draw(shapes.rectangle(i, i, 2, 2))

# Better - batch or optimize
def update():
    # Draw fewer, larger shapes
    screen.draw(shapes.rectangle(0, 0, 100, 100))
```

### Cache Computed Values

```python
# Cache expensive calculations
_cached_sprites = None

def get_sprites():
    global _cached_sprites
    if _cached_sprites is None:
        _cached_sprites = SpriteSheet("sprites.png", 8, 8)
    return _cached_sprites

def update():
    sprites = get_sprites()  # Fast after first call
    screen.blit(sprites.sprite(0, 0), 10, 10)
```

### Minimize Memory Allocation

```python
# Bad - creates new lists every frame
def update():
    items = [1, 2, 3, 4, 5]  # Don't do this in update()
    for item in items:
        process(item)

# Good - create once, reuse
items = [1, 2, 3, 4, 5]  # Module level

def update():
    for item in items:  # Reuse existing list
        process(item)
```

## Project Structure Best Practices

### Simple App (Single File)

```
my_app/
├── icon.png
└── __init__.py
```

### Complex App (Multiple Files)

```
my_app/
├── icon.png
├── __init__.py      # Entry point
└── assets/
    ├── sprites.png
    ├── font.ppf
    └── config.json
```

Access assets using relative paths (assets/ is auto-added to sys.path):

```python
# In __init__.py
from badgeware import Image

# Load from assets/
sprite = Image.load("assets/sprites.png")

# Or if assets/ in path:
sprite = Image.load("sprites.png")
```

## Error Handling

```python
def update():
    """Update with error handling"""
    try:
        # Your update code
        draw_ui()
        handle_input()
    except Exception as e:
        # Show error on screen
        screen.brush = brushes.color(255, 0, 0)
        screen.clear()
        screen.brush = brushes.color(255, 255, 255)
        screen.text("Error:", 10, 10)
        screen.text(str(e)[:30], 10, 30)

        # Log to console for debugging
        import sys
        sys.print_exception(e)
```

## Testing & Debugging

### Test Locally

```bash
# Run app temporarily without installing
mpremote run my_app/__init__.py
```

### REPL Debugging

```bash
# Connect to REPL
mpremote

# Test imports
>>> from badgeware import screen, brushes
>>> screen.brush = brushes.color(255, 0, 0)
>>> screen.clear()
```

### Print Debugging

```python
def update():
    # Print statements appear in REPL/serial console
    print(f"State: {state}, Counter: {counter}")

    # Draw debug info on screen
    screen.text(f"Debug: {value}", 0, 110)
```

## Official Examples

Study official examples: https://github.com/badger/home/tree/main/badge/apps

Key examples:
- **Commits Game**: Sprite animations, collision detection
- **Snake Game**: Grid-based movement, state management
- **Menu System**: Navigation, app launching

## Official API Documentation

- **Image class**: https://github.com/badger/home/blob/main/badgerware/Image.md
- **shapes module**: https://github.com/badger/home/blob/main/badgerware/shapes.md
- **brushes module**: https://github.com/badger/home/blob/main/badgerware/brushes.md
- **PixelFont class**: https://github.com/badger/home/blob/main/PixelFont.md
- **Matrix class**: https://github.com/badger/home/blob/main/Matrix.md
- **io module**: https://github.com/badger/home/blob/main/badgerware/io.md

## Complete Example App

```python
# __init__.py - Complete counter app with persistence
from badgeware import screen, brushes, shapes, io, PixelFont
import json

# State
counter = 0
high_score = 0

def init():
    """Load saved state"""
    global counter, high_score

    screen.font = PixelFont.load("nope.ppf")

    try:
        with open("/storage/counter_state.json", "r") as f:
            data = json.load(f)
            counter = data.get("counter", 0)
            high_score = data.get("high_score", 0)
    except:
        pass

    print(f"Counter initialized: {counter}, High: {high_score}")

def update():
    """Update every frame"""
    global counter, high_score

    # Clear screen
    screen.brush = brushes.color(20, 40, 60)
    screen.clear()

    # Draw title
    screen.brush = brushes.color(255, 255, 255)
    screen.text("COUNTER APP", 30, 10)

    # Draw counter (large)
    screen.text(f"{counter}", 60, 40, scale=3)

    # Draw high score
    screen.text(f"High: {high_score}", 40, 80)

    # Draw instructions
    screen.text("A: +1  B: Reset", 20, 105)

    # Handle buttons
    if io.BUTTON_A in io.pressed:
        counter += 1
        if counter > high_score:
            high_score = counter

    if io.BUTTON_B in io.pressed:
        counter = 0

def on_exit():
    """Save state before exit"""
    try:
        with open("/storage/counter_state.json", "w") as f:
            json.dump({
                "counter": counter,
                "high_score": high_score
            }, f)
        print("State saved!")
    except Exception as e:
        print(f"Save failed: {e}")
```

## Next Steps

- **See Official Hacks**: https://badger.github.io/hacks/
- **Explore Badge Hardware**: Use `badger-hardware` skill
- **WiFi & Bluetooth**: See MicroPython docs
- **Deploy Your App**: Use `badger-deploy` skill

Happy coding! 🦡
