diff --git a/CREDITS b/CREDITS index 8949336..cf1af5f 100644 --- a/CREDITS +++ b/CREDITS @@ -1,5 +1,7 @@ Thanks to Pixabay for the sound effects and the music! +The Roboto Black font used in this project is licensed under the Open Font License. Read assets/fonts/OFL.txt for more information. + Huge Thanks to Python for being the programming language used in this game. https://www.python.org/ diff --git a/assets/fonts/OFL.txt b/assets/fonts/OFL.txt new file mode 100644 index 0000000..a417551 --- /dev/null +++ b/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/ProtestStrike-Regular.ttf b/assets/fonts/ProtestStrike-Regular.ttf deleted file mode 100644 index 3a88f0c..0000000 Binary files a/assets/fonts/ProtestStrike-Regular.ttf and /dev/null differ diff --git a/assets/fonts/Roboto-Black.ttf b/assets/fonts/Roboto-Black.ttf new file mode 100644 index 0000000..d51221a Binary files /dev/null and b/assets/fonts/Roboto-Black.ttf differ diff --git a/game/file_manager.py b/game/file_manager.py new file mode 100644 index 0000000..ecf6682 --- /dev/null +++ b/game/file_manager.py @@ -0,0 +1,122 @@ +import arcade, arcade.gui, os, time + +from utils.constants import button_style +from utils.preload import button_texture, button_hovered_texture + +from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar + +class FileManager(arcade.gui.UIView): + def __init__(self, start_directory, allowed_extensions, *args): + super().__init__() + + self.current_directory = start_directory + self.allowed_extensions = allowed_extensions + self.file_buttons = [] + self.submitted_content = "" + self.done = False + self.args = args + + self.anchor = self.ui.add(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) + self.box = self.anchor.add(arcade.gui.UIBoxLayout(size_hint=(0.7, 0.7)), anchor_x="center", anchor_y="center") + + self.content_cache = {} + self.pre_cache_contents() + + def on_show_view(self): + super().on_show_view() + + self.current_directory_label = self.anchor.add(arcade.gui.UILabel(text=self.current_directory, font_name="Roboto", font_size=24), anchor_x="center", anchor_y="top", align_y=-15) + + self.scroll_area = UIScrollArea(size_hint=(0.95, 1)) # center on screen + self.scroll_area.scroll_speed = -50 + self.box.add(self.scroll_area) + + self.scrollbar = UIScrollBar(self.scroll_area) + self.scrollbar.size_hint = (0.02, 1) + self.anchor.add(self.scrollbar, anchor_x="right", anchor_y="center") + + self.files_box = arcade.gui.UIBoxLayout(space_between=5) + self.scroll_area.add(self.files_box) + + self.back_button = self.anchor.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50), anchor_x="left", anchor_y="top", align_x=5, align_y=-5) + self.back_button.on_click = lambda event: self.change_directory(os.path.dirname(self.current_directory)) + + self.show_directory() + + def submit(self, content): + self.submitted_content = content + self.done = True + + if os.path.isfile(content): + from game.play import Game + self.window.show_view(Game(*self.args, load_from=self.submitted_content)) + + def get_content(self, directory): + if not directory in self.content_cache or time.perf_counter() - self.content_cache[directory][-1] >= 30: + try: + entries = os.listdir(directory) + except PermissionError: + return None + + filtered = [ + entry for entry in entries + if (os.path.isdir(os.path.join(directory, entry)) and not "." in entry) or + os.path.splitext(entry)[1].lower() in self.allowed_extensions + ] + + sorted_entries = sorted( + filtered, + key=lambda x: (0 if os.path.isdir(os.path.join(directory, x)) else 1, x.lower()) + ) + + self.content_cache[directory] = sorted_entries + self.content_cache[directory].append(time.perf_counter()) + + return self.content_cache[directory][:-1] + + def pre_cache_contents(self): + for directory in self.walk_limited_depth(self.current_directory): + self.get_content(directory) + + def walk_limited_depth(self, start_dir, max_depth=2): + start_dir = os.path.abspath(start_dir) + + def _walk(current_dir, current_depth): + if current_depth > max_depth: + return + + yield current_dir + try: + with os.scandir(current_dir) as it: + for entry in it: + if entry.is_dir(follow_symlinks=False): + yield from _walk(entry.path, current_depth + 1) + except PermissionError: + pass # skip directories you can't access + + return _walk(start_dir, 0) + + def show_directory(self): + self.files_box.clear() + self.file_buttons.clear() + + self.current_directory_label.text = self.current_directory + + for file in self.get_content(self.current_directory): + self.file_buttons.append(self.files_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=file, style=button_style, width=self.window.width / 1.5))) + if os.path.isdir(f"{self.current_directory}/{file}"): + self.file_buttons[-1].on_click = lambda event, directory=f"{self.current_directory}/{file}": self.change_directory(directory) + else: + self.file_buttons[-1].on_click = lambda event, file=f"{self.current_directory}/{file}": self.submit(file) + + def change_directory(self, directory): + if directory.startswith("//"): # Fix / paths + directory = directory[1:] + + self.current_directory = directory + + self.show_directory() + + def on_mouse_press(self, x, y, button, modifiers): + if button == arcade.MOUSE_BUTTON_RIGHT: + self.change_directory(os.path.dirname(self.current_directory)) diff --git a/game/play.py b/game/play.py index c4ff7f1..762b550 100644 --- a/game/play.py +++ b/game/play.py @@ -1,16 +1,18 @@ -import arcade, arcade.gui, random, math, copy, time, json +import arcade, arcade.gui, random, math, copy, time, json, os from utils.constants import COLS, ROWS, CELL_SIZE, SPACING, NEIGHBORS, button_style from utils.preload import create_sound, destroy_sound, button_texture, button_hovered_texture class Game(arcade.gui.UIView): - def __init__(self, pypresence_client=None): + def __init__(self, pypresence_client=None, generation=None, running=False, cell_grid=None, load_from=None): super().__init__() - self.generation = 0 + self.generation = generation or 0 self.population = 0 - self.running = True + self.running = running or False self.generation_fps = 10 - + self.cell_grid = cell_grid or {} + self.sprite_grid = {} + self.load_from = load_from self.pypresence_generation_count = 0 self.pypresence_client = pypresence_client @@ -30,48 +32,63 @@ class Game(arcade.gui.UIView): self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) self.info_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=5, vertical=False), anchor_x="center", anchor_y="top") - self.population_label = arcade.gui.UILabel(text="Population: 0", font_name="Protest Strike", font_size=16) + self.population_label = arcade.gui.UILabel(text="Population: 0", font_name="Roboto", font_size=16) self.info_box.add(self.population_label) - self.generation_label = arcade.gui.UILabel(text="Generation: 0", font_name="Protest Strike", font_size=16) + self.generation_label = arcade.gui.UILabel(text="Generation: 0", font_name="Roboto", font_size=16) self.info_box.add(self.generation_label) - self.fps_label = arcade.gui.UILabel(text="FPS: 10", font_name="Protest Strike", font_size=16) + self.fps_label = arcade.gui.UILabel(text="FPS: 10", font_name="Roboto", font_size=16) self.info_box.add(self.fps_label) self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50) - self.back_button.on_click = lambda e: self.main_exit() + self.back_button.on_click = lambda event: self.main_exit() self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5) + self.load_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="Load", style=button_style, width=200, height=100) + self.load_button.on_click = lambda event: self.load() + self.anchor.add(self.load_button, anchor_x="left", anchor_y="bottom", align_x=5, align_y=5) + arcade.schedule(self.update_generation, 1 / self.generation_fps) def main_exit(self): from menus.main import Main self.window.show_view(Main(self.pypresence_client)) - def setup_grid(self, randomized=False): + def setup_grid(self, load_existing=False, randomized=False): self.spritelist.clear() - self.cell_grid = {} - self.sprite_grid = {} - for row in range(ROWS): self.cell_grid[row] = {} self.sprite_grid[row] = {} + + if self.load_from: + loaded_data = [] + with open(self.load_from, "r") as file: + data = file.read().splitlines() + for line in data: + if line == "#Life 1.06": + continue + + x, y = line.split(" ") + x = int(COLS / 2 + int(x)) + y = int(ROWS / 2 + int(y)) + loaded_data.append((y, x)) + + for row in range(ROWS): for col in range(COLS): - if randomized and random.randint(0, 1) == 1: - cell = arcade.SpriteSolidColor(CELL_SIZE, CELL_SIZE, center_x=self.start_x + col * (CELL_SIZE + SPACING), center_y=self.start_y + row * (CELL_SIZE + SPACING), color=arcade.color.WHITE) - self.cell_grid[row][col] = 1 - self.sprite_grid[row][col] = cell - self.spritelist.append(cell) + if self.load_from: + self.cell_grid[row][col] = 1 if (row, col) in loaded_data else 0 + elif not load_existing: + if randomized and random.randint(0, 1): + self.cell_grid[row][col] = 1 + self.population += 1 + continue - self.population += 1 + self.cell_grid[row][col] = 0 - continue - - self.cell_grid[row][col] = 0 cell = arcade.SpriteSolidColor(CELL_SIZE, CELL_SIZE, center_x=self.start_x + col * (CELL_SIZE + SPACING), center_y=self.start_y + row * (CELL_SIZE + SPACING), color=arcade.color.WHITE) - cell.visible = False + cell.visible = self.cell_grid[row][col] self.sprite_grid[row][col] = cell self.spritelist.append(cell) @@ -96,10 +113,10 @@ class Game(arcade.gui.UIView): if neighbor_x == 0 and neighbor_y == 0: continue - if grid.get(y + neighbor_y, {}).get(x + neighbor_x) == 1: + if grid.get(y + neighbor_y, {}).get(x + neighbor_x): cell_neighbors += 1 - if grid[y][x] == 1: + if grid[y][x]: if (cell_neighbors == 2 or cell_neighbors == 3): pass # survives else: # dies @@ -129,6 +146,10 @@ class Game(arcade.gui.UIView): self.population_label.text = f"Population: {self.population}" self.generation_label.text = f"Generation: {self.generation}" + self.cell_grid.clear() + self.spritelist.clear() + self.sprite_grid.clear() + arcade.unschedule(self.update_generation) self.setup_grid() arcade.schedule(self.update_generation, 1 / self.generation_fps) @@ -170,13 +191,17 @@ class Game(arcade.gui.UIView): if grid_col < 0 or grid_row < 0 or grid_row >= ROWS or grid_col >= COLS: return - if self.cell_grid[grid_row][grid_col] == 1: + if self.cell_grid[grid_row][grid_col]: self.population -= 1 if self.settings_dict.get("sfx", True): destroy_sound.play(volume=self.settings_dict.get("sfx_volume", 50) / 100) self.sprite_grid[grid_row][grid_col].visible = False self.cell_grid[grid_row][grid_col] = 0 + def load(self): + from game.file_manager import FileManager + self.window.show_view(FileManager(os.path.expanduser("~"), [".txt"], self.pypresence_client, self.generation, self.running, self.cell_grid)) + def on_draw(self): super().on_draw() diff --git a/menus/main.py b/menus/main.py index c8211c3..e78fee7 100644 --- a/menus/main.py +++ b/menus/main.py @@ -1,5 +1,5 @@ import arcade, arcade.gui, asyncio, pypresence, time, json, copy -from utils.preload import button_texture, button_hovered_texture, theme_sound +from utils.preload import button_texture, button_hovered_texture from utils.constants import button_style, discord_presence_id from utils.utils import FakePyPresence @@ -51,7 +51,7 @@ class Main(arcade.gui.UIView): def on_show_view(self): super().on_show_view() - self.title_label = self.box.add(arcade.gui.UILabel(text="Game Of Life", font_name="Protest Strike", font_size=48)) + self.title_label = self.box.add(arcade.gui.UILabel(text="Game Of Life", font_name="Roboto", font_size=48)) self.play_button = self.box.add(arcade.gui.UITextureButton(text="Play", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=150, style=button_style)) self.play_button.on_click = lambda event: self.play() diff --git a/menus/settings.py b/menus/settings.py index 6f7c6be..198db70 100644 --- a/menus/settings.py +++ b/menus/settings.py @@ -82,7 +82,7 @@ class Settings(arcade.gui.UIView): self.value_layout.clear() for setting in settings[category]: - label = arcade.gui.UILabel(text=setting, font_name="Protest Strike", font_size=28, text_color=arcade.color.WHITE ) + label = arcade.gui.UILabel(text=setting, font_name="Roboto", font_size=28, text_color=arcade.color.WHITE ) self.key_layout.add(label) setting_dict = settings[category][setting] @@ -160,7 +160,10 @@ class Settings(arcade.gui.UIView): if self.settings_dict['vsync']: self.window.set_vsync(True) display_mode = self.window.display.get_default_screen().get_mode() - refresh_rate = display_mode.rate + if display_mode: + refresh_rate = display_mode.rate + else: + refresh_rate = 60 self.window.set_update_rate(1 / refresh_rate) self.window.set_draw_rate(1 / refresh_rate) @@ -273,7 +276,7 @@ class Settings(arcade.gui.UIView): else: font_size = 12 - self.credits_label = arcade.gui.UILabel(text=text, text_color=arcade.color.WHITE, font_name="Protest Strike", font_size=font_size, align="center", multiline=True) + self.credits_label = arcade.gui.UILabel(text=text, text_color=arcade.color.WHITE, font_name="Roboto", font_size=font_size, align="center", multiline=True) self.key_layout.add(self.credits_label) diff --git a/run.py b/run.py index 197266d..fd8a1ca 100644 --- a/run.py +++ b/run.py @@ -77,7 +77,10 @@ window = arcade.Window(width=resolution[0], height=resolution[1], title='Game Of if vsync: window.set_vsync(True) display_mode = window.display.get_default_screen().get_mode() - refresh_rate = display_mode.rate + if display_mode: + refresh_rate = display_mode.rate + else: + refresh_rate = 60 window.set_update_rate(1 / refresh_rate) window.set_draw_rate(1 / refresh_rate) elif not fps_limit == 0: diff --git a/utils/constants.py b/utils/constants.py index 08ab363..de4c5e8 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -13,10 +13,10 @@ discord_presence_id = 1363780625928028200 log_dir = 'logs' menu_background_color = arcade.color.DARK_SLATE_GRAY -button_style = {'normal': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK), 'hover': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK), - 'press': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK), 'disabled': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK)} -dropdown_style = {'normal': UIFlatButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'hover': UIFlatButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK, bg=Color(49, 154, 54)), - 'press': UIFlatButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'disabled': UIFlatButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK, bg=Color(128, 128, 128))} +button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), + 'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK)} +dropdown_style = {'normal': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'hover': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(49, 154, 54)), + 'press': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'disabled': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128))} slider_default_style = UISliderStyle(bg=Color(128, 128, 128), unfilled_track=Color(128, 128, 128), filled_track=Color(49, 154, 54)) slider_hover_style = UISliderStyle(bg=Color(49, 154, 54), unfilled_track=Color(128, 128, 128), filled_track=Color(49, 154, 54))