#!/usr/bin/python3 import pygame from pygame._sdl2 import Window from dataclasses import dataclass from wobbl_tools.data_file import load_dataclass_json, save_dataclass_json from random import getrandbits def true_false_random(): return getrandbits(1) == 0 @dataclass class Settings: fps: int = 60 window_size: tuple = (1000, 600) slipperiness_left: int = 1 slipperiness_right: int = 1 class FallingSandParticle: def __init__(self, app, start_pos: tuple, color: tuple=(50, 50, 200)): self.app = app self.pos = start_pos self.color = color self.not_moving = 0 x, y = start_pos self.app.matrix[x, y] = self.color def update(self): old_pos = self.pos ox, oy = old_pos if oy >= self.app.sand_surface.get_height() - self.app.max_height: self.app.falling_sand_particles.remove(self) return x, y = self.pos if self.app.matrix[ox, oy + 1] == self.app.sand_surface.map_rgb(self.app.gray): # fall down y += 1 elif ( self.app.matrix[ox - 1, oy + self.app.slipperiness_left] == self.app.sand_surface.map_rgb(self.app.gray) and self.app.matrix[ox + 1, oy + self.app.slipperiness_right] == self.app.sand_surface.map_rgb(self.app.gray) ): # when the sand can slide in two directions if true_false_random(): x -= 1 y += self.app.slipperiness_left else: x += 1 y += self.app.slipperiness_right elif self.app.matrix[ox - 1, oy + self.app.slipperiness_left] == self.app.sand_surface.map_rgb(self.app.gray): # can only slide left x -= 1 y += self.app.slipperiness_left elif self.app.matrix[ox + 1, oy + self.app.slipperiness_right] == self.app.sand_surface.map_rgb(self.app.gray): # can only slide right x += 1 y += self.app.slipperiness_right else: if self.not_moving == 32: # counter that increases when the particle is inactive and if it is 32, if self.app.mouse_pressed[0]: # the particle's physics get deactivated for better performance. self.not_moving = 0 else: self.app.falling_sand_particles.remove(self) return else: if self.app.mouse_pressed[0]: self.not_moving = 0 else: self.not_moving += 1 return if not x > self.app.sand_surface.get_width() - 2: # game would crash when the particle goes outside the border. self.not_moving = 0 self.pos = (x, y) self.app.matrix[ox, oy] = self.app.gray self.app.matrix[x, y] = self.color class FallingSand: def __init__(self): pygame.init() self.screen = pygame.display.set_mode((1000, 600), pygame.RESIZABLE) # pygame init default shit pygame.display.set_caption("Wobbl Sand") self.window = Window.from_display_module() self.loading_surface = self.generate_loading_surface() # just the loading screen self.screen.blit(self.loading_surface, (0, 0)) pygame.display.update() self.settings = load_dataclass_json(Settings, "settings.json") # load settings setattr(self.settings, "save", lambda: save_dataclass_json(self.settings, "settings.json")) self.fps = self.settings.fps self.slipperiness_left = self.settings.slipperiness_left self.slipperiness_right = self.settings.slipperiness_right self.max_height = self.slipperiness_left if self.slipperiness_left > self.slipperiness_right else self.slipperiness_right self.falling_sand_particles = [] self.rainbow_data = [[255, 40, 40], 0] # color calculation stuff self.rainbow_steps = [(1, True), (0, False), (2, True), (1, False), (0, True), (2, False)] # colors self.gray = (20, 20, 20) # pygame objects self.clock = pygame.time.Clock() self.mouse = pygame.mouse self.mouse_pressed = self.mouse.get_pressed() self.mouse_pos = self.mouse.get_pos() self.sand_surface = pygame.Surface(self.settings.window_size) self.sand_surface.fill(self.gray) self.matrix = pygame.PixelArray(self.sand_surface) # loading finished self.window.size = self.settings.window_size self.running = True def generate_loading_surface(self): default_font = pygame.font.SysFont("ubuntu", 32, True) loading_text = default_font.render("Loading...", True, (240, 240, 240)) w, h = self.screen.get_size() loading_surface = pygame.Surface((w, h), flags=pygame.SRCALPHA) loading_surface.fill((40, 40, 40)) loading_surface.blit(loading_text, (w // 2 - loading_text.get_width() // 2, h // 2 - loading_text.get_height() // 2)) return loading_surface def loop(self): self.screen.fill(self.gray) self.matrix = pygame.PixelArray(self.sand_surface) # matrix to draw the sand particles on self.mouse_pressed = self.mouse.get_pressed() self.mouse_pos = self.mouse.get_pos() if self.mouse_pressed[0]: self.spawn_sand(self.mouse_pos) self.get_events() if not self.running: return for particle in self.falling_sand_particles: particle.update() self.matrix.close() self.screen.blit(self.sand_surface, (0, 0)) if self.loading_surface.get_alpha() > 0: self.screen.blit(self.loading_surface, (0, 0)) self.loading_surface.set_alpha(self.loading_surface.get_alpha() - 4) pygame.display.update() self.clock.tick(self.fps) def get_events(self): for event in pygame.event.get(): if event.type == pygame.QUIT: self.exit() return elif event.type == pygame.VIDEORESIZE: self.window_update(event.size) elif event.type == pygame.KEYDOWN: if event.key == pygame.K_r: self.reset_sand_particles() def exit(self): self.running = False self.settings.save() print("Bye!") def window_update(self, size): w, h = size self.settings.window_size = size self.matrix.close() old_sand_surface = self.sand_surface self.sand_surface = pygame.Surface(size) self.sand_surface.fill(self.gray) self.sand_surface.blit(old_sand_surface, (0, 0)) self.matrix = pygame.PixelArray(self.sand_surface) def spawn_sand(self, position): for ax in range(-8, 9): color = self.rainbow() for ay in range(-8, 9): bx, by = position bx += ax by += ay if not bx > self.sand_surface.get_width() - 2 and self.matrix[bx, by] == self.sand_surface.map_rgb(self.gray) and bx % 2 == 0 and by % 2 == 0: self.falling_sand_particles.append(FallingSandParticle(self, (bx, by), color)) def rainbow(self): color = self.rainbow_data[0] step_index = self.rainbow_data[1] step = self.rainbow_steps[step_index] if step[1]: if color[step[0]] == 255: if step_index == 5: step_index = 0 else: step_index += 1 else: color[step[0]] += 1 else: if color[step[0]] == 40: if step_index == 5: step_index = 0 else: step_index += 1 else: color[step[0]] -= 1 self.rainbow_data = [color, step_index] return tuple(color) def reset_sand_particles(self): self.falling_sand_particles.clear() self.sand_surface.fill(self.gray) self.rainbow_data = [[255, 40, 40], 0] if __name__ == "__main__": sand = FallingSand() while sand.running: sand.loop()