Skip to main content

Overview

Worm Bucket is a time-based catching game where players tap emerging worms to catch them with Nyuron’s extending claw. Caught worms go into a bucket, but they can escape if not recaptured quickly. Bad worms stun the player for 2 seconds, adding risk to quick tapping. Scene Path: res://minigames/worm_bucket/scripts/main.gd

Cognitive Skills Developed

  • Visual Tracking: Following multiple moving worms simultaneously
  • Timing: Tapping at optimal moment when worm fully emerges
  • Selective Attention: Distinguishing good worms from bad worms
  • Motor Control: Precise tapping under time pressure
  • Decision Making: Prioritizing which worms to catch first
  • Impulse Control: Avoiding bad worms despite rapid gameplay

How to Play

Controls

  • Tap Worm: Click/tap worm to catch with claw
  • Back Button: Pause game

Objective

Catch as many worms as possible in 60 seconds while avoiding bad worms. Recapture escaping worms to maintain bucket count.

Gameplay Flow

  1. Start: Tap “Play” on intro screen
  2. Catch: Tap emerging worms (green)
  3. Avoid: Don’t tap bad worms (red)
  4. Recapture: Stop escaping worms from leaving bucket
  5. Time Up: Score = total worms caught

Scoring System

Point Mechanics

var score := 0

if not is_recapture:
    score += 1
    
func _on_worm_escaped(_worm: Node) -> void:
    score = max(0, score - 1)
  • Catch Normal Worm: +1 point
  • Recatch Escaping Worm: 0 points (no bonus/penalty)
  • Worm Fully Escapes: -1 point (minimum 0)
  • Bad Worm: No point change (2s stun penalty)

Coin Conversion

var coins_earned = int(score * 2.5)
score_manager.add_coins(coins_earned)
Coins = 2.5x final score (highest multiplier in the game) Examples:
  • 10 worms: 25 coins
  • 20 worms: 50 coins
  • 40 worms: 100 coins

High Score Tracking

score_manager.save_high_score("worm_catch", score)
Saved under key “worm_catch” in global ScoreManager.

Time Limit

@onready var game_timer: Timer = $GameTimer

func _process(_delta: float) -> void:
    var t_left: float = max(0.0, game_timer.time_left)
    ui_time.text = "%.2f s" % t_left
Default game duration: 60 seconds (configurable in Timer node)

Worm Types

Normal Worms

  • Appearance: Green/brown coloring
  • Behavior: Emerge, wait briefly, retract
  • Effect: +1 score when caught

Bad Worms

if "is_bad" in worm and worm.is_bad:
    sfx_fail.play()
    is_stunned = true
    if nyuron.has_method("stun"):
        nyuron.stun(2.0)
    _start_damage_pulse()
    
    get_tree().create_timer(2.0).timeout.connect(func ():
        is_stunned = false
        _stop_damage_pulse()
    )
  • Appearance: Red coloring or spikes
  • Behavior: Emerges like normal worm
  • Effect: 2-second stun, screen pulse, no score change
  • Purpose: Teaches impulse control

Escaping Worms

func _on_escape_timer() -> void:
    if bucket_count <= 0:
        return

    bucket_count = max(0, bucket_count - 1)
    _update_ui()

    var w = spawner.worm_scene.instantiate()
    $Playfield.add_child(w)

    if w.has_method("start_escape"):
        w.start_escape(bucket.global_position)
  • Trigger: EscapeTimer timeout (periodic)
  • Appearance: Worm emerges from bucket position
  • Behavior: Moves away from bucket toward edge
  • Effect: Must be recaptured or -1 score on full escape

Key Code Examples

Worm Spawning

@onready var spawner = $Playfield/Spawner

func _on_spawner_spawned(worm: Node) -> void:
    if "game" in worm:
        worm.game = self

    if worm.has_signal("request_catch"):
        worm.request_catch.connect(_on_worm_request_catch)
    if worm.has_signal("escaped"):
        worm.escaped.connect(_on_worm_escaped)

    if worm is Area2D:
        worm.input_pickable = true
        worm.input_event.connect(_on_worm_input_event.bind(worm))
Dynamic signal connection when spawner creates new worms.

Claw Shooting Mechanic

func _on_worm_request_catch(worm: Node) -> void:
    if is_paused or is_stunned:
        return

    # Check for bad worm
    if "is_bad" in worm and worm.is_bad:
        # Handle stun...
        return

    var is_recapture: bool = ("state" in worm and worm.state == "ESCAPING")

    # Shoot claw to worm position
    if nyuron.has_method("shoot_to"):
        nyuron.shoot_to(worm.global_position, is_recapture)

    # When claw reaches target
    if nyuron.has_signal("claw_reached_target"):
        nyuron.claw_reached_target.connect(func (_pos):
            if is_instance_valid(worm):
                var claw_tip_to_use = nyuron.claw_tip if not is_recapture else nyuron.claw_tip_recap
                if "attach_to_claw" in worm:
                    worm.attach_to_claw(claw_tip_to_use)
        , CONNECT_ONE_SHOT)

    # When claw retracts
    if nyuron.has_signal("shot_finished"):
        nyuron.shot_finished.connect(func ():
            if not is_instance_valid(worm):
                return
            if "detach_and_go_to_bucket" in worm:
                worm.detach_and_go_to_bucket()
            
            if not is_recapture:
                score += 1
                sfx_catch.pitch_scale = randf_range(0.9, 1.1)
                sfx_catch.play()
            
            bucket_count += 1
            _update_ui()
        , CONNECT_ONE_SHOT)
Animation Sequence:
  1. Player taps worm
  2. Nyuron’s claw extends to worm position
  3. claw_reached_target signal fires
  4. Worm attaches to claw tip
  5. Claw retracts
  6. shot_finished signal fires
  7. Worm moves to bucket
  8. Score/bucket updated

Bucket Visual States

func _update_ui() -> void:
    ui_score.text = "Puntos: %d" % score

    if bucket_sprite:
        match bucket_count:
            0: bucket_sprite.play("balde vacio")
            1: bucket_sprite.play("con 1 gusano")
            2: bucket_sprite.play("con 2 gusano")
            3: bucket_sprite.play("con 3 gusano")
            4: bucket_sprite.play("con 4 gusano")
            _: bucket_sprite.play("balde lleno")
Bucket sprite shows visual fullness based on worm count (0-5+ states).

Stun Visual Feedback

func _start_damage_pulse() -> void:
    if damage_tween:
        damage_tween.kill()
    damage_tween = create_tween().set_loops()
    damage_tween.tween_property(damage_mat, "shader_parameter/intensity", 0.4, 0.2).set_trans(Tween.TRANS_SINE)
    damage_tween.tween_property(damage_mat, "shader_parameter/intensity", 0.0, 0.2).set_trans(Tween.TRANS_SINE)

func _stop_damage_pulse() -> void:
    if damage_tween:
        damage_tween.kill()
        damage_tween = null
    if damage_mat:
        damage_mat.set_shader_parameter("intensity", 0.0)
Red border pulses during 2-second stun from bad worm.

Audio System

@onready var sfx_catch: AudioStreamPlayer = $SFX_Catch
@onready var sfx_fail: AudioStreamPlayer = $SFX_Fail

sfx_catch.pitch_scale = randf_range(0.9, 1.1)
sfx_catch.play()
  • Catch Sound: Varied pitch (0.9-1.1x) to avoid repetition
  • Fail Sound: Plays on bad worm tap

Nyuron Component

The player character with claw mechanics:
@onready var nyuron = $Nyuron
Required Methods:
  • shoot_to(position: Vector2, is_recapture: bool)
  • stun(duration: float)
Required Signals:
  • claw_reached_target(position: Vector2)
  • shot_finished()
Required Properties:
  • claw_tip: Node2D for normal catch attachment
  • claw_tip_recap: Node2D for recapture attachment

Worm Component

Each worm must implement: Required Properties:
  • is_bad: bool (optional, false by default)
  • state: String (“EMERGING”, “WAITING”, “ESCAPING”, etc.)
  • game: Reference to main scene
Required Signals:
  • request_catch(worm: Node) - Emitted when tapped
  • escaped(worm: Node) - Emitted when fully escapes
