Bounce Ball

Hey everyone!

Sharing my latest game.

05222-ezgif.com-resize

The game is already fully playable in your browser on Poki, weighs in at under 3.5-4 MB, and even runs smoothly on potato. We’re about 80 % of the way to the feature-complete version, but there’s already plenty to enjoy—and I’d love to hear what you think.


Heart-felt thanks

Special thanks to @aglitchman—his help was priceless. Bounce Ball was built around his high-quality trail system. When it began throwing errors, he not only repaired the code for the project but also answered an endless stream of my naïve questions, patiently explained how everything works, and delivered powerful feedback. At times it felt like he was doing more of the heavy lifting than I was. I’m immensely grateful.

Many thanks as well to the Defold community, especially @AGulev and @BunBunBun. As an artist, Defold still isn’t the most intuitive toolset for me, but the speed at which the community steps in to help (and the core team rolls out fixes) more than makes up for it. They are the reason this entire build fits in 4 MB yet still feels slick.

Finally, credit to the Poki team—none of them will probably read this, but their developer portal, fit-tests, and video tooling allowed us to polish the game’s presentation in ways that simply wouldn’t have been feasible otherwise. Huge respect!

Behind-the-scenes lessons & tools

  • First-ever shaders. This project finally pushed me to write custom shaders. Shaders itself wasn’t the hard part—the real challenge was persuading Defold’s material pipeline to accept and wire them up correctly. Harder than expected, but very satisfying once they lit up on screen.
  • The indefatigable testing bot. I also built an autopilot bot that grinds through Bounce Ball for hours while I’m sleeping, eating, or showering. It records video, logs stats, and tries every combination of options. Its random, rule-oblivious clicks have exposed more bugs than I ever could— and, thankfully, it works for free. Simulating a “typical” player this way has been invaluable, and I’ve grown rather fond of the little QA gremlin.

What’s next?

  • Add the missing 10 power-ups/objects
  • More visual juice (screen shake and better particle bursts)
  • Localise

How you can help

  • Play the current build (still in soft launch) on Poki and tell me what feels fun—or frustrating.
  • Spot a bug? Post it here and I’ll squash it.
  • If you enjoy the game, a simple Like and small review on Poki is the best thank-you you can give right now.

image

17 Likes

We have the best community in the world. :heart:

I love it! Sounds like you built a really useful tool for yourself!

6 Likes

thanks for building it and thanks for your engine


it is really helpful when numbers are starting to grow, like after 6-8 hours of playing.

5 Likes

This is a very engaging game. I spent quite some time playing it and even came back later to finish it off :slight_smile:
I’m not sure if it was intentional, but I really appreciate that the game doesn’t overwhelm the player with closing blocks. instead, it feels quite meditative.
May I ask how the ball movement is handled? Is it done using built-in physics?

1 Like

Thanks for the feedback—that’s pretty much exactly what we were aiming for.
Yes, we’re using the engine’s built-in physics and no extra libraries. In my experience it’s best not to mess around with physics and just rely on what’s already in the engine; otherwise you can spend ages chasing bottlenecks and bugs and wondering why the game behaves differently from the last update.

4 Likes

Thank you so much for the initial support, the game got a nice boost thanks to your votes and is feeling good right now, it’s above average poki game in terms of performance.

Came up with the idea to freshen up the boss collection a bit, so introduced combining different bosses into some kind of brainrot monster. The mechanics seem to look organic, hopefully it will appeal to everyone.

P.S. With this update I’ve certainly gotten further away from God, but I hope that the average play time will be higher

8 Likes

CONGRATS! I’ve been trying to publish a game on Poki for a year now and I never passed the 3 min filters.
Your game is really nice, great sfx and addictive. Awesome work!

4 Likes

Thanks a ton for the kind words! Welcome to the Defold community—keep at it and you’ll nail those 3-minute Poki checks soon.

1 Like

