You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
New app: Connect4 game
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "Connect 4",
|
||||
"publisher": "MicroPythonOS",
|
||||
"short_description": "Classic Connect 4 game",
|
||||
"long_description": "Play Connect 4 against the computer with three difficulty levels: Easy, Medium, and Hard. Drop colored discs and try to connect four in a row!",
|
||||
"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.connect4/icons/com.micropythonos.connect4_0.0.1_64x64.png",
|
||||
"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.connect4/mpks/com.micropythonos.connect4_0.0.1.mpk",
|
||||
"fullname": "com.micropythonos.connect4",
|
||||
"version": "0.0.1",
|
||||
"category": "games",
|
||||
"activities": [
|
||||
{
|
||||
"entrypoint": "assets/connect4.py",
|
||||
"classname": "Connect4",
|
||||
"intent_filters": [
|
||||
{
|
||||
"action": "main",
|
||||
"category": "launcher"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
import time
|
||||
import random
|
||||
|
||||
from mpos.apps import Activity
|
||||
import mpos.ui
|
||||
|
||||
try:
|
||||
import lvgl as lv
|
||||
except ImportError:
|
||||
pass # lv is already available as a global in MicroPython OS
|
||||
|
||||
|
||||
class Connect4(Activity):
|
||||
# Board dimensions
|
||||
COLS = 7
|
||||
ROWS = 6
|
||||
|
||||
# Screen layout
|
||||
SCREEN_WIDTH = 320
|
||||
SCREEN_HEIGHT = 240
|
||||
BOARD_TOP = 50
|
||||
CELL_SIZE = 35
|
||||
PIECE_RADIUS = 14
|
||||
|
||||
# Colors
|
||||
COLOR_EMPTY = 0x2C3E50
|
||||
COLOR_PLAYER = 0xE74C3C # Red
|
||||
COLOR_COMPUTER = 0xF1C40F # Yellow
|
||||
COLOR_BOARD = 0x3498DB # Blue
|
||||
COLOR_HIGHLIGHT = 0x2ECC71 # Green
|
||||
COLOR_WIN = 0x9B59B6 # Purple
|
||||
|
||||
# Game state
|
||||
EMPTY = 0
|
||||
PLAYER = 1
|
||||
COMPUTER = 2
|
||||
|
||||
# Difficulty levels
|
||||
DIFFICULTY_EASY = 0
|
||||
DIFFICULTY_MEDIUM = 1
|
||||
DIFFICULTY_HARD = 2
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.board = [[self.EMPTY for _ in range(self.COLS)] for _ in range(self.ROWS)]
|
||||
self.difficulty = self.DIFFICULTY_EASY
|
||||
self.game_over = False
|
||||
self.winner = None
|
||||
self.winning_positions = []
|
||||
self.current_player = self.PLAYER
|
||||
self.animating = False
|
||||
|
||||
# UI elements
|
||||
self.screen = None
|
||||
self.pieces = [] # 2D array of LVGL objects
|
||||
self.column_buttons = []
|
||||
self.status_label = None
|
||||
self.difficulty_label = None
|
||||
self.last_time = 0
|
||||
|
||||
def onCreate(self):
|
||||
self.screen = lv.obj()
|
||||
self.screen.set_style_bg_color(lv.color_hex(0x34495E), 0)
|
||||
self.screen.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
|
||||
self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE)
|
||||
|
||||
# Title
|
||||
title = lv.label(self.screen)
|
||||
title.set_text("Connect 4")
|
||||
title.set_style_text_font(lv.font_montserrat_20, 0)
|
||||
title.set_style_text_color(lv.color_hex(0xFFFFFF), 0)
|
||||
title.set_pos(10, 5)
|
||||
|
||||
# Difficulty selector
|
||||
difficulty_cont = lv.obj(self.screen)
|
||||
difficulty_cont.set_size(150, 30)
|
||||
difficulty_cont.set_pos(165, 5)
|
||||
difficulty_cont.set_style_bg_color(lv.color_hex(0x2C3E50), 0)
|
||||
difficulty_cont.set_style_border_width(1, 0)
|
||||
difficulty_cont.set_style_border_color(lv.color_hex(0xFFFFFF), 0)
|
||||
difficulty_cont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
|
||||
difficulty_cont.add_flag(lv.obj.FLAG.CLICKABLE)
|
||||
difficulty_cont.add_event_cb(self.cycle_difficulty, lv.EVENT.CLICKED, None)
|
||||
|
||||
self.difficulty_label = lv.label(difficulty_cont)
|
||||
self.difficulty_label.set_text("Difficulty: Easy")
|
||||
self.difficulty_label.set_style_text_font(lv.font_montserrat_12, 0)
|
||||
self.difficulty_label.set_style_text_color(lv.color_hex(0xFFFFFF), 0)
|
||||
self.difficulty_label.center()
|
||||
|
||||
# Status label
|
||||
self.status_label = lv.label(self.screen)
|
||||
self.status_label.set_text("Your turn!")
|
||||
self.status_label.set_style_text_font(lv.font_montserrat_14, 0)
|
||||
self.status_label.set_style_text_color(lv.color_hex(0xFFFFFF), 0)
|
||||
self.status_label.set_pos(10, 32)
|
||||
|
||||
# Create board background
|
||||
board_bg = lv.obj(self.screen)
|
||||
board_bg.set_size(self.COLS * self.CELL_SIZE + 10, self.ROWS * self.CELL_SIZE + 10)
|
||||
board_bg.set_pos(
|
||||
(self.SCREEN_WIDTH - self.COLS * self.CELL_SIZE) // 2 - 5,
|
||||
self.BOARD_TOP - 5
|
||||
)
|
||||
board_bg.set_style_bg_color(lv.color_hex(self.COLOR_BOARD), 0)
|
||||
board_bg.set_style_radius(8, 0)
|
||||
board_bg.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
|
||||
|
||||
# Create pieces (visual representation)
|
||||
board_x = (self.SCREEN_WIDTH - self.COLS * self.CELL_SIZE) // 2
|
||||
for row in range(self.ROWS):
|
||||
piece_row = []
|
||||
for col in range(self.COLS):
|
||||
piece = lv.obj(self.screen)
|
||||
piece.set_size(self.PIECE_RADIUS * 2, self.PIECE_RADIUS * 2)
|
||||
x = board_x + col * self.CELL_SIZE + (self.CELL_SIZE - self.PIECE_RADIUS * 2) // 2
|
||||
y = self.BOARD_TOP + row * self.CELL_SIZE + (self.CELL_SIZE - self.PIECE_RADIUS * 2) // 2
|
||||
piece.set_pos(x, y)
|
||||
piece.set_style_radius(lv.RADIUS_CIRCLE, 0)
|
||||
piece.set_style_bg_color(lv.color_hex(self.COLOR_EMPTY), 0)
|
||||
piece.set_style_border_width(1, 0)
|
||||
piece.set_style_border_color(lv.color_hex(0x1C2833), 0)
|
||||
piece.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
|
||||
piece_row.append(piece)
|
||||
self.pieces.append(piece_row)
|
||||
|
||||
# Create column buttons (invisible clickable areas)
|
||||
for col in range(self.COLS):
|
||||
btn = lv.obj(self.screen)
|
||||
btn.set_size(self.CELL_SIZE, self.ROWS * self.CELL_SIZE)
|
||||
x = board_x + col * self.CELL_SIZE
|
||||
btn.set_pos(x, self.BOARD_TOP)
|
||||
btn.set_style_bg_opa(0, 0) # Transparent
|
||||
btn.set_style_border_width(0, 0)
|
||||
btn.add_flag(lv.obj.FLAG.CLICKABLE)
|
||||
btn.add_event_cb(lambda e, c=col: self.on_column_click(c), lv.EVENT.CLICKED, None)
|
||||
self.column_buttons.append(btn)
|
||||
|
||||
# New Game button
|
||||
new_game_btn = lv.button(self.screen)
|
||||
new_game_btn.set_size(100, 30)
|
||||
new_game_btn.align(lv.ALIGN.BOTTOM_MID, 0, -5)
|
||||
new_game_btn.add_event_cb(lambda e: self.new_game(), lv.EVENT.CLICKED, None)
|
||||
new_game_label = lv.label(new_game_btn)
|
||||
new_game_label.set_text("New Game")
|
||||
new_game_label.center()
|
||||
|
||||
self.setContentView(self.screen)
|
||||
|
||||
def onResume(self, screen):
|
||||
self.last_time = time.ticks_ms()
|
||||
|
||||
def cycle_difficulty(self, event):
|
||||
if self.animating:
|
||||
return
|
||||
self.difficulty = (self.difficulty + 1) % 3
|
||||
difficulty_names = ["Easy", "Medium", "Hard"]
|
||||
self.difficulty_label.set_text(f"Difficulty: {difficulty_names[self.difficulty]}")
|
||||
self.difficulty_label.center()
|
||||
|
||||
def on_column_click(self, col):
|
||||
if self.game_over or self.animating or self.current_player != self.PLAYER:
|
||||
return
|
||||
|
||||
if self.drop_piece(col, self.PLAYER):
|
||||
self.animate_drop(col)
|
||||
|
||||
def drop_piece(self, col, player):
|
||||
"""Try to drop a piece in the given column. Returns True if successful."""
|
||||
# Find the lowest empty row in this column
|
||||
for row in range(self.ROWS - 1, -1, -1):
|
||||
if self.board[row][col] == self.EMPTY:
|
||||
self.board[row][col] = player
|
||||
return True
|
||||
return False
|
||||
|
||||
def animate_drop(self, col):
|
||||
"""Animate the piece dropping and then check for win/computer move"""
|
||||
self.animating = True
|
||||
|
||||
# Find which row the piece landed in
|
||||
row = -1
|
||||
player = self.EMPTY
|
||||
for r in range(self.ROWS):
|
||||
if self.board[r][col] != self.EMPTY:
|
||||
row = r
|
||||
player = self.board[r][col]
|
||||
break
|
||||
|
||||
if row == -1:
|
||||
self.animating = False
|
||||
return
|
||||
|
||||
# Update the visual
|
||||
color = self.COLOR_PLAYER if player == self.PLAYER else self.COLOR_COMPUTER
|
||||
self.pieces[row][col].set_style_bg_color(lv.color_hex(color), 0)
|
||||
|
||||
# Check for win or tie
|
||||
if self.check_win(row, col):
|
||||
self.game_over = True
|
||||
self.winner = player
|
||||
self.highlight_winning_pieces()
|
||||
winner_text = "You win!" if player == self.PLAYER else "Computer wins!"
|
||||
self.status_label.set_text(winner_text)
|
||||
self.animating = False
|
||||
return
|
||||
|
||||
if self.is_board_full():
|
||||
self.game_over = True
|
||||
self.status_label.set_text("It's a tie!")
|
||||
self.animating = False
|
||||
return
|
||||
|
||||
# Switch player
|
||||
self.current_player = self.COMPUTER if player == self.PLAYER else self.PLAYER
|
||||
|
||||
if self.current_player == self.COMPUTER:
|
||||
self.status_label.set_text("Computer thinking...")
|
||||
# Delay computer move slightly for better UX
|
||||
lv.timer_create(lambda t: self.computer_move(), 500, None).set_repeat_count(1)
|
||||
else:
|
||||
self.status_label.set_text("Your turn!")
|
||||
self.animating = False
|
||||
|
||||
def computer_move(self):
|
||||
"""Make a computer move based on difficulty"""
|
||||
if self.game_over:
|
||||
self.animating = False
|
||||
return
|
||||
|
||||
if self.difficulty == self.DIFFICULTY_EASY:
|
||||
col = self.get_random_move()
|
||||
elif self.difficulty == self.DIFFICULTY_MEDIUM:
|
||||
col = self.get_medium_move()
|
||||
else: # HARD
|
||||
col = self.get_hard_move()
|
||||
|
||||
if col is not None and self.drop_piece(col, self.COMPUTER):
|
||||
self.animate_drop(col)
|
||||
else:
|
||||
self.animating = False
|
||||
|
||||
def get_random_move(self):
|
||||
"""Easy: Random valid column"""
|
||||
valid_cols = [c for c in range(self.COLS) if self.board[0][c] == self.EMPTY]
|
||||
return random.choice(valid_cols) if valid_cols else None
|
||||
|
||||
def get_medium_move(self):
|
||||
"""Medium: Block player wins, try to win, otherwise random"""
|
||||
# First, try to win
|
||||
for col in range(self.COLS):
|
||||
if self.is_valid_move(col):
|
||||
row = self.get_next_row(col)
|
||||
self.board[row][col] = self.COMPUTER
|
||||
if self.check_win(row, col):
|
||||
self.board[row][col] = self.EMPTY
|
||||
return col
|
||||
self.board[row][col] = self.EMPTY
|
||||
|
||||
# Second, block player from winning
|
||||
for col in range(self.COLS):
|
||||
if self.is_valid_move(col):
|
||||
row = self.get_next_row(col)
|
||||
self.board[row][col] = self.PLAYER
|
||||
if self.check_win(row, col):
|
||||
self.board[row][col] = self.EMPTY
|
||||
return col
|
||||
self.board[row][col] = self.EMPTY
|
||||
|
||||
# Otherwise, random
|
||||
return self.get_random_move()
|
||||
|
||||
def get_hard_move(self):
|
||||
"""Hard: Minimax algorithm"""
|
||||
best_score = -float('inf')
|
||||
best_col = None
|
||||
|
||||
for col in range(self.COLS):
|
||||
if self.is_valid_move(col):
|
||||
row = self.get_next_row(col)
|
||||
self.board[row][col] = self.COMPUTER
|
||||
score = self.minimax(3, False, -float('inf'), float('inf'))
|
||||
self.board[row][col] = self.EMPTY
|
||||
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_col = col
|
||||
|
||||
return best_col if best_col is not None else self.get_random_move()
|
||||
|
||||
def minimax(self, depth, is_maximizing, alpha, beta):
|
||||
"""Minimax with alpha-beta pruning"""
|
||||
# Check terminal states
|
||||
for row in range(self.ROWS):
|
||||
for col in range(self.COLS):
|
||||
if self.board[row][col] != self.EMPTY:
|
||||
if self.check_win(row, col):
|
||||
if self.board[row][col] == self.COMPUTER:
|
||||
return 1000
|
||||
else:
|
||||
return -1000
|
||||
|
||||
if self.is_board_full():
|
||||
return 0
|
||||
|
||||
if depth == 0:
|
||||
return self.evaluate_board()
|
||||
|
||||
if is_maximizing:
|
||||
max_score = -float('inf')
|
||||
for col in range(self.COLS):
|
||||
if self.is_valid_move(col):
|
||||
row = self.get_next_row(col)
|
||||
self.board[row][col] = self.COMPUTER
|
||||
score = self.minimax(depth - 1, False, alpha, beta)
|
||||
self.board[row][col] = self.EMPTY
|
||||
max_score = max(max_score, score)
|
||||
alpha = max(alpha, score)
|
||||
if beta <= alpha:
|
||||
break
|
||||
return max_score
|
||||
else:
|
||||
min_score = float('inf')
|
||||
for col in range(self.COLS):
|
||||
if self.is_valid_move(col):
|
||||
row = self.get_next_row(col)
|
||||
self.board[row][col] = self.PLAYER
|
||||
score = self.minimax(depth - 1, True, alpha, beta)
|
||||
self.board[row][col] = self.EMPTY
|
||||
min_score = min(min_score, score)
|
||||
beta = min(beta, score)
|
||||
if beta <= alpha:
|
||||
break
|
||||
return min_score
|
||||
|
||||
def evaluate_board(self):
|
||||
"""Heuristic evaluation of board position"""
|
||||
score = 0
|
||||
|
||||
# Evaluate all possible windows of 4
|
||||
for row in range(self.ROWS):
|
||||
for col in range(self.COLS):
|
||||
if col <= self.COLS - 4:
|
||||
window = [self.board[row][col + i] for i in range(4)]
|
||||
score += self.evaluate_window(window)
|
||||
|
||||
if row <= self.ROWS - 4:
|
||||
window = [self.board[row + i][col] for i in range(4)]
|
||||
score += self.evaluate_window(window)
|
||||
|
||||
if row <= self.ROWS - 4 and col <= self.COLS - 4:
|
||||
window = [self.board[row + i][col + i] for i in range(4)]
|
||||
score += self.evaluate_window(window)
|
||||
|
||||
if row >= 3 and col <= self.COLS - 4:
|
||||
window = [self.board[row - i][col + i] for i in range(4)]
|
||||
score += self.evaluate_window(window)
|
||||
|
||||
return score
|
||||
|
||||
def evaluate_window(self, window):
|
||||
"""Evaluate a window of 4 positions"""
|
||||
score = 0
|
||||
computer_count = window.count(self.COMPUTER)
|
||||
player_count = window.count(self.PLAYER)
|
||||
empty_count = window.count(self.EMPTY)
|
||||
|
||||
if computer_count == 3 and empty_count == 1:
|
||||
score += 5
|
||||
elif computer_count == 2 and empty_count == 2:
|
||||
score += 2
|
||||
|
||||
if player_count == 3 and empty_count == 1:
|
||||
score -= 4
|
||||
|
||||
return score
|
||||
|
||||
def is_valid_move(self, col):
|
||||
"""Check if a column has space"""
|
||||
return self.board[0][col] == self.EMPTY
|
||||
|
||||
def get_next_row(self, col):
|
||||
"""Get the row where a piece would land in this column"""
|
||||
for row in range(self.ROWS - 1, -1, -1):
|
||||
if self.board[row][col] == self.EMPTY:
|
||||
return row
|
||||
return -1
|
||||
|
||||
def check_win(self, row, col):
|
||||
"""Check if the piece at (row, col) creates a winning connection"""
|
||||
player = self.board[row][col]
|
||||
if player == self.EMPTY:
|
||||
return False
|
||||
|
||||
# Check horizontal
|
||||
positions = self.check_direction(row, col, 0, 1)
|
||||
if len(positions) >= 4:
|
||||
self.winning_positions = positions
|
||||
return True
|
||||
|
||||
# Check vertical
|
||||
positions = self.check_direction(row, col, 1, 0)
|
||||
if len(positions) >= 4:
|
||||
self.winning_positions = positions
|
||||
return True
|
||||
|
||||
# Check diagonal (down-right)
|
||||
positions = self.check_direction(row, col, 1, 1)
|
||||
if len(positions) >= 4:
|
||||
self.winning_positions = positions
|
||||
return True
|
||||
|
||||
# Check diagonal (down-left)
|
||||
positions = self.check_direction(row, col, 1, -1)
|
||||
if len(positions) >= 4:
|
||||
self.winning_positions = positions
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def check_direction(self, row, col, dr, dc):
|
||||
"""Count consecutive pieces in a direction (both ways)"""
|
||||
player = self.board[row][col]
|
||||
positions = [(row, col)]
|
||||
|
||||
# Check positive direction
|
||||
r, c = row + dr, col + dc
|
||||
while 0 <= r < self.ROWS and 0 <= c < self.COLS and self.board[r][c] == player:
|
||||
positions.append((r, c))
|
||||
r += dr
|
||||
c += dc
|
||||
|
||||
# Check negative direction
|
||||
r, c = row - dr, col - dc
|
||||
while 0 <= r < self.ROWS and 0 <= c < self.COLS and self.board[r][c] == player:
|
||||
positions.append((r, c))
|
||||
r -= dr
|
||||
c -= dc
|
||||
|
||||
return positions
|
||||
|
||||
def highlight_winning_pieces(self):
|
||||
"""Highlight the winning pieces"""
|
||||
for row, col in self.winning_positions:
|
||||
self.pieces[row][col].set_style_bg_color(lv.color_hex(self.COLOR_WIN), 0)
|
||||
self.pieces[row][col].set_style_border_width(3, 0)
|
||||
self.pieces[row][col].set_style_border_color(lv.color_hex(0xFFFFFF), 0)
|
||||
|
||||
def is_board_full(self):
|
||||
"""Check if the board is full"""
|
||||
return all(self.board[0][col] != self.EMPTY for col in range(self.COLS))
|
||||
|
||||
def new_game(self):
|
||||
"""Reset the game"""
|
||||
self.board = [[self.EMPTY for _ in range(self.COLS)] for _ in range(self.ROWS)]
|
||||
self.game_over = False
|
||||
self.winner = None
|
||||
self.winning_positions = []
|
||||
self.current_player = self.PLAYER
|
||||
self.animating = False
|
||||
self.status_label.set_text("Your turn!")
|
||||
|
||||
# Reset visual pieces
|
||||
for row in range(self.ROWS):
|
||||
for col in range(self.COLS):
|
||||
self.pieces[row][col].set_style_bg_color(lv.color_hex(self.COLOR_EMPTY), 0)
|
||||
self.pieces[row][col].set_style_border_width(1, 0)
|
||||
self.pieces[row][col].set_style_border_color(lv.color_hex(0x1C2833), 0)
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
# Create 64x64 icon
|
||||
img = Image.new('RGB', (64, 64), color=(52, 73, 94))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Draw blue board
|
||||
draw.rectangle([4, 4, 60, 60], fill=(52, 152, 219))
|
||||
|
||||
# Draw grid of circles with a pattern
|
||||
colors = [(231, 76, 60), (241, 196, 15), (44, 62, 80)] # Red, Yellow, Empty
|
||||
|
||||
pattern = [
|
||||
[2, 2, 2, 2, 2, 2, 2],
|
||||
[2, 2, 2, 2, 2, 2, 2],
|
||||
[2, 2, 0, 0, 2, 2, 2],
|
||||
[2, 0, 0, 1, 2, 2, 2],
|
||||
[0, 1, 0, 1, 0, 2, 2],
|
||||
[0, 1, 1, 0, 0, 1, 2],
|
||||
]
|
||||
|
||||
cell_size = 8
|
||||
start_x = 8
|
||||
start_y = 8
|
||||
|
||||
for row in range(6):
|
||||
for col in range(7):
|
||||
x = start_x + col * cell_size
|
||||
y = start_y + row * cell_size
|
||||
draw.ellipse([x, y, x + 6, y + 6], fill=colors[pattern[row][col]])
|
||||
|
||||
img.save('res/mipmap-mdpi/icon_64x64.png')
|
||||
print("Icon created: res/mipmap-mdpi/icon_64x64.png")
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 440 B |
Reference in New Issue
Block a user