Required Methods:
  • attach_to_claw(claw_tip: Node2D)
  • detach_and_go_to_bucket()
  • start_escape(from_position: Vector2) (for escaping worms)

Spawner Component

@onready var spawner = $Playfield/Spawner
Required Properties:
  • worm_scene: PackedScene for worm instantiation
Required Signals:
  • spawned(worm: Node) - Emitted when new worm created
Contains:
  • SpawnTimer: Timer controlling spawn intervals

Escape Mechanic

@onready var escape_timer: Timer = $EscapeTimer
var bucket_count := 0

func _on_escape_timer() -> void:
    if bucket_count <= 0:
        return
    
    bucket_count = max(0, bucket_count - 1)
    var w = spawner.worm_scene.instantiate()
    w.start_escape(bucket.global_position)
    $Playfield.add_child(w)
Escape Frequency: Configurable via EscapeTimer (default ~5-10 seconds) Strategy: Maintaining high bucket count increases escape pressure, forcing active recapture.

Pause System

func pause_game():
    is_paused = true

    game_timer.paused = true
    escape_timer.paused = true
    if spawner and spawner.has_node("SpawnTimer"):
        spawner.get_node("SpawnTimer").paused = true

    if nyuron:
        nyuron.process_mode = Node.PROCESS_MODE_DISABLED
        var nyuron_anim := nyuron.get_node_or_null("AnimatedSprite2D")
        if nyuron_anim:
            nyuron_anim.speed_scale = 0.0

    pause_all_worms(true)

    if bucket_sprite:
        bucket_sprite.speed_scale = 0.0

    if damage_tween:
        damage_tween.kill()

func pause_all_worms(pause: bool):
    for worm in $Playfield.get_children():
        if is_instance_valid(worm) and worm is Area2D:
            if pause:
                worm.process_mode = Node.PROCESS_MODE_DISABLED
                var worm_anim := worm.get_node_or_null("AnimatedSprite2D")
                if worm_anim:
                    worm_anim.speed_scale = 0.0
            else:
                worm.process_mode = Node.PROCESS_MODE_INHERIT
                var worm_anim := worm.get_node_or_null("AnimatedSprite2D")
                if worm_anim:
                    worm_anim.speed_scale = 1.0
Pauses all timers, animations, and physics for perfect state preservation.

Game Over Flow

func _on_game_over() -> void:
    $Playfield/Spawner/SpawnTimer.stop()
    escape_timer.stop()

    ui_time.text = "Fin. Puntos: %d" % score

    if score > 0:
        var score_manager = get_node("/root/ScoreManager")
        score_manager.save_high_score("worm_catch", score)
        var coins_earned = int(score * 2.5)
        score_manager.add_coins(coins_earned)

    _show_game_over_panel()

    for worm in $Playfield.get_children():
        if worm is Area2D:
            worm.input_pickable = false
Disables all input and timers when game timer expires.

Optimal Strategy

High scores require balancing:
  1. Speed: Catch worms quickly before they retract
  2. Accuracy: Avoid bad worms to prevent 2s stun
  3. Prioritization: Recapture escaping worms first (prevent -1)
  4. Risk Management: Don’t tap uncertain worms near time limit
Score Breakdown for 60s:
  • Excellent: 40+ worms (0.67 worms/second)
  • Good: 25-40 worms (0.42-0.67 worms/second)
  • Average: 15-25 worms (0.25-0.42 worms/second)
  • Learning: <15 worms

Screen Configuration

DisplayServer.screen_set_orientation(DisplayServer.SCREEN_PORTRAIT)
get_tree().root.set_content_scale_size(Vector2i(270, 480))
Standard portrait mode with playfield at top, bucket at bottom.

Educational Value

Worm Bucket develops:
  • Inhibitory Control: Not tapping bad worms despite rapid gameplay
  • Working Memory: Tracking multiple worm positions and states
  • Processing Speed: Quick decision-making under time pressure
  • Sustained Attention: Maintaining focus for full 60 seconds
  • Hand-Eye Coordination: Precise tapping on moving targets
The 2-second stun penalty makes this game particularly effective for teaching impulse control to children.

Food Catch

Similar catching mechanics with passive falling

Minigames Overview

Return to all minigames

Build docs developers (and LLMs) love