From a996081ea51e9b4bec010b28486276333f05fcd8 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Tue, 3 Feb 2026 00:53:46 +0100 Subject: [PATCH] columns: simple falling-blocks game This is a start of falling-blocks game. More improvements are possible (and some are described in the sources), but this is already playable. You can try to beat score of 120 :-). --- .../META-INF/MANIFEST.JSON | 25 ++ .../apps/cz.ucw.pavel.columns/assets/main.py | 337 ++++++++++++++++++ .../res/mipmap-mdpi/icon_64x64.png | Bin 0 -> 8820 bytes 3 files changed, 362 insertions(+) create mode 100644 internal_filesystem/apps/cz.ucw.pavel.columns/META-INF/MANIFEST.JSON create mode 100644 internal_filesystem/apps/cz.ucw.pavel.columns/assets/main.py create mode 100644 internal_filesystem/apps/cz.ucw.pavel.columns/res/mipmap-mdpi/icon_64x64.png diff --git a/internal_filesystem/apps/cz.ucw.pavel.columns/META-INF/MANIFEST.JSON b/internal_filesystem/apps/cz.ucw.pavel.columns/META-INF/MANIFEST.JSON new file mode 100644 index 00000000..51d15601 --- /dev/null +++ b/internal_filesystem/apps/cz.ucw.pavel.columns/META-INF/MANIFEST.JSON @@ -0,0 +1,25 @@ +{ +"name": "Columns", +"publisher": "Pavel Machek", +"short_description": "Falling columns game", +"long_description": "Blocks of 3 colors are falling. Align the colors to make blocks di\ +sappear.", +"icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.columns/icons/cz.ucw.pavel.columns_0.0.1_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.columns/mpks/cz.ucw.pavel.columns_0.0.1.mpk", +"fullname": "cz.ucw.pavel.columns", +"version": "0.0.1", +"category": "utilities", +"activities": [ + { + "entrypoint": "assets/main.py", + "classname": "Main", + "intent_filters": [ + { + "action": "main", + "category": "launcher" + } + ] + } + ] +} + diff --git a/internal_filesystem/apps/cz.ucw.pavel.columns/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.columns/assets/main.py new file mode 100644 index 00000000..4bbc58b5 --- /dev/null +++ b/internal_filesystem/apps/cz.ucw.pavel.columns/assets/main.py @@ -0,0 +1,337 @@ +import time +import random + +""" +Columns -- falling columns game + +Possible TODOs: + +should blink explosions +explodes while moving? +/ in bottom left part may not explode + +smooth moving? +music? +some kind of game over? + +more contrast colors? +different shapes? + +""" + +from mpos import Activity + +try: + import lvgl as lv +except ImportError: + pass + +class Main(Activity): + + COLS = 6 + ROWS = 12 + + COLORS = [ + 0xE74C3C, # red + 0xF1C40F, # yellow + 0x2ECC71, # green + 0x3498DB, # blue + 0x9B59B6, # purple + ] + + EMPTY = -1 + + FALL_INTERVAL = 1000 # ms + # I can do 120 in this config :-). + + def __init__(self): + super().__init__() + self.board = [[self.EMPTY for _ in range(self.COLS)] for _ in range(self.ROWS)] + self.cells = [] + + self.active_col = self.COLS // 2 + self.active_row = -3 + self.active_colors = [] + + self.timer = None + self.animating = False + + # --------------------------------------------------------------------- + + def onCreate(self): + self.screen = lv.obj() + self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE) + + vert = 60 + horiz = 60 + font = lv.font_montserrat_20 + + score = lv.label(self.screen) + score.align(lv.ALIGN.TOP_LEFT, 5, 25) + score.set_text("Score") + score.set_style_text_font(font, 0) + self.lb_score = score + + btn_left = lv.button(self.screen) + btn_left.set_size(horiz, vert) + btn_left.align(lv.ALIGN.BOTTOM_LEFT, 5, -10-vert) + btn_left.add_event_cb(lambda e: self.move(-1), lv.EVENT.CLICKED, None) + lc = lv.label(btn_left) + lc.set_style_text_font(font, 0) + lc.set_text("<") + lc.center() + + btn_right = lv.button(self.screen) + btn_right.set_size(horiz, vert) + btn_right.align(lv.ALIGN.BOTTOM_RIGHT, -5, -10-vert) + btn_right.add_event_cb(lambda e: self.move(1), lv.EVENT.CLICKED, None) + lc = lv.label(btn_right) + lc.set_style_text_font(font, 0) + lc.set_text(">") + lc.center() + + btn_rotate = lv.button(self.screen) + btn_rotate.set_size(horiz, vert) + btn_rotate.align(lv.ALIGN.BOTTOM_RIGHT, -5, -15-vert-vert) + btn_rotate.add_event_cb(lambda e: self.rotate(), lv.EVENT.CLICKED, None) + lc = lv.label(btn_rotate) + lc.set_style_text_font(font, 0) + lc.set_text("R") + lc.center() + + btn_down = lv.button(self.screen) + btn_down.set_size(horiz, vert) + btn_down.align(lv.ALIGN.BOTTOM_LEFT, 5, -5) + btn_down.add_event_cb(lambda e: self.tick(0), lv.EVENT.CLICKED, None) + lc = lv.label(btn_down) + lc.set_style_text_font(font, 0) + lc.set_text("v") + lc.center() + + d = lv.display_get_default() + self.SCREEN_WIDTH = d.get_horizontal_resolution() + self.SCREEN_HEIGHT = d.get_vertical_resolution() + + self.CELL = min( + self.SCREEN_WIDTH // (self.COLS + 1), + self.SCREEN_HEIGHT // (self.ROWS + 1) + ) + + board_x = (self.SCREEN_WIDTH - self.CELL * self.COLS) // 2 + board_y = (self.SCREEN_HEIGHT - self.CELL * self.ROWS) // 2 + + for r in range(self.ROWS): + row = [] + for c in range(self.COLS): + o = lv.obj(self.screen) + o.set_size(self.CELL - 2, self.CELL - 2) + o.set_pos( + board_x + c * self.CELL + 1, + board_y + r * self.CELL + 1 + ) + o.set_style_radius(4, 0) + o.set_style_bg_color(lv.color_hex(0x1C2833), 0) + o.set_style_border_width(1, 0) + row.append(o) + self.cells.append(row) + + # Make screen focusable for keyboard input + focusgroup = lv.group_get_default() + if focusgroup: + focusgroup.add_obj(self.screen) + + #self.screen.add_event_cb(self.on_touch, lv.EVENT.CLICKED, None) + self.screen.add_event_cb(self.on_key, lv.EVENT.KEY, None) + + self.setContentView(self.screen) + + self.new_game() + self.spawn_piece() + + + def new_game(self): + self.score = 0 + # --------------------------------------------------------------------- + + def onResume(self, screen): + self.timer = lv.timer_create(self.tick, self.FALL_INTERVAL, None) + + def onPause(self, screen): + if self.timer: + self.timer.delete() + self.timer = None + + # --------------------------------------------------------------------- + + def spawn_piece(self): + self.active_col = self.COLS // 2 + self.active_row = -3 + self.active_colors = [random.randrange(len(self.COLORS)) for _ in range(3)] + + def tick(self, t): + if self.can_fall(): + self.active_row += 1 + else: + self.lock_piece() + self.clear_matches() + self.spawn_piece() + + self.redraw() + + # --------------------------------------------------------------------- + + def can_fall(self): + for i in range(3): + r = self.active_row + i + 1 + c = self.active_col + if r >= self.ROWS: + return False + if r >= 0 and self.board[r][c] != self.EMPTY: + return False + return True + + def lock_piece(self): + for i in range(3): + r = self.active_row + i + if r >= 0: + self.board[r][self.active_col] = self.active_colors[i] + + # --------------------------------------------------------------------- + + def clear_matches(self): + to_clear = set() + score = 0 + + for r in range(self.ROWS): + for c in range(self.COLS): + color = self.board[r][c] + if color == self.EMPTY: + continue + + # horizontal + if c <= self.COLS - 3: + if all(self.board[r][c + i] == color for i in range(3)): + for i in range(3): + to_clear.add((r, c + i)) + score += 1 + + # vertical + if r <= self.ROWS - 3: + if all(self.board[r + i][c] == color for i in range(3)): + for i in range(3): + to_clear.add((r + i, c)) + score += 1 + + # diagonal \ + if r <= self.ROWS - 3 and c <= self.COLS - 3: + if all(self.board[r + i][c + i] == color for i in range(3)): + for i in range(3): + to_clear.add((r + i, c + i)) + score += 1 + + # diagonal / + if r <= self.ROWS - 3 and c > 2: + if all(self.board[r + i][c - i] == color for i in range(3)): + for i in range(3): + to_clear.add((r + i, c - i)) + score += 1 + + if not to_clear: + return + + print("Score: ", score) + self.score += score + self.lb_score.set_text("Score\n%d" % self.score) + for r, c in to_clear: + self.board[r][c] = self.EMPTY + + self.redraw() + time.sleep(.5) + self.apply_gravity() + self.redraw() + time.sleep(.5) + self.clear_matches() + self.redraw() + + def apply_gravity(self): + for c in range(self.COLS): + stack = [self.board[r][c] for r in range(self.ROWS) if self.board[r][c] != self.EMPTY] + for r in range(self.ROWS): + self.board[r][c] = self.EMPTY + for i, v in enumerate(reversed(stack)): + self.board[self.ROWS - 1 - i][c] = v + + # --------------------------------------------------------------------- + + def redraw(self): + # draw board + for r in range(self.ROWS): + for c in range(self.COLS): + v = self.board[r][c] + if v == self.EMPTY: + self.cells[r][c].set_style_bg_color(lv.color_hex(0x1C2833), 0) + else: + self.cells[r][c].set_style_bg_color( + lv.color_hex(self.COLORS[v]), 0 + ) + + # draw active piece + for i in range(3): + r = self.active_row + i + if r >= 0 and r < self.ROWS: + self.cells[r][self.active_col].set_style_bg_color( + lv.color_hex(self.COLORS[self.active_colors[i]]), 0 + ) + + # --------------------------------------------------------------------- + + def on_touch(self, e): + return + print("Touch event") + p = lv.indev_get_act().get_point() + x = p.x + + if x < self.SCREEN_WIDTH // 3: + self.move(-1) + elif x > self.SCREEN_WIDTH * 2 // 3: + self.move(1) + else: + self.rotate() + + def on_key(self, event): + """Handle keyboard input""" + print("Keyboard event") + key = event.get_key() + if key == ord("a"): + self.move(-1) + return + if key == ord("w"): + self.rotate() + return + if key == ord("d"): + self.move(1) + return + if key == ord("s"): + self.tick(0) + return + + #if key == lv.KEY.ENTER or key == lv.KEY.UP or key == ord("A") or key == ord("a"): + print(f"on_key: unhandled key {key}") + + def move(self, dx): + nc = self.active_col + dx + if not(0 <= nc < self.COLS): + return + + for i in range(3): + r = self.active_row + i + if self.board[r][nc] != self.EMPTY: + return + + self.active_col = nc + self.redraw() + + def rotate(self): + self.active_colors = self.active_colors[-1:] + self.active_colors[:-1] + self.redraw() + diff --git a/internal_filesystem/apps/cz.ucw.pavel.columns/res/mipmap-mdpi/icon_64x64.png b/internal_filesystem/apps/cz.ucw.pavel.columns/res/mipmap-mdpi/icon_64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..49812a67e41c1cc2a86d6e37a8d318be137fb5a2 GIT binary patch literal 8820 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 I>8}0%1%q%D-v9sr literal 0 HcmV?d00001