From 9b78e05bb52aeae994115d42609eb0785a509cda Mon Sep 17 00:00:00 2001 From: cardonator Date: Thu, 29 Oct 2026 08:45:41 -0700 Subject: [PATCH 1/3] Add Mona Jezzball badge app Mona-themed JezzBall for the Universe 2026 badge with animated, direction-facing Mona sprites, successive levels with more Monas, directional slicer marker, and cuts that stop at walls but cross captured territory. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- badge/apps/mona_jezzball/README.md | 7 + badge/apps/mona_jezzball/__init__.py | 449 ++++++++++++++++++ badge/apps/mona_jezzball/assets/mona.png | Bin 0 -> 2612 bytes badge/apps/mona_jezzball/assets/mona_left.png | Bin 0 -> 3085 bytes badge/apps/mona_jezzball/icon.png | Bin 0 -> 725 bytes 5 files changed, 456 insertions(+) create mode 100644 badge/apps/mona_jezzball/README.md create mode 100644 badge/apps/mona_jezzball/__init__.py create mode 100644 badge/apps/mona_jezzball/assets/mona.png create mode 100644 badge/apps/mona_jezzball/assets/mona_left.png create mode 100755 badge/apps/mona_jezzball/icon.png diff --git a/badge/apps/mona_jezzball/README.md b/badge/apps/mona_jezzball/README.md new file mode 100644 index 00000000..f9929e3a --- /dev/null +++ b/badge/apps/mona_jezzball/README.md @@ -0,0 +1,7 @@ +# Mona Jezzball + +Claim cozy corners by carving the playfield while keeping Mona and her friends safe. + +## Usage + +Move the cursor with the arrows, press Select to start a cut, and press Back to switch between horizontal and vertical cuts; the slicer marker shows the selected direction. The cursor can move anywhere, and a cut started in claimed territory travels through it into open space; once in open space, each end stops independently at the next wall, claimed area, or edge. Claim at least 75% of the field before losing all three paws. Mona's animated sprite always faces left or right, following her horizontal movement. Clear a level to meet more Monas, up to five, and face faster movement. Press Select to begin, continue, or replay. No extra hardware or network access is required. diff --git a/badge/apps/mona_jezzball/__init__.py b/badge/apps/mona_jezzball/__init__.py new file mode 100644 index 00000000..1c4fd5e6 --- /dev/null +++ b/badge/apps/mona_jezzball/__init__.py @@ -0,0 +1,449 @@ +import os +import sys + +APP_DIR = "/system/apps/mona_jezzball" +os.chdir(APP_DIR) +sys.path.insert(0, APP_DIR) + +from badgeware import State + +badge.mode(HIRES | VSYNC) +screen.font = font.nope + +GRID_W = 36 +GRID_H = 20 +CELL = 8 +BOARD_X = 16 +BOARD_Y = 42 +BOARD_W = GRID_W * CELL +BOARD_H = GRID_H * CELL +BALL_R = 6 +SPRITE_HALF = 12 +GOAL = 75 +MAX_MONAS = 5 +CUT_SPEED = 28 +CURSOR_REPEAT = 0.085 + +INK = color.rgb(20, 15, 41) +PANEL = color.rgb(38, 25, 67) +FIELD = color.rgb(20, 13, 37) +MINT = color.rgb(112, 255, 215) +PINK = color.rgb(255, 124, 201) +LAVENDER = color.rgb(171, 135, 255) +WHITE = color.rgb(255, 240, 255) +GOLD = color.rgb(255, 220, 112) + +mona_sprites = image.load("assets/mona.png").spritesheet(7, 2) +mona_left_sprites = image.load("assets/mona_left.png").spritesheet(7, 2) +best = {"percent": 0, "level": 1} +State.load("mona_jezzball", best) + +state = "title" +grid = [] +walls = [] +monas = [] +level = 1 +cursor_x = GRID_W // 2 +cursor_y = GRID_H // 2 +cut_axis = "horizontal" +cut = None +lives = 3 +percent = 0 +cuts = 0 +repeat_timer = 0 + + +def new_game(): + global lives, level + + lives = 3 + level = 1 + start_level(level) + + +def start_level(next_level): + global state, grid, walls, monas, cursor_x, cursor_y, cut_axis, cut + global percent, cuts, level + + level = next_level + grid = [[False for _ in range(GRID_W)] for _ in range(GRID_H)] + walls = [[False for _ in range(GRID_W)] for _ in range(GRID_H)] + speed = 1 + min(level - 1, 8) * 0.1 + starts = ( + (0.43, 0.53, 49, 34), + (0.68, 0.35, -38, 46), + (0.30, 0.72, 54, -37), + (0.74, 0.70, -43, -52), + (0.49, 0.27, 61, 42), + ) + monas = [] + for x, y, vx, vy in starts[:min(level, MAX_MONAS)]: + monas.append({ + "x": BOARD_X + BOARD_W * x, + "y": BOARD_Y + BOARD_H * y, + "vx": vx * speed, + "vy": vy * speed, + }) + cursor_x = GRID_W // 2 + cursor_y = GRID_H // 2 + cut_axis = "horizontal" + cut = None + percent = 0 + cuts = 0 + state = "playing" + + +def blocked_at(x, y): + gx = int((x - BOARD_X) // CELL) + gy = int((y - BOARD_Y) // CELL) + if gx < 0 or gx >= GRID_W or gy < 0 or gy >= GRID_H: + return True + return grid[gy][gx] + + +def update_ball(mona, dt): + next_x = mona["x"] + mona["vx"] * dt + min_x = BOARD_X + SPRITE_HALF + max_x = BOARD_X + BOARD_W - SPRITE_HALF + if next_x < min_x: + mona["x"] = min_x + mona["vx"] = abs(mona["vx"]) + elif next_x > max_x: + mona["x"] = max_x + mona["vx"] = -abs(mona["vx"]) + elif blocked_at(next_x + (BALL_R if mona["vx"] > 0 else -BALL_R), mona["y"]): + mona["vx"] = -mona["vx"] + else: + mona["x"] = next_x + + next_y = mona["y"] + mona["vy"] * dt + min_y = BOARD_Y + SPRITE_HALF + max_y = BOARD_Y + BOARD_H - SPRITE_HALF + if next_y < min_y: + mona["y"] = min_y + mona["vy"] = abs(mona["vy"]) + elif next_y > max_y: + mona["y"] = max_y + mona["vy"] = -abs(mona["vy"]) + elif blocked_at(mona["x"], next_y + (BALL_R if mona["vy"] > 0 else -BALL_R)): + mona["vy"] = -mona["vy"] + else: + mona["y"] = next_y + + +def move_cursor(): + global cursor_x, cursor_y, repeat_timer + + direction = None + if badge.held(BUTTON_LEFT): + direction = (-1, 0) + elif badge.held(BUTTON_RIGHT): + direction = (1, 0) + elif badge.held(BUTTON_UP): + direction = (0, -1) + elif badge.held(BUTTON_DOWN): + direction = (0, 1) + + if direction is None: + repeat_timer = 0 + return + + repeat_timer -= badge.ticks_delta / 1000 + if repeat_timer > 0: + return + + repeat_timer = CURSOR_REPEAT + cursor_x = max(0, min(GRID_W - 1, cursor_x + direction[0])) + cursor_y = max(0, min(GRID_H - 1, cursor_y + direction[1])) + + +def cell_blocked(x, y): + return grid[y][x] or walls[y][x] + + +def cut_extent(dx, dy): + # Pass through captured cells until open space is reached, then stop at the next blocker. + seen_open = not cell_blocked(cursor_x, cursor_y) + step = 1 + while True: + x = cursor_x + dx * step + y = cursor_y + dy * step + if x < 0 or x >= GRID_W or y < 0 or y >= GRID_H: + return step - 0.5 if seen_open else 0 + if cell_blocked(x, y): + if seen_open: + return step - 0.5 + else: + seen_open = True + step += 1 + + +def start_cut(): + global cut + + if cut_axis == "horizontal": + negative = cut_extent(-1, 0) + positive = cut_extent(1, 0) + else: + negative = cut_extent(0, -1) + positive = cut_extent(0, 1) + if negative <= 0 and positive <= 0: + return + cut = { + "axis": cut_axis, + "negative": negative, + "positive": positive, + "negative_length": 0, + "positive_length": 0, + } + + +def cut_hits_mona(negative_length, positive_length, mona): + center_x = BOARD_X + cursor_x * CELL + CELL / 2 + center_y = BOARD_Y + cursor_y * CELL + CELL / 2 + if cut["axis"] == "horizontal": + along = mona["x"] - center_x + return ( + abs(mona["y"] - center_y) <= BALL_R + 2 + and -negative_length * CELL - BALL_R <= along <= positive_length * CELL + BALL_R + ) + along = mona["y"] - center_y + return ( + abs(mona["x"] - center_x) <= BALL_R + 2 + and -negative_length * CELL - BALL_R <= along <= positive_length * CELL + BALL_R + ) + + +def fill_regions(): + global percent + + visited = [[False for _ in range(GRID_W)] for _ in range(GRID_H)] + queue = [] + for mona in monas: + start_x = max(0, min(GRID_W - 1, int((mona["x"] - BOARD_X) // CELL))) + start_y = max(0, min(GRID_H - 1, int((mona["y"] - BOARD_Y) // CELL))) + if not grid[start_y][start_x] and not walls[start_y][start_x] and not visited[start_y][start_x]: + queue.append(start_y * GRID_W + start_x) + visited[start_y][start_x] = True + + index = 0 + while index < len(queue): + cell = queue[index] + index += 1 + x = cell % GRID_W + y = cell // GRID_W + for nx, ny in ((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)): + if ( + 0 <= nx < GRID_W + and 0 <= ny < GRID_H + and not grid[ny][nx] + and not walls[ny][nx] + and not visited[ny][nx] + ): + visited[ny][nx] = True + queue.append(ny * GRID_W + nx) + + filled = 0 + for y in range(GRID_H): + for x in range(GRID_W): + if not visited[y][x] and not walls[y][x]: + grid[y][x] = True + if grid[y][x] or walls[y][x]: + filled += 1 + percent = int(filled * 100 / (GRID_W * GRID_H)) + + +def finish_cut(): + global cut, cuts, state, best + + negative_cells = int(cut["negative"]) + positive_cells = int(cut["positive"]) + if cut["axis"] == "horizontal": + for x in range(cursor_x - negative_cells, cursor_x + positive_cells + 1): + walls[cursor_y][x] = True + grid[cursor_y][x] = True + else: + for y in range(cursor_y - negative_cells, cursor_y + positive_cells + 1): + walls[y][cursor_x] = True + grid[y][cursor_x] = True + cut = None + cuts += 1 + fill_regions() + if percent >= GOAL: + state = "level_clear" + if level > best["level"]: + best["level"] = level + State.save("mona_jezzball", best) + + +def update_cut(dt): + global cut, lives, state + + cut["negative_length"] = min( + cut["negative"], + cut["negative_length"] + CUT_SPEED * dt, + ) + cut["positive_length"] = min( + cut["positive"], + cut["positive_length"] + CUT_SPEED * dt, + ) + for mona in monas: + if cut_hits_mona(cut["negative_length"], cut["positive_length"], mona): + cut = None + lives -= 1 + if lives <= 0: + state = "lost" + return + if ( + cut["negative_length"] >= cut["negative"] + and cut["positive_length"] >= cut["positive"] + ): + finish_cut() + + +def draw_board(): + screen.pen = PANEL + screen.shape(shape.rounded_rectangle(BOARD_X - 3, BOARD_Y - 3, BOARD_W + 6, BOARD_H + 6, 5)) + screen.pen = FIELD + screen.shape(shape.rectangle(BOARD_X, BOARD_Y, BOARD_W, BOARD_H)) + + for y in range(GRID_H): + for x in range(GRID_W): + if grid[y][x]: + screen.pen = LAVENDER if (x + y) % 4 else PINK + screen.shape(shape.rectangle(BOARD_X + x * CELL, BOARD_Y + y * CELL, CELL, CELL)) + + if cut is not None: + center_x = BOARD_X + cursor_x * CELL + CELL / 2 + center_y = BOARD_Y + cursor_y * CELL + CELL / 2 + negative_distance = cut["negative_length"] * CELL + positive_distance = cut["positive_length"] * CELL + screen.pen = MINT + if cut["axis"] == "horizontal": + screen.line( + center_x - negative_distance, + center_y, + center_x + positive_distance, + center_y, + ) + else: + screen.line( + center_x, + center_y - negative_distance, + center_x, + center_y + positive_distance, + ) + else: + x = BOARD_X + cursor_x * CELL + y = BOARD_Y + cursor_y * CELL + center_x = x + CELL / 2 + center_y = y + CELL / 2 + screen.pen = GOLD + screen.shape(shape.rectangle(x + 1, y + 1, CELL - 2, CELL - 2)) + screen.pen = FIELD + screen.shape(shape.rectangle(x + 3, y + 3, CELL - 6, CELL - 6)) + screen.pen = MINT + if cut_axis == "horizontal": + screen.line(center_x - 10, center_y, center_x - 4, center_y) + screen.line(center_x + 4, center_y, center_x + 10, center_y) + else: + screen.line(center_x, center_y - 10, center_x, center_y - 4) + screen.line(center_x, center_y + 4, center_x, center_y + 10) + screen.pen = GOLD + screen.shape(shape.rectangle(center_x - 3, center_y - 3, 6, 6)) + screen.pen = FIELD + screen.shape(shape.rectangle(center_x - 1, center_y - 1, 2, 2)) + + for index, mona in enumerate(monas): + frame = int(badge.ticks / 115 + index * 2) % 7 + sprites = mona_left_sprites if mona["vx"] < 0 else mona_sprites + sprite = sprites.sprite(frame, 0) + screen.blit(sprite, vec2(mona["x"] - SPRITE_HALF, mona["y"] - SPRITE_HALF)) + + +def draw_hud(): + screen.pen = WHITE + screen.text("MONA JEZZBALL", 16, 8) + screen.pen = MINT + screen.text("LV %d %d%%/%d%%" % (level, percent, GOAL), 166, 8) + screen.pen = PINK + screen.text("PAWS %d" % lives, 272, 8) + + screen.pen = PANEL + screen.shape(shape.rounded_rectangle(16, 210, 288, 9, 4)) + if percent: + screen.pen = MINT + screen.shape(shape.rounded_rectangle(16, 210, max(2, int(288 * percent / 100)), 9, 4)) + screen.pen = WHITE + if cut is None: + screen.text("ARROWS MOVE SELECT CUT BACK: %s" % cut_axis.upper(), 16, 224) + else: + screen.text("MONA IS ON THE MOVE!", 16, 224) + + +def draw_title(): + screen.pen = INK + screen.shape(shape.rectangle(0, 0, screen.width, screen.height)) + screen.pen = LAVENDER + screen.shape(shape.circle(160, 74, 40)) + frame = int(badge.ticks / 115) % 7 + screen.blit(mona_sprites.sprite(frame, 0), vec2(148, 62)) + centered_text("MONA JEZZBALL", 19, WHITE) + centered_text("Snip the space. Keep Mona safe!", 122, MINT) + centered_text("Carve off empty corners to claim them.", 146, WHITE) + centered_text("Reach 75% before your paws run out.", 164, WHITE) + if int(badge.ticks / 500) % 2: + centered_text("PRESS SELECT TO PLAY", 194, GOLD) + + +def centered_text(text, y, pen): + width, _ = screen.measure_text(text) + screen.pen = pen + screen.text(text, (screen.width - width) / 2, y) + + +def draw_end(): + screen.pen = INK + screen.shape(shape.rectangle(0, 0, screen.width, screen.height)) + cleared = state == "level_clear" + centered_text("LEVEL CLEARED!" if cleared else "OH NO, MONA!", 64, MINT if cleared else PINK) + centered_text("Level %d Territory: %d%%" % (level, percent), 105, WHITE) + centered_text("Monas: %d Best level: %d" % (len(monas), best["level"]), 128, WHITE) + if int(badge.ticks / 500) % 2: + prompt = "SELECT: NEXT LEVEL" if cleared else "SELECT: TRY AGAIN" + centered_text(prompt, 171, GOLD) + + +def update(): + global cut_axis + + dt = min(0.05, max(0, badge.ticks_delta / 1000)) + if state == "title": + draw_title() + if badge.pressed(BUTTON_SELECT): + new_game() + return + + if state == "playing": + if cut is None: + move_cursor() + if badge.pressed(BUTTON_BACK): + cut_axis = "vertical" if cut_axis == "horizontal" else "horizontal" + if badge.pressed(BUTTON_SELECT): + start_cut() + for mona in monas: + update_ball(mona, dt) + if cut is not None: + update_cut(dt) + draw_board() + draw_hud() + return + + draw_end() + if badge.pressed(BUTTON_SELECT) and state == "level_clear": + start_level(level + 1) + elif badge.pressed(BUTTON_SELECT): + new_game() + + +run(update) diff --git a/badge/apps/mona_jezzball/assets/mona.png b/badge/apps/mona_jezzball/assets/mona.png new file mode 100644 index 0000000000000000000000000000000000000000..8a927590c023290935bd23cdf6a4eb4b55892983 GIT binary patch literal 2612 zcmbVOeK?bQA733s(h*51X^nX)du6fZ-I$e))+$ML_Htv`rfu2~!bz5hc`M0Fs8o0; zp)fh8P{~82mg2-|DZO2eti;Z(()0Y$b*|@n?(4em-|zSReLmm!?{(c$Un*&d_9|@% z1hRzeMWli2dGJMREd;-ZV=;N)VkGkL6K&%Mio^^70C8pWnE;f`Wds2-@!3qULP$m$>;W@(IUYEn59JV8DCys)q zgu4U59Iq$=uq}#8XGMjw@NAf~6VyRM02Ocn5d$jWhVg_1i6iWbUIMtE6{BF#FA!0v zBkZeFeiUD*J6`}m@kk7Ug+^ncc6el<4bBz_WZ9vup;$E528F?((0Bv}OF-iYI2-7< z2L>7xumcG+qUSer;K>mdEE0tiP$;ojj1=1-`GO!629L+1&{z}}ivSS_VI)t)kRW(M z(>V=9K*$ns!bKcD4?3%n!Q@AX9ATidU!&lLQz+jR^Mv1W1!fH;VT7YFNHmJeoek{^ zS}39c|8e7oXdyi^96-?kAwNRE0!tETI!6Z6_s@oAfuJ`89{~rf3nPrkXGL%Uo`_6z zgn>6mHiu1M*xK9Sa2s(591Dj>*fIlI2z(&J9)ZWQZS3sWSX(q3J*VeKe4>XZ(ap`q z2Ji0ShQWBE-Q3;n-7sz*7&ok|od@22j!WhVMGPJbn6u3RZGYop|B*{@7XSnH<&Nt3xoOLP%I9KgIZD;EDmp0Vl`W(uR#+50p|d~_7w2B&@X8waDL#z{}%pN zKKuVo9}08|HQOuy)jM+{upwu+--iJ_d>OqN|z|c_Hbp- z>~-MuG`F8Kr@IE2O0+RcB{xoLUYugoTVW`(euSkY@A1$xmQY8O=Z>2_Ih9(xr?T_@ zTkaZp-TGnm^(kTTt(;GrVjX7kI^>;3%1Kcp_na7gw09r#b6R1WvVC9Tl%d8=PpACJ zM?!6Gl*(8YU}AC>h$lJMOnlt7S;%-O~QD_N7qf{rrlt8ON=qtXueusq!Ve^dsaxop_qo>NY)~KwY{Dv`D%R09su?wMLDQ}sCy)WrNWoV@p{?=(_o8C(^Ihxi* z;>qwbYX2hpLTPo&(B&p*{?o2plI;1r+E!b0Uxd?A=_$y=wbHeeSgLFFbf|%X)>yR* z0&}&Bw})SgDUhVwG87}svP09Omo*1h-N|~kO|~t&Woq{jNmjTqOug#xy~uXMb+_9q zm|Q@qWm%a(+PZ<_YC0SjQaFaAg;}pOIHs@X zz&d#Eo!&|K&Hj6)FIAyk^Rz4;c?y;O`cm?zPQ#!Z8#<#@MQf^(V)D{-_a0i!>ZK=` zTC9L5u_PbvR_j4}x@Ow02ck&H#m<`WT8{;0>FxaYXt*{Pf$B@L6lJ95xSCqv z&hU1)-wSYcU}lgfyixe~)x%vbG}OoamXP(A%HtOCxXH5ZzU4*K)a89IR&K4d6Gxmq zpmPn5A5s$5U5#)GfAHZnV1RQVkGV+wxE` zNUziS<(V0y9&u=oBYxS!AqO*?(eaOOuPbR5cdc$JZzZm#^MKCGV&AHUr-ma3+s*B& zUs_R^y(sfX%pf*4>8_~hneUpIU{!D(y@R9vq*e83CyXB#FuJN?>+e9d}2 z!L`ThZ=T;2OZ80Ieazey_i< zV&y^D-RuqJOJfwHo7i+leU$@sIZa7S3o*z~Swx7J=uqq5kl;(2X7@~n>68c|q$@jRol8GS+PV&|&X ziK^2ME6!FNKC)YyuRg6FxT-!G3rscLN*bJ>^FU6Y@3C|ddLejUl;}y_vZa&|W2}k7 zBM@sy7fa0vdas-&rP?n!Qlj3XOyZ1|IH}M2wg(eD~a>hNZ=xsUZqtS6X#F%S(QEXW>w`IeF+A zJi!;%y6H`#xs@e}mLeE_J7R)=_xsTYuYTX!v3#cKuc2MKb}JM7_c*{|<`;9icVi{T zievg^-O(0`sk$R)cp9U9@$(ndp0|YSJZWo)q;Ut9MKm<=-wH?xg4UXY~u~^vBvx3P%B&6buyK;}}-h;XNX5Lh2D*W>C^>430rP*UoYBk~Hp#n@tquGS_+1%2qmoo+L!@B&3 zA0rUDyQ+%sOE4>1brh|AA5?u0-z5qwp4FFaAjXZ^U!6v^K+w4d VZtN?9D`x-W$R1Q;rQ0tFe**;NX{`VN literal 0 HcmV?d00001 diff --git a/badge/apps/mona_jezzball/assets/mona_left.png b/badge/apps/mona_jezzball/assets/mona_left.png new file mode 100644 index 0000000000000000000000000000000000000000..1b97c160cc14aa6180fd1df173e425a61c516f7e GIT binary patch literal 3085 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 literal 0 HcmV?d00001 diff --git a/badge/apps/mona_jezzball/icon.png b/badge/apps/mona_jezzball/icon.png new file mode 100755 index 0000000000000000000000000000000000000000..a680f3ee098714928605ad61d3ff854ddb264892 GIT binary patch literal 725 zcmV;`0xJE9P)NI6Q7{g#z{3&dGiZV}j5gO3s< zd=h)g4>JP<1e@eb^MJY$UDH)m3)$w~u#KdNvNch^jPQPQyqLXV^S6`OxUaWxRsUB2 z#_sD4$n^CFlg71k9a8-P4fg?zm(?4#Sr09;k%et9>n)0~K6KcTM;vJq+)m>IPB(#t z15o6MV5K|OL&uaxcPB8yr8&Y5xfyv1PC1AvjqU>&_tl@dJrE5tCmuqcOMpP)Mx%!S zCQ(pcmQxcu~b@K4D%t4oRU3xK{#4t+hg86^2&KKsdj3=Bi!A}VHXlNgIn zk=LQ4uQrIlYIs3h0RX3+B1s0<+GLP3!R5XDWI+sm0i^Q%WxZuJbjc=<*0r&*sKW*> z+5;S7o3Vth4PZ{DxC>zH-#)xSgqY=RW@0yu}U64Z>Ibw2;%Vb5BMOy<&)cy z-SN!FZXcP~m#X|j00|v_ywKNM6ve@9-jQOA9fLbGz;pY*+{}sOgP1=V_(h$2{00000NkvXX Hu0mjf`-n^_ literal 0 HcmV?d00001 From 80ec3569a219786aa31ccd5da792640aed52e69a Mon Sep 17 00:00:00 2001 From: cardonator Date: Thu, 29 Oct 2026 12:09:32 -0700 Subject: [PATCH 2/3] Rename Mona Jezzball to Mona Slice Avoid using a third-party game name per the PR template's IP guidance, and credit the reused flappy_mona sprite assets in the README. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- badge/apps/{mona_jezzball => mona_slice}/README.md | 6 +++++- .../apps/{mona_jezzball => mona_slice}/__init__.py | 10 +++++----- .../{mona_jezzball => mona_slice}/assets/mona.png | Bin .../assets/mona_left.png | Bin badge/apps/{mona_jezzball => mona_slice}/icon.png | Bin 5 files changed, 10 insertions(+), 6 deletions(-) rename badge/apps/{mona_jezzball => mona_slice}/README.md (82%) rename badge/apps/{mona_jezzball => mona_slice}/__init__.py (98%) rename badge/apps/{mona_jezzball => mona_slice}/assets/mona.png (100%) rename badge/apps/{mona_jezzball => mona_slice}/assets/mona_left.png (100%) rename badge/apps/{mona_jezzball => mona_slice}/icon.png (100%) diff --git a/badge/apps/mona_jezzball/README.md b/badge/apps/mona_slice/README.md similarity index 82% rename from badge/apps/mona_jezzball/README.md rename to badge/apps/mona_slice/README.md index f9929e3a..01b69edb 100644 --- a/badge/apps/mona_jezzball/README.md +++ b/badge/apps/mona_slice/README.md @@ -1,7 +1,11 @@ -# Mona Jezzball +# Mona Slice Claim cozy corners by carving the playfield while keeping Mona and her friends safe. ## Usage Move the cursor with the arrows, press Select to start a cut, and press Back to switch between horizontal and vertical cuts; the slicer marker shows the selected direction. The cursor can move anywhere, and a cut started in claimed territory travels through it into open space; once in open space, each end stops independently at the next wall, claimed area, or edge. Claim at least 75% of the field before losing all three paws. Mona's animated sprite always faces left or right, following her horizontal movement. Clear a level to meet more Monas, up to five, and face faster movement. Press Select to begin, continue, or replay. No extra hardware or network access is required. + +## Credits + +Mona sprite sheet and icon are reused from this repository's `flappy_mona` app; `assets/mona_left.png` is a horizontally mirrored copy. diff --git a/badge/apps/mona_jezzball/__init__.py b/badge/apps/mona_slice/__init__.py similarity index 98% rename from badge/apps/mona_jezzball/__init__.py rename to badge/apps/mona_slice/__init__.py index 1c4fd5e6..f7083a76 100644 --- a/badge/apps/mona_jezzball/__init__.py +++ b/badge/apps/mona_slice/__init__.py @@ -1,7 +1,7 @@ import os import sys -APP_DIR = "/system/apps/mona_jezzball" +APP_DIR = "/system/apps/mona_slice" os.chdir(APP_DIR) sys.path.insert(0, APP_DIR) @@ -36,7 +36,7 @@ mona_sprites = image.load("assets/mona.png").spritesheet(7, 2) mona_left_sprites = image.load("assets/mona_left.png").spritesheet(7, 2) best = {"percent": 0, "level": 1} -State.load("mona_jezzball", best) +State.load("mona_slice", best) state = "title" grid = [] @@ -273,7 +273,7 @@ def finish_cut(): state = "level_clear" if level > best["level"]: best["level"] = level - State.save("mona_jezzball", best) + State.save("mona_slice", best) def update_cut(dt): @@ -363,7 +363,7 @@ def draw_board(): def draw_hud(): screen.pen = WHITE - screen.text("MONA JEZZBALL", 16, 8) + screen.text("MONA SLICE", 16, 8) screen.pen = MINT screen.text("LV %d %d%%/%d%%" % (level, percent, GOAL), 166, 8) screen.pen = PINK @@ -388,7 +388,7 @@ def draw_title(): screen.shape(shape.circle(160, 74, 40)) frame = int(badge.ticks / 115) % 7 screen.blit(mona_sprites.sprite(frame, 0), vec2(148, 62)) - centered_text("MONA JEZZBALL", 19, WHITE) + centered_text("MONA SLICE", 19, WHITE) centered_text("Snip the space. Keep Mona safe!", 122, MINT) centered_text("Carve off empty corners to claim them.", 146, WHITE) centered_text("Reach 75% before your paws run out.", 164, WHITE) diff --git a/badge/apps/mona_jezzball/assets/mona.png b/badge/apps/mona_slice/assets/mona.png similarity index 100% rename from badge/apps/mona_jezzball/assets/mona.png rename to badge/apps/mona_slice/assets/mona.png diff --git a/badge/apps/mona_jezzball/assets/mona_left.png b/badge/apps/mona_slice/assets/mona_left.png similarity index 100% rename from badge/apps/mona_jezzball/assets/mona_left.png rename to badge/apps/mona_slice/assets/mona_left.png diff --git a/badge/apps/mona_jezzball/icon.png b/badge/apps/mona_slice/icon.png similarity index 100% rename from badge/apps/mona_jezzball/icon.png rename to badge/apps/mona_slice/icon.png From 1fd00a2ed3e103469b1a0d5a02201f753b8394cb Mon Sep 17 00:00:00 2001 From: cardonator Date: Thu, 29 Oct 2026 12:43:19 -0700 Subject: [PATCH 3/3] Fix frame-rate-independent controls and Mona movement Preserve cursor repeat time across frames so held input remains consistent at varying frame rates. Substep Mona movement so narrow compartments do not trap her or cause her facing to flicker. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- badge/apps/mona_slice/__init__.py | 87 ++++++++++++++++++------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/badge/apps/mona_slice/__init__.py b/badge/apps/mona_slice/__init__.py index f7083a76..1596b09b 100644 --- a/badge/apps/mona_slice/__init__.py +++ b/badge/apps/mona_slice/__init__.py @@ -102,38 +102,47 @@ def blocked_at(x, y): def update_ball(mona, dt): - next_x = mona["x"] + mona["vx"] * dt - min_x = BOARD_X + SPRITE_HALF - max_x = BOARD_X + BOARD_W - SPRITE_HALF - if next_x < min_x: - mona["x"] = min_x - mona["vx"] = abs(mona["vx"]) - elif next_x > max_x: - mona["x"] = max_x - mona["vx"] = -abs(mona["vx"]) - elif blocked_at(next_x + (BALL_R if mona["vx"] > 0 else -BALL_R), mona["y"]): - mona["vx"] = -mona["vx"] - else: - mona["x"] = next_x - - next_y = mona["y"] + mona["vy"] * dt - min_y = BOARD_Y + SPRITE_HALF - max_y = BOARD_Y + BOARD_H - SPRITE_HALF - if next_y < min_y: - mona["y"] = min_y - mona["vy"] = abs(mona["vy"]) - elif next_y > max_y: - mona["y"] = max_y - mona["vy"] = -abs(mona["vy"]) - elif blocked_at(mona["x"], next_y + (BALL_R if mona["vy"] > 0 else -BALL_R)): - mona["vy"] = -mona["vy"] - else: - mona["y"] = next_y + for axis in ("x", "y"): + position_key = axis + velocity_key = "v" + axis + if axis == "x": + minimum = BOARD_X + SPRITE_HALF + maximum = BOARD_X + BOARD_W - SPRITE_HALF + else: + minimum = BOARD_Y + SPRITE_HALF + maximum = BOARD_Y + BOARD_H - SPRITE_HALF + + remaining = abs(mona[velocity_key] * dt) + while remaining > 0: + distance = min(1, remaining) + direction = 1 if mona[velocity_key] > 0 else -1 + next_position = mona[position_key] + direction * distance + if axis == "x": + blocked = blocked_at(next_position + BALL_R * direction, mona["y"]) + else: + blocked = blocked_at(mona["x"], next_position + BALL_R * direction) + + if next_position < minimum or next_position > maximum: + mona[position_key] = max(minimum, min(maximum, next_position)) + mona[velocity_key] = -mona[velocity_key] + elif blocked: + reverse_position = mona[position_key] - direction * distance + if axis == "x": + reverse_blocked = blocked_at( + reverse_position - BALL_R * direction, mona["y"] + ) + else: + reverse_blocked = blocked_at( + mona["x"], reverse_position - BALL_R * direction + ) + if not reverse_blocked: + mona[velocity_key] = -mona[velocity_key] + else: + mona[position_key] = next_position + remaining -= distance def move_cursor(): - global cursor_x, cursor_y, repeat_timer - direction = None if badge.held(BUTTON_LEFT): direction = (-1, 0) @@ -145,16 +154,24 @@ def move_cursor(): direction = (0, 1) if direction is None: - repeat_timer = 0 + advance_cursor(None, 0) return - repeat_timer -= badge.ticks_delta / 1000 - if repeat_timer > 0: + advance_cursor(direction, badge.ticks_delta / 1000) + + +def advance_cursor(direction, elapsed): + global cursor_x, cursor_y, repeat_timer + + if direction is None: + repeat_timer = 0 return - repeat_timer = CURSOR_REPEAT - cursor_x = max(0, min(GRID_W - 1, cursor_x + direction[0])) - cursor_y = max(0, min(GRID_H - 1, cursor_y + direction[1])) + repeat_timer -= elapsed + while repeat_timer <= 0: + cursor_x = max(0, min(GRID_W - 1, cursor_x + direction[0])) + cursor_y = max(0, min(GRID_H - 1, cursor_y + direction[1])) + repeat_timer += CURSOR_REPEAT def cell_blocked(x, y):