/run.py Secret
Created
March 27, 2025 15:44
Minecraft with Pygame by Gemini 2.5
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import pygame | |
import sys | |
import random | |
import math | |
import array # For generating sound buffers | |
# --- Constants --- | |
SCREEN_WIDTH = 960 | |
SCREEN_HEIGHT = 720 | |
BLOCK_SIZE = 32 | |
CAMERA_SPEED = 6 # Pixels per frame | |
WORLD_WIDTH_BLOCKS = 150 # World width in number of blocks | |
WORLD_HEIGHT_BLOCKS = 80 # World height in number of blocks | |
# Colors | |
WHITE = (255, 255, 255) | |
BLACK = (0, 0, 0) | |
SKY_BLUE = (135, 206, 250) | |
# Block Base Colors (Used for generating textures) | |
GRASS_BASE = (34, 177, 76) | |
DIRT_BASE = (139, 69, 19) | |
STONE_BASE = (128, 128, 128) | |
HIGHLIGHT_COLOR = (255, 255, 0, 100) # Yellow, semi-transparent alpha | |
# Block Types (ID -> Name) | |
BLOCK_TYPES = { | |
1: "Grass", | |
2: "Dirt", | |
3: "Stone", | |
} | |
# --- Pygame Setup --- | |
pygame.init() | |
pygame.mixer.init(frequency=22050, size=-16, channels=2, buffer=512) # Standard init | |
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) | |
pygame.display.set_caption("PyCraft 2D - Place[LMB]/Break[RMB] - Select[1,2,3] - Move[WASD]") | |
clock = pygame.time.Clock() | |
try: | |
font = pygame.font.Font(None, 30) # Default Pygame font | |
except Exception as e: | |
print(f"Warning: Could not load default font. Text UI might not work. Error: {e}") | |
font = None # Handle missing font gracefully | |
# --- Asset Generation --- | |
block_textures = {} | |
highlight_surface = None | |
def generate_block_texture(base_color, detail_variation=25, detail_density=0.2): | |
"""Generates a simple noisy texture for a block.""" | |
texture = pygame.Surface((BLOCK_SIZE, BLOCK_SIZE)) | |
texture.fill(base_color) | |
num_details = int((BLOCK_SIZE * BLOCK_SIZE) * detail_density) | |
for _ in range(num_details): | |
# Add slightly darker/lighter pixels | |
mod_r = random.randint(-detail_variation, detail_variation) | |
mod_g = random.randint(-detail_variation, detail_variation) | |
mod_b = random.randint(-detail_variation, detail_variation) | |
detail_color = ( | |
max(0, min(255, base_color[0] + mod_r)), | |
max(0, min(255, base_color[1] + mod_g)), | |
max(0, min(255, base_color[2] + mod_b)) | |
) | |
px = random.randint(0, BLOCK_SIZE - 1) | |
py = random.randint(0, BLOCK_SIZE - 1) | |
texture.set_at((px, py), detail_color) | |
# Add a simple border | |
pygame.draw.rect(texture, BLACK, (0, 0, BLOCK_SIZE, BLOCK_SIZE), 1) | |
return texture | |
print("Generating textures...") | |
try: | |
block_textures[1] = generate_block_texture(GRASS_BASE, detail_variation=20, detail_density=0.3) | |
# Add a greener top to grass | |
pygame.draw.rect(block_textures[1], (50, 200, 80), (0, 0, BLOCK_SIZE, BLOCK_SIZE // 5)) | |
pygame.draw.rect(block_textures[1], BLACK, (0, 0, BLOCK_SIZE, BLOCK_SIZE), 1) # Re-apply border | |
block_textures[2] = generate_block_texture(DIRT_BASE, detail_variation=15, detail_density=0.25) | |
block_textures[3] = generate_block_texture(STONE_BASE, detail_variation=30, detail_density=0.4) | |
# Highlight Surface | |
highlight_surface = pygame.Surface((BLOCK_SIZE, BLOCK_SIZE), pygame.SRCALPHA) # SRCALPHA allows transparency | |
highlight_surface.fill(HIGHLIGHT_COLOR) | |
print("Textures generated successfully.") | |
except Exception as e: | |
print(f"Error generating textures: {e}. Block rendering might fail.") | |
# Basic fallback - without textures, drawing needs adjustment, but let's try anyway | |
block_textures = {} # Clear partially generated textures on error | |
# --- Sound Generation --- | |
sound_place = None | |
sound_break = None | |
def generate_beep(freq, duration_ms, vol=0.1): | |
"""Generates a pygame Sound object with a sine wave.""" | |
try: | |
sample_rate = pygame.mixer.get_init()[0] | |
num_channels = pygame.mixer.get_init()[1] | |
bits = abs(pygame.mixer.get_init()[2]) | |
max_amp = (2**(bits - 1)) - 1 | |
num_samples = int(sample_rate * duration_ms / 1000.0) | |
if bits == 16: | |
pack_format = "<h" # Little-endian signed short | |
elif bits == 8: | |
pack_format = "<b" # Little-endian signed char | |
else: | |
print(f"Unsupported bit depth for sound generation: {bits}") | |
return None # Cannot generate sound | |
buf = array.array(pack_format, (0 for _ in range(num_samples * num_channels))) | |
for i in range(num_samples): | |
val = int(vol * max_amp * math.sin(2 * math.pi * freq * i / sample_rate)) | |
# Write to all channels (e.g., stereo) | |
for c in range(num_channels): | |
buf[i * num_channels + c] = val | |
sound = pygame.mixer.Sound(buffer=buf) | |
return sound | |
except Exception as e: | |
print(f"Error generating sound: {e}") | |
return None | |
print("Generating sounds...") | |
try: | |
# Generate simple beep sounds | |
sound_place = generate_beep(880, 80, vol=0.08) # Higher pitch, short duration | |
sound_break = generate_beep(440, 120, vol=0.1) # Lower pitch, slightly longer | |
if sound_place and sound_break: | |
print("Sounds generated successfully.") | |
else: | |
print("Failed to generate some or all sounds.") | |
except Exception as e: | |
print(f"Could not initialize or generate sounds: {e}. Sound disabled.") | |
sound_place = None | |
sound_break = None | |
# --- Game State --- | |
world_data = {} # Dictionary: (col, row) -> block_type_id | |
camera_x = (WORLD_WIDTH_BLOCKS * BLOCK_SIZE) // 2 - SCREEN_WIDTH // 2 # Start camera in the middle horizontally | |
camera_y = (WORLD_HEIGHT_BLOCKS * BLOCK_SIZE) // 3 - SCREEN_HEIGHT // 2 # Start camera slightly above the middle vertically | |
current_block_type = 1 # Start with Grass | |
# --- Initial World Generation --- | |
print("Generating initial world terrain...") | |
for col in range(WORLD_WIDTH_BLOCKS): | |
# Simple terrain: Grass top layer, 3 Dirt, rest Stone down to world bottom | |
surface_level = WORLD_HEIGHT_BLOCKS // 2 + int(math.sin(col * 0.1) * 3) # Add slight waviness | |
# Ensure surface level is within reasonable bounds | |
surface_level = max(5, min(WORLD_HEIGHT_BLOCKS - 10, surface_level)) | |
world_data[(col, surface_level)] = 1 # Grass | |
for depth in range(1, 4): | |
row = surface_level + depth | |
if row < WORLD_HEIGHT_BLOCKS: | |
world_data[(col, row)] = 2 # Dirt | |
for row in range(surface_level + 4, WORLD_HEIGHT_BLOCKS): | |
world_data[(col, row)] = 3 # Stone | |
print("World generation complete.") | |
# --- Helper Functions --- | |
def world_to_screen(world_x, world_y, cam_x, cam_y): | |
"""Converts world coordinates to screen coordinates.""" | |
screen_x = world_x - cam_x | |
screen_y = world_y - cam_y | |
return int(screen_x), int(screen_y) | |
def screen_to_grid(screen_x, screen_y, cam_x, cam_y): | |
"""Converts screen coordinates to world grid coordinates.""" | |
world_x = screen_x + cam_x | |
world_y = screen_y + cam_y | |
grid_col = world_x // BLOCK_SIZE | |
grid_row = world_y // BLOCK_SIZE | |
return grid_col, grid_row | |
# --- Game Loop --- | |
running = True | |
while running: | |
dt = clock.tick(60) / 1000.0 # Delta time in seconds, helps smooth movement if needed | |
# --- Event Handling --- | |
for event in pygame.event.get(): | |
if event.type == pygame.QUIT: | |
running = False | |
if event.type == pygame.KEYDOWN: | |
if event.key == pygame.K_ESCAPE: | |
running = False | |
# Block selection | |
if event.key == pygame.K_1: | |
current_block_type = 1 | |
elif event.key == pygame.K_2: | |
current_block_type = 2 | |
elif event.key == pygame.K_3: | |
current_block_type = 3 | |
# Add more elif blocks here for keys 4, 5, etc. if you add more block types | |
if event.type == pygame.MOUSEBUTTONDOWN: | |
mouse_screen_x, mouse_screen_y = event.pos | |
grid_col, grid_row = screen_to_grid(mouse_screen_x, mouse_screen_y, camera_x, camera_y) | |
# Optional: Check if click is within world boundaries if you want defined edges | |
# if 0 <= grid_col < WORLD_WIDTH_BLOCKS and 0 <= grid_row < WORLD_HEIGHT_BLOCKS: | |
if event.button == 1: # Left-click to place | |
# Place block only if the space is empty | |
if (grid_col, grid_row) not in world_data: | |
world_data[(grid_col, grid_row)] = current_block_type | |
if sound_place: | |
sound_place.play() | |
elif event.button == 3: # Right-click to break | |
# Break block if it exists | |
if (grid_col, grid_row) in world_data: | |
del world_data[(grid_col, grid_row)] | |
if sound_break: | |
sound_break.play() | |
# --- Continuous Key Presses (Camera Movement) --- | |
keys = pygame.key.get_pressed() | |
if keys[pygame.K_w] or keys[pygame.K_UP]: | |
camera_y -= CAMERA_SPEED | |
if keys[pygame.K_s] or keys[pygame.K_DOWN]: | |
camera_y += CAMERA_SPEED | |
if keys[pygame.K_a] or keys[pygame.K_LEFT]: | |
camera_x -= CAMERA_SPEED | |
if keys[pygame.K_d] or keys[pygame.K_RIGHT]: | |
camera_x += CAMERA_SPEED | |
# Optional: Clamp camera to world boundaries if desired | |
# camera_x = max(0, min(camera_x, WORLD_WIDTH_BLOCKS * BLOCK_SIZE - SCREEN_WIDTH)) | |
# camera_y = max(0, min(camera_y, WORLD_HEIGHT_BLOCKS * BLOCK_SIZE - SCREEN_HEIGHT)) | |
# --- Drawing --- | |
# Background | |
screen.fill(SKY_BLUE) | |
# Calculate visible grid range | |
start_col = camera_x // BLOCK_SIZE | |
end_col = start_col + (SCREEN_WIDTH // BLOCK_SIZE) + 2 # Add a buffer of 1-2 blocks | |
start_row = camera_y // BLOCK_SIZE | |
end_row = start_row + (SCREEN_HEIGHT // BLOCK_SIZE) + 2 # Add a buffer | |
# Draw Blocks (Visible ones only) | |
if block_textures: # Only draw if textures were loaded/generated | |
for row in range(start_row, end_row): | |
for col in range(start_col, end_col): | |
block_id = world_data.get((col, row)) | |
if block_id: | |
texture = block_textures.get(block_id) | |
if texture: # Check if texture exists for this ID | |
world_px = col * BLOCK_SIZE | |
world_py = row * BLOCK_SIZE | |
screen_px, screen_py = world_to_screen(world_px, world_py, camera_x, camera_y) | |
# Basic Culling: Only blit if block is potentially on screen | |
if -BLOCK_SIZE < screen_px < SCREEN_WIDTH and -BLOCK_SIZE < screen_py < SCREEN_HEIGHT: | |
screen.blit(texture, (screen_px, screen_py)) | |
else: | |
if font: # Display error message if textures failed | |
error_text = font.render("Error: Textures failed to load/generate.", True, BLACK) | |
screen.blit(error_text, (SCREEN_WIDTH // 2 - error_text.get_width() // 2, SCREEN_HEIGHT // 2)) | |
# Draw Mouse Hover Highlight | |
if highlight_surface: | |
mouse_screen_x, mouse_screen_y = pygame.mouse.get_pos() | |
hover_col, hover_row = screen_to_grid(mouse_screen_x, mouse_screen_y, camera_x, camera_y) | |
highlight_world_x = hover_col * BLOCK_SIZE | |
highlight_world_y = hover_row * BLOCK_SIZE | |
highlight_screen_x, highlight_screen_y = world_to_screen(highlight_world_x, highlight_world_y, camera_x, camera_y) | |
# Ensure highlight is drawn within screen bounds if needed (optional) | |
# if 0 <= highlight_screen_x < SCREEN_WIDTH - BLOCK_SIZE and 0 <= highlight_screen_y < SCREEN_HEIGHT - BLOCK_SIZE: | |
screen.blit(highlight_surface, (highlight_screen_x, highlight_screen_y)) | |
# --- Draw UI --- | |
if font: | |
try: | |
# Display current block type | |
block_name = BLOCK_TYPES.get(current_block_type, "Unknown") | |
ui_text_line1 = f"Selected: {block_name} [{current_block_type}]" | |
ui_surf1 = font.render(ui_text_line1, True, BLACK, WHITE) # Black text on white background box | |
screen.blit(ui_surf1, (10, 10)) | |
# Display Coordinates (optional) | |
mouse_sx, mouse_sy = pygame.mouse.get_pos() | |
h_col, h_row = screen_to_grid(mouse_sx, mouse_sy, camera_x, camera_y) | |
coord_text = f"Coords: ({h_col}, {h_row})" | |
coord_surf = font.render(coord_text, True, BLACK, WHITE) | |
screen.blit(coord_surf, (10, 40)) | |
# Draw small preview of current block | |
preview_texture = block_textures.get(current_block_type) | |
if preview_texture: | |
preview_size = BLOCK_SIZE * 1.5 # Slightly larger preview | |
scaled_preview = pygame.transform.scale(preview_texture, (int(preview_size), int(preview_size))) | |
preview_x = 15 | |
preview_y = 70 | |
# Simple border for preview | |
pygame.draw.rect(screen, WHITE, (preview_x - 2, preview_y - 2, scaled_preview.get_width() + 4, scaled_preview.get_height() + 4)) | |
pygame.draw.rect(screen, BLACK, (preview_x - 2, preview_y - 2, scaled_preview.get_width() + 4, scaled_preview.get_height() + 4), 1) | |
screen.blit(scaled_preview, (preview_x, preview_y)) | |
except Exception as e: | |
print(f"Error drawing UI: {e}") # Avoid crashing if UI drawing fails | |
# --- Update Display --- | |
pygame.display.flip() | |
# --- Cleanup --- | |
print("Exiting game.") | |
pygame.quit() | |
sys.exit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment