commit eda1f9f8913ed65d8f7603fbd3cf3f59de2288de Author: LucasKalil-Programador Date: Mon Jul 22 17:07:41 2024 -0300 Projeto iniciado e finalizado diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bb619f --- /dev/null +++ b/.gitignore @@ -0,0 +1,185 @@ +# Created by .ignore support plugin (hsz.mobi) +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject +### VirtualEnv template +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +.venv +pip-selfcheck.json + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# idea folder, uncomment if you don't need it +# .idea \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/SandBoxPyGame.iml b/.idea/SandBoxPyGame.iml new file mode 100644 index 0000000..e01febf --- /dev/null +++ b/.idea/SandBoxPyGame.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..a8d5fdb --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..33bf82c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/SandBox.py b/SandBox.py new file mode 100644 index 0000000..9c03aca --- /dev/null +++ b/SandBox.py @@ -0,0 +1,190 @@ +import random +from enum import Enum +import pygame +import numpy as np +from numba import njit + + +class Types(Enum): + BG = (0, 0, 0, 0, 0) + SAND = (203, 189, 147, 1, 0) + WATER = (28, 163, 236, 2, 0) + STONE = (115, 112, 112, 3, 0) + VACUUM = (20, 20, 20, 4, 0) + CLONER = (108, 60, 12, 5, 0) + + +@njit +def run_physics(data, size): + if random.random() > 0.5: + data = data[::-1] + + for x in range(len(data)): + skip_y = False + for y in range(len(data[x])): + if skip_y or y + 1 >= size[1]: + skip_y = False + elif data[x, y, 3] == Types.SAND.value[3]: + skip_y = sand_physics(data, size, x, y) + elif data[x, y, 3] == Types.WATER.value[3]: + skip_y = water_physics(data, size, x, y) + elif data[x, y, 3] == Types.VACUUM.value[3]: + skip_y = vacuum_physics(data, size, x, y) + elif data[x, y, 3] == Types.CLONER.value[3]: + skip_y = cloner_physics(data, size, x, y) + + +@njit +def cloner_physics(data, size, x, y): + adjacent = ((x - 1, y), (x, y - 1), (x + 1, y), (x, y + 1)) + type_id = data[x, y, 4] + for adj_x, adj_y in adjacent: + if adj_x < 0 or adj_y < 0 or adj_x >= size[0] or adj_y >= size[1]: + continue + if type_id == Types.BG.value[3]: + if data[adj_x, adj_y, 3] not in (Types.CLONER.value[3], Types.BG.value[3], Types.VACUUM.value[3]): + data[x, y, 4] = data[adj_x, adj_y, 3] + else: + if data[adj_x, adj_y, 3] == Types.BG.value[3]: + type_b = Types.BG + if data[x, y, 4] == Types.SAND.value[3]: + type_b = Types.SAND + elif data[x, y, 4] == Types.WATER.value[3]: + type_b = Types.WATER + elif data[x, y, 4] == Types.STONE.value[3]: + type_b = Types.STONE + + if type_b.value[3] in (Types.SAND.value[3], Types.WATER.value[3], Types.STONE.value[3]): + spawn_pixel(data, adj_x, adj_y, color=type_b.value, cmod=10) + elif type_b.value[3] in (Types.BG.value[3], Types.VACUUM.value[3], Types.CLONER.value[3]): + spawn_pixel(data, adj_x, adj_y, color=type_b.value) + return False + + +@njit +def vacuum_physics(data, size, x, y): + adjacent = ((x - 1, y), (x, y - 1), (x + 1, y), (x, y + 1)) + for adj_x, adj_y in adjacent: + if adj_x < 0 or adj_y < 0 or adj_x >= size[0] or adj_y >= size[1]: + continue + if data[adj_x, adj_y, 3] not in (Types.VACUUM.value[3], Types.BG.value[3]): + spawn_pixel(data, adj_x, adj_y, color=Types.BG.value) + return False + + +@njit +def sand_physics(data, size, x, y): + # region Gravity + if data[x, y + 1, 3] == Types.BG.value[3]: + data[x, y + 1] = data[x, y] + data[x, y] = Types.BG.value + return True + # endregion Gravity + # region Drop down left or right + random_side = -1 if random.random() > 0.5 else 1 + if x + random_side < 0 or x + random_side >= size[0]: + random_side *= -1 + + if data[x + random_side, y + 1, 3] in (Types.BG.value[3], Types.WATER.value[3]): + data[x + random_side, y + 1], data[x, y] = data[x, y], data[x + random_side, y + 1].copy() + return False + + random_side *= -1 + if x + random_side < 0 or x + random_side >= size[0]: + return False + + if data[x + random_side * -1, y + 1, 3] in (Types.BG.value[3], Types.WATER.value[3]): + data[x + random_side * -1, y + 1], data[x, y] = data[x, y], data[x + random_side * -1, y + 1].copy() + return False + # endregion Drop down left or right + return False + + +@njit +def water_physics(data, size, x, y): + # region Gravity + if data[x, y + 1, 3] == Types.BG.value[3]: + data[x, y + 1], data[x, y] = data[x, y], Types.BG.value + return True + # endregion Gravity + # region Drop down left or Right + random_side = -1 if random.random() > 0.5 else 1 + if x + random_side < 0 or x + random_side >= size[0]: + random_side *= -1 + + if data[x + random_side, y + 1, 3] == Types.BG.value[3]: + data[x + random_side, y + 1], data[x, y] = data[x, y], Types.BG.value + return False + # endregion Drop down left or Right + # region Sand fall over water + if y - 1 > 0 and data[x, y - 1, 3] == Types.SAND.value[3]: + data[x, y - 1], data[x, y] = data[x, y], data[x, y - 1].copy() + return True + # endregion Sand fall over water + # region Moving horizontal + max_steps = size[0] + if data[x, y + 1][3] != Types.BG.value[3]: + for new_x in range(x + 1, min(x + max_steps, size[0])): + if data[new_x, y, 3] == Types.BG.value[3]: + if data[new_x, y + 1, 3] == Types.BG.value[3]: + data[new_x, y], data[x, y] = data[x, y], Types.BG.value + return False + else: + break + for new_x in range(x - 1, max(x - max_steps, 0), -1): + if data[new_x, y, 3] == Types.BG.value[3]: + if data[new_x, y + 1, 3] == Types.BG.value[3]: + data[new_x, y], data[x, y] = data[x, y], Types.BG.value + return False + else: + break + # endregion Moving horizontal + return False + + +@njit +def spawn_pixel(data, x, y, color=Types.BG.value, cmod=0): + color = (color[0] + random.randint(-cmod, cmod), + color[1] + random.randint(-cmod, cmod), + color[2] + random.randint(-cmod, cmod), + color[3], color[4]) + data[x, y] = color + + +@njit +def spawn(data, size, center, radius, type_b=Types.BG): + for x in range(center[0] - radius, center[0] + radius): + for y in range(center[1] - radius, center[1] + radius): + if y < 0 or x < 0 or y >= size[1] or x >= size[0]: + continue + + if (y - center[1]) ** 2 + (x - center[0]) ** 2 <= radius ** 2 and data[x, y, 3] != type_b.value[3]: + if type_b.value[3] in (Types.SAND.value[3], Types.WATER.value[3], Types.STONE.value[3]): + spawn_pixel(data, x, y, color=type_b.value, cmod=10) + elif type_b.value[3] in (Types.BG.value[3], Types.VACUUM.value[3], Types.CLONER.value[3]): + spawn_pixel(data, x, y, color=type_b.value) + + +class SandBox: + def __init__(self, size: tuple[int, int]): + self.__size = size + self.__data: np.ndarray = np.empty(0) + self.reset() + self.__surface = pygame.Surface(size) + self.update_surface() + + def spawn(self, center, radius, type_b=Types.BG): + spawn(self.__data, self.__size, center, radius, type_b) + + def run_physics(self): + run_physics(self.__data, self.__size) + + def reset(self): + self.__data = np.full((self.__size[0], self.__size[1], 5), Types.BG.value, dtype=np.uint8) + + def update_surface(self): + pygame.surfarray.blit_array(self.__surface, self.__data[:, :, :3]) + + def get_surface(self): + self.update_surface() + return self.__surface diff --git a/main.py b/main.py new file mode 100644 index 0000000..0becafa --- /dev/null +++ b/main.py @@ -0,0 +1,150 @@ +import random +import threading +import time + +import pygame +import sys +import SandBox + + +def handle_actions(keys_pressed: dict[any, bool], on_key_click): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + + if event.type == pygame.KEYDOWN: + keys_pressed[event.key] = True + elif event.type == pygame.KEYUP: + if keys_pressed.get(event.key, False): + keys_pressed[event.key] = False + on_key_click(event) + + +class Game: + def __init__(self, size=(160, 160), scale=4.0, fps=60, font_size=30, test_mode=False): + # Start variables + self.performance_str = None + self.size, self.scale, self.fps = size, scale, fps + self.selected_item, self.spawn_radius = SandBox.Types.SAND, 10 + self.screen_size = (self.size[0] * self.scale, self.size[1] * self.scale) + self.test_mode, self.performance_data, self.performance_str = test_mode, {}, "" + self.paused = False + self.keys_pressed = {} + + # Start + self.sandBox = SandBox.SandBox(self.size) + + # Start pygame + pygame.init() + self.screen = pygame.display.set_mode(self.screen_size, pygame.RESIZABLE) + pygame.display.set_caption("Sand box game") + + # Start pygame utils + self.font = pygame.font.Font(None, font_size) + self.fps_clock = pygame.time.Clock() + + def handle_key_inputs(self, event): + key_to_item = { + pygame.K_1: SandBox.Types.BG, + pygame.K_2: SandBox.Types.SAND, + pygame.K_3: SandBox.Types.WATER, + pygame.K_4: SandBox.Types.STONE, + pygame.K_5: SandBox.Types.VACUUM, + pygame.K_6: SandBox.Types.CLONER, + } + + if event.key == pygame.K_KP_PLUS and self.spawn_radius < min(self.size[0], self.size[1]): + self.spawn_radius += 10 if pygame.key.get_pressed()[pygame.K_LCTRL] else 1 + elif event.key == pygame.K_KP_MINUS and self.spawn_radius > 1: + self.spawn_radius -= 10 if pygame.key.get_pressed()[pygame.K_LCTRL] else 1 + elif event.key == pygame.K_p: + self.paused = not self.paused + elif event.key == pygame.K_r: + self.performance_data = {} + self.selected_item = SandBox.Types.SAND + self.spawn_radius = 10 + self.sandBox.reset() + elif event.key in key_to_item: + for key, item in key_to_item.items(): + if key == event.key: + self.selected_item = item + break + + def handle_mouse_inputs(self): + buttons_pressed = pygame.mouse.get_pressed() + mouse_pos = pygame.mouse.get_pos() + if buttons_pressed[0]: + pos = (int(mouse_pos[0] / self.scale), int(mouse_pos[1] / self.scale)) + self.sandBox.spawn(pos, self.spawn_radius, type_b=self.selected_item) + + def perform_random_spawn(self): + type_b = random.choice(list(SandBox.Types)) + x, y = random.randint(0, self.size[0]), random.randint(0, self.size[1]) + self.sandBox.spawn((x, y), self.spawn_radius, type_b) + + def performance_monitor(self): + current_fps = int(self.fps_clock.get_fps()) + if current_fps in self.performance_data: + self.performance_data[current_fps] = self.performance_data[current_fps] + 1 + else: + self.performance_data[current_fps] = 1 + + fps_sum, occurrences_count = 0, 0 + for fps, count in self.performance_data.items(): + occurrences_count += count + fps_sum += count * fps + sorted_keys = sorted(self.performance_data.keys()) + self.performance_str = f"MIN: {sorted_keys[0]}, MAX: {sorted_keys[-1]}, AVG: {fps_sum // occurrences_count}" + + def run(self): + self.__loading() + + while True: + handle_actions(self.keys_pressed, self.handle_key_inputs) + self.handle_mouse_inputs() + + if self.test_mode and not self.paused: + self.performance_monitor() + self.perform_random_spawn() + + self.__render_sand_box() + self.__render_gui() + pygame.display.flip() + + self.fps_clock.tick(self.fps) + + def __loading(self): + # First run to force numba compilation + thread1 = threading.Thread(target=self.sandBox.spawn, args=((0, 0), 10)) + thread2 = threading.Thread(target=self.sandBox.run_physics) + thread1.start() + thread2.start() + while thread1.is_alive() or thread2.is_alive(): + for i in range(4): + self.__render_message("Loading" + "".ljust(i, "."), (50, 255, 50)) + time.sleep(.25) + + def __render_message(self, text="", color=(0, 0, 0)): + fps_text = self.font.render(text, True, color) + self.screen.fill((0, 0, 0)) + self.screen.blit(fps_text, (5, 5)) + pygame.display.flip() + + def __render_sand_box(self): + if not self.paused: + self.sandBox.run_physics() + sb_surface = pygame.transform.scale(self.sandBox.get_surface(), self.screen_size) + self.screen.blit(sb_surface, (0, 0)) + + def __render_gui(self): + fps_str = f'Item: {self.selected_item.name}, Pencil size: {self.spawn_radius}, Fps: {int(self.fps_clock.get_fps())} ' + if self.test_mode: + fps_str += self.performance_str + fps_text = self.font.render(fps_str, True, (50, 255, 50)) + self.screen.blit(fps_text, (5, 5)) + + +if __name__ == '__main__': + game = Game(size=(1000, 1000), fps=300, scale=1, test_mode=True) + game.run() diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..e69de29