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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 literal 0 HcmV?d00001