i`ve made a ton of updates with sfx, animatons bosses etc, but most important - we are checking how event updates can perform in web games.

We are using update for 1 month Obon event with Mega Booster in the end as a reward.

i will post results after fingers crossed

7 Likes

As promised, checking in after the Obon holiday event. 18,000 players finished it — roughly 10× more than we expected, especially given it takes about 5–8 hours to complete.

We’ve also launched on Google Play:

Bounce Ball on Google Play

Right now it’s running on purely organic traffic (no UA spend yet), so we’d really appreciate your ratings and reviews.

To be a bit closer to mobile F2P norms, we’ve added a shop and special boosters. The build is lightweight and runs smoothly even on very old devices.

What’s next

  • An update with 50 new mini-bosses (in progress)

  • A Halloween update

  • Plus 3 additional enemies
    After that, we’ll consider the game about 95% complete.

Results on the web have been very strong — now the goal is to bring that success over to mobile.
Thanks for playing and for any feedback you can share!

12 Likes

Quick heads-up: the Halloween event build is finished and currently in review. This is our biggest update so far.

What’s new

  • Limited-time Halloween Event + a new Daily Event system

  • New boosters, including an exclusive booster unlocked after completing the event

  • new enemies, including:

    • Regenerator — must be defeated in a single turn

    • Splitter — duplicates if you leave it alive

    • Invisible (with glasses) — visible only for one turn

    • Black Hole

  • 6 new hybrid boss artworks (e.g., Mummy + Skeleton, etc.)

I’ll share an update as soon as the build is approved. Feedback on balance and difficulty is very welcome!

8 Likes

aaaaand done. We got some errors due to some engine bugs, but now everything is fixed and live on android and poki.

5 Likes

This is a really nice an polished game. Some inspiration from Archero and Punball. Love those games. I’ll be playing this a while I imagine.

I’m curious, how does Google play perform as compared to the much younger and much more casual audience on Poki? Did you change the game to be more accessible/easy to play for Poki in some way?

3 Likes

Thanks a lot!

On audience: funnily enough, most of the bug reports I got via Poki came straight to my email with photos and company emails - and they were from office workers, managers, even company directors. So I can’t claim I “know” Poki’s audience, but it’s definitely older than I expected.

Google Play vs Poki: GP for me is 100% organic so far, plus this thread (about ~100 clicks), so the numbers are tiny and look like the same kind of players -j ust fewer of them. Poki obviously has way more volume.

Did I make it “easier” for Poki? Not really. The core game is the same. I only polished the onboarding/UX (clearer first minutes, tiny early-game softness, mouse/keyboard niceties). After that, the difficulty curve is the same as GP. Also i have the shop ofcourse.

Marketing: my weak spot! I haven’t run paid UA before, so GP is still pure organic. I do plan to start a proper marketing push soon, just figuring out where to begin - tips welcome.

4 Likes

Poki’s audience must be different from what I thought, or maybe a bit broader at least, not just very young. That’s a good thing.

Google Play said < 5000, which I suppose is true with a small audience, but I expected it to be in the thousands, which is good for a new game without UA, in my experience.

In your first post you mention a test bot. So cool. How did your record the videos? Just the standard QuickTime on Mac or some screen capture on Windows, or did you actually record video from inside defold?

1 Like
import collectionsimport datetimeimport globimport osimport randomimport shutilimport signalimport subprocessimport sysimport tempfileimport timefrom typing import Optional

import cv2import numpy as npimport pyautoguiimport pygetwindow as gwimport win32api, win32confrom mss import mss



PRIMARY_BUTTON_IMAGES   = [‘revive_btn.png’, ‘claim_btn.png’]SECONDARY_BUTTON_IMAGES = [‘1.png’, ‘2.png’, ‘3.png’, ‘4.png’]SECONDARY_PROBABILITY   = 0.50PRIMARY_PAUSE_SEC = 10

WINDOW_TITLE            = ‘Bounce Ball’CLICK_INTERVAL_SEC      = 0.5



CAPTURE_W, CAPTURE_H    = 1200, 700   # зона, которую сравниваем (из окна)CHECK_INTERVAL_SEC      = 5           # раз в 5 с берём кадрFREEZE_CHECKS_LIMIT     = 10          # подряд «тихих» кадров → фризDIFF_THRESHOLD          = 2.0         # среднее |Δ| яркости 0-255



RECORD_FPS         = 24BUFFER_SECONDS     = 2500VIDEO_BITRATE      = ‘6M’VIDEO_CODEC        = ‘h264_nvenc’



sct = mss()run_dir: strscreenshot_index: int = 0



def move_and_click(x: int, y: int):win32api.SetCursorPos((x, y))time.sleep(0.05)win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0)time.sleep(0.05)win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,   x, y, 0, 0)time.sleep(0.05)

def locate_any(images, confidence=0.7):for image_path in images:try:location = pyautogui.locateCenterOnScreen(image_path,confidence=confidence,grayscale=True)except pyautogui.ImageNotFoundException:location = Noneif location is not None:return location, image_pathreturn None, None

def save_screenshot(region):global screenshot_indexscreenshot_index += 1path = os.path.join(run_dir, f"{screenshot_index}.png")pyautogui.screenshot(path, region=region)print(f"[INFO] Сохранён скриншот: {path}")



def start_ffmpeg(x, y, w, h):tmp_dir = tempfile.mkdtemp(prefix=“bb_ffmpeg_”)out_pattern = rf"{tmp_dir}\ring_%02d.mp4"  # будет ring_00.mp4

cmd = [
    "ffmpeg", "-loglevel", "error",  # или "warning", если нужно больше логов
    "-f", "gdigrab",
    "-framerate", str(RECORD_FPS),
    "-offset_x", str(x),
    "-offset_y", str(y),
    "-video_size", f"{w}x{h}",
    "-draw_mouse", "0",
    "-show_region", "0",
    "-i", "desktop",                # если хотите по title: "-i", f"title={WINDOW_TITLE}"
    "-an",                          # аудио отключено
    "-c:v", VIDEO_CODEC,
    "-preset", "p5" if VIDEO_CODEC.endswith("nvenc") else "ultrafast",
    "-b:v",  VIDEO_BITRATE,
    "-pix_fmt", "nv12" if VIDEO_CODEC.endswith("nvenc") else "yuv420p",
    "-f", "segment",
    "-segment_time", str(BUFFER_SECONDS),
    "-segment_wrap", "1",
    "-reset_timestamps", "1",
    out_pattern
]
print("[INFO] FFmpeg cmd:", " ".join(cmd))  # можно закомментировать
try:
    proc = subprocess.Popen(cmd, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
except FileNotFoundError:
    sys.exit("FFmpeg не найден. Добавьте ffmpeg.exe в PATH.")
return proc, tmp_dir, out_pattern

def stop_ffmpeg(proc: subprocess.Popen):if proc.poll() is None:proc.send_signal(signal.CTRL_BREAK_EVENT)try:proc.wait(timeout=3)except subprocess.TimeoutExpired:proc.kill()



def grab_frame(x, y, w, h):raw = sct.grab({“top”: y, “left”: x, “width”: w, “height”: h})gray = cv2.cvtColor(np.array(raw)[:, :, :3], cv2.COLOR_BGR2GRAY)return gray



def main_loop():global run_dir

run_dir = os.path.join(os.getcwd(),
                       datetime.datetime.now().strftime("run_%Y-%m-%d_%H-%M-%S"))
os.makedirs(run_dir, exist_ok=True)
print(f"[INFO] Каталог: {run_dir}")

windows = gw.getWindowsWithTitle(WINDOW_TITLE)
if not windows:
    sys.exit(f"[ERROR] Окно «{WINDOW_TITLE}» не найдено")
game_win = windows[0]
try:
    game_win.activate()
except Exception:
    pass

x0, y0, w, h = game_win.left, game_win.top, game_win.width, game_win.height

rx1 = x0 + int(w * 0.10); ry1 = y0 + int(h * 0.30)
rx2 = x0 + int(w * 0.70); ry2 = y0 + int(h * 0.70)

ff_proc, ff_dir, ff_pattern = start_ffmpeg(x0, y0, w, h)


last_frame = grab_frame(x0, y0, CAPTURE_W, CAPTURE_H)
static_hits = 0
next_check_t = time.monotonic() + CHECK_INTERVAL_SEC

print(f"[INFO] Запись {RECORD_FPS} fps. Детектор фриза: "
      f"{FREEZE_CHECKS_LIMIT}×<{DIFF_THRESHOLD} за {CHECK_INTERVAL_SEC}s")
time.sleep(3)

try:
    while True:
        now = time.monotonic()

        
        if now >= next_check_t:
            cur_frame = grab_frame(x0, y0, CAPTURE_W, CAPTURE_H)
            diff = cv2.absdiff(cur_frame, last_frame).mean()
            if diff < DIFF_THRESHOLD:
                static_hits += 1
                print(f"[FREEZE?] Δ={diff:.2f}  hit {static_hits}/{FREEZE_CHECKS_LIMIT}")
                if static_hits >= FREEZE_CHECKS_LIMIT:
                    print("[FREEZE] Обнаружен фриз, выходим из цикла.")
                    break
            else:
                static_hits = 0
            last_frame = cur_frame
            next_check_t += CHECK_INTERVAL_SEC

       
        location, img_name = locate_any(PRIMARY_BUTTON_IMAGES)
        if location:
            if os.path.basename(img_name) == 'claim_btn.png':
                save_screenshot((x0, y0, w, h))
            move_and_click(location.x, location.y)
            print(f"[CLICK] {img_name} @({location.x},{location.y})")
            time.sleep(PRIMARY_PAUSE_SEC)      # ← Пауза 5 с

        else:
            if random.random() < SECONDARY_PROBABILITY:
                paths = SECONDARY_BUTTON_IMAGES[:]
                random.shuffle(paths)
                sec_location, sec_img = locate_any(paths)
                if sec_location:
                    move_and_click(sec_location.x, sec_location.y)
                    print(f"[CLICK] secondary {sec_img} "
                          f"@({sec_location.x},{sec_location.y})")
                    time.sleep(CLICK_INTERVAL_SEC)
                    continue
            rand_x, rand_y = random.randint(rx1, rx2), random.randint(ry1, ry2)
            move_and_click(rand_x, rand_y)
            print(f"[CLICK] random ({rand_x},{rand_y})")

        time.sleep(CLICK_INTERVAL_SEC)

finally:
    stop_ffmpeg(ff_proc)
    try:
        last_seg = max(glob.glob(os.path.join(ff_dir, "*.mp4")),
                       key=os.path.getmtime)
        shutil.copy2(last_seg, os.path.join(run_dir, "freeze_capture.mp4"))
        print(f"[INFO] Сегмент скопирован → {run_dir}\\freeze_capture.mp4")
    except (ValueError, FileNotFoundError):
        print("[WARN] Видео-файл не найден.")
    shutil.rmtree(ff_dir, ignore_errors=True)



if name == “main”:main_loop()

something like that

2 Likes

Holy cow, that’s a tad more advanced than I thought. I can understand why you’re friendly with this bot. :slight_smile:

1 Like

I saw this game while strolling on Poki. It’s really cool, I especially like the progression curve, it’s smooth and engaging. Can I ask how you managed your balance and progression curve? I always struggle with those, so I’m curious. Thank you in advance!

1 Like

Hi, congrats with first message!

How block HP gets picked (per row)

  • Rows 1–3 are hand-holding:

    • Row 1: every block is 5.

    • Row 2: 5×j (depends on the slot index).

    • Row 3: 10×j.

  • Row 4+ uses the live “wall power”:
    For each block on the row we roll a random number in
    [currentPower, currentPower × powerMul] where
    currentPower = max(1, game.wall_power) and powerMul = random(x1..x2).
    Super-big numbers are stored in sci-notation internally, but it’s still “that HP”.

  • “Downspill” extra lines (when the board has lots of free vertical space) copy a random existing block and set its HP to the source ±10% (clamped to ≥1).

Power progression: you start with wall_power = d1, ball_power = d2.
Each round:
wall_power += floor((ball_power - wall_power)/8) → walls steadily catch up to balls without huge spikes.

What spawns on a row

  • Regular rows: 2–5 shapes out of cube / cilinder / trian, then some slots get randomly emptied to create gaps.

  • Special rows:

    • line: strips made of line, then line_half, then line_quater on subsequent lines.

    • move: one big moving block; its HP = sum of that row’s rolled HP values.

    • big: one chunky block (scale ~2.5), HP = sum of the row, with a chest flag.

    • boss: single boss piece (boss_split or black_hole) by schedule (see below).

Level types & how often

  • Boss: every 20 levels starting at 20, 40, 60, ….

  • Key levels: one per each 10-level decade (1–10, 11–20, …), chosen randomly in that decade, never colliding with bonus/boss. On that level one of the existing blocks turns into a rainbow cube with a key.

  • Bonus levels: one per decade too, non-overlapping with key/boss. The bonus “style” depends on your progress:

    • <10: always capsule.

    • 10–19: move or capsule (50/50).

    • ≥ rocket_line (30): equally among move / capsule / line / rocket.

    • Otherwise (before 30): roughly split among move / capsule / line.

  • Everything else:

    • 80% chance to be extra_ball (drops a chest on the row).

    • Otherwise it’s a normal row.

Blockers (the nasty tilted cylinders)

  • Only considered on normal/extra_ball levels and after hat_line = 60.

  • Chance on a given line: p = (line − (hat_line+1)) mod x3 → smoothly ramps 0% → … → x3%, then resets and repeats every x3+1 lines.
    After a trigger, hat_line is internally reset lower (to keep the cadence going).

Lottery & free reward logic

Here’s the short, practical view:

When do you see a lottery?

  • Baseline chance is ~30% after a level.

  • Safety net: if you miss the lottery 4 times in a row, the next one is guaranteed.

If there’s no lottery (static claim):

  • Roughly:

    • 30%: bomb ×1–3

    • 30%: ball(s) ×1–3

    • 30%: plusline ×1–3

    • 8%: a mixed set (bomb + ball + “lift”)

    • 2%: rare sets

If there is a lottery:

  • Multiple “modes” depending on progress (e.g., “epic” ≥ level 30 when you have no ticket; rarer ones ≥ level 50).

  • Payouts are 2–5 items in combos across bomb / plusline / magneto / maxball / ticket (weights depend on the exact mode; generally escalates with level).

Prize button (in-level, ad-based):

  • Always gives 2–5 items.

  • First it fills missing booster types in your inventory, then adds random goodies among magneto / maxball / bomb / plusline.

Achievements / dailies:

  • Feed into the same resource pool — frequent small drops of balls/boosters, sometimes tickets.

My list of readings for this game:

also i can recommend https://machinations.io/ for experiments. I hope this is what you`ve asked for

11 Likes

Thank you for sharing this post! I feel like this could be turned into a great blog post. Also, machniations.io is pretty damn cool!

2 Likes