def __init__(self, fps=60): Game.instance = self self.stopped = False self.show_debug_info = False self.debug_info = { "FPS": 0, "FPS Target": 0, "Chars Replaced": 0, } self.screen = Screen() self.printer = None self.input_controller = None self.timer_thread = None self.printer = ConsolePrinter() self.input_controller = InputController(self) self.input_controller.start_watching_key_presses() self.printer.clear_screen() self._clear_set_empty_screen() # FPS and timming self.fps_target: int = fps self.fps_avg: float = self.fps_target self.fps_samples: int = self.fps_target * 2 self.time_adjustment: float = 0 self.last_loop_start_time: float = timestamp() - 1 / self.fps_target
def draw_screen(self, screen: Screen): ConsolePrinter.replaced = 0 _terminal_size = os.get_terminal_size() if self.terminal_size.columns != _terminal_size.columns or self.terminal_size.lines != _terminal_size.lines: self.terminal_size = _terminal_size self.previous_screen = self.get_empty_screen() # Print over the entire screen with what has been stored # in our screen representation. # Only prints over characters that have changed since the last print. screen_size = screen.size rows = min(screen_size.y, self.terminal_size.lines) columns = min(screen_size.x, self.terminal_size.columns) for row in range(0, rows): for column in range(0, columns): prev_char = self.previous_screen.get(y=row, x=column) new_char = screen.get(y=row, x=column) if new_char != prev_char: ConsolePrinter.replaced += 1 self.print_character_at(column, row, new_char) # So we don't leave the cursor in an annoying place between draws # we will draw the final character at the bottom right corner. # Also, changes don't seem to reflect on the screen until "enter" # is pressed, this adds "end='\n'" which accomplishes that. # If "end=''" changes don't draw on the screen any more." bottom_right_char = screen.get(y=rows - 1, x=columns - 1) self.print_character_at(columns - 1, rows - 2, bottom_right_char, end='\n') # Store the current state of the screen so we can # use it again next cycle self.previous_screen.apply(screen)
def draw_laid_shapes(self, grid: Grid, screen: Screen): offset = Vector2(x=grid.position.x, y=grid.position.y) for i in range(0, len(self.matrix)): row = self.matrix[i] for j in range(0, len(row)): should_draw = row[j] if should_draw: x = j + offset.x y = i + offset.y screen.set(y=y, x=x, char=self.matrix[i][j])
def draw(self, screen: Screen): # Note: Shape positions are NOT relative to the grid position offset = Vector2( x=self.position.x, y=self.position.y ) for i in range(0, len(self.matrix)): row = self.matrix[i] for j in range(0, len(row)): should_draw = row[j] if should_draw: x = j + offset.x y = i + offset.y # TODO fix this screen.set(y=y, x=x, char=self.char)
class Game(ABC): def __init__(self, fps=60): Game.instance = self self.stopped = False self.show_debug_info = False self.debug_info = { "FPS": 0, "FPS Target": 0, "Chars Replaced": 0, } self.screen = Screen() self.printer = None self.input_controller = None self.timer_thread = None self.printer = ConsolePrinter() self.input_controller = InputController(self) self.input_controller.start_watching_key_presses() self.printer.clear_screen() self._clear_set_empty_screen() # FPS and timming self.fps_target: int = fps self.fps_avg: float = self.fps_target self.fps_samples: int = self.fps_target * 2 self.time_adjustment: float = 0 self.last_loop_start_time: float = timestamp() - 1 / self.fps_target def _clear_set_empty_screen(self): self.screen = self.printer.get_empty_screen() @abstractmethod def update(self, deltatime: float): pass @abstractmethod def draw(self): # The Strategy: # Every draw cycle will print to every position in the console. # We need to build a 2D array of what should be printed. # So we create a "screen" 2D array that is filled with spaces, # then replace values at certain positions. # Then we only do a print cycle once everything is in place. if self.show_debug_info: debug_size = { "key": 0, "value": 0, } for key in self.debug_info: value = self.debug_info[key] key_width = len(str(key)) if key_width > debug_size["key"]: debug_size["key"] = key_width val_width = len(str(value)) if value is not None else 0 if val_width > debug_size["value"]: debug_size["value"] = val_width # + 2 for the colon + space we will use key_value_width = debug_size["key"] + debug_size["value"] + 2 i = 0 screen_width = self.printer.terminal_size.columns for key in self.debug_info: value = self.debug_info[key] _key = str(key).rjust(debug_size["key"]) _value = str(value if value is not None else " ").ljust(debug_size["value"]) key_value = f"{_key}: {_value}" self.screen.set(screen_width - key_value_width, i, key_value) i += 1 self.printer.draw_screen(self.screen) self._clear_set_empty_screen() def run(self): """ Called to start the game loop """ # First Loop Setup self.last_loop_start_time = timestamp() - 1 / self.fps_target # Set the last loop_start time, expected value # Loop forever while True: # Record loop time data loop_start_time = timestamp() loop_delta_time = loop_start_time - self.last_loop_start_time self.last_loop_start_time = loop_start_time if self.stopped: return self.game_loop(loop_delta_time) if self.show_debug_info: self.calculate_debug(loop_delta_time) # Calculate the "Cumulative Moving Average" with fixed n # https://en.wikipedia.org/wiki/Moving_average#Cumulative_moving_average avg = self.fps_avg samples = self.fps_samples self.fps_avg = (avg * samples + (1.0 / loop_delta_time)) / (samples + 1) # Time after game_loop is end loop_end_time = timestamp() frame_time = 1.0 / self.fps_target # How long a frame should take loop_used = loop_end_time - loop_start_time # How much time the current loop took. if self.fps_target > self.fps_avg: self.time_adjustment = self.time_adjustment + 0.00001 if self.fps_target + 0.1 < self.fps_avg: self.time_adjustment = self.time_adjustment - 0.00001 self.time_adjustment = clamp(self.time_adjustment, -frame_time, frame_time) real_wait_time = frame_time - loop_used # - self.time_adjustment real_wait_time = clamp(real_wait_time, 0, frame_time) safe_sleep(real_wait_time) def game_loop(self, loop_delta_time: float): """ [summary] Args: loop_delta_time (float): Time elapsed since last game_loop (seconds) """ if self.show_debug_info: self.calculate_debug(loop_delta_time) self.update(loop_delta_time) self.draw() def calculate_debug(self, loop_delta_time: float): self.debug_info["FPS"] = round(self.fps_avg, 1) self.debug_info["FPS Target"] = self.fps_target self.debug_info["Chars Replaced"] = ConsolePrinter.replaced def end_game(self): self.printer.clear_screen() self.stopped = True self.input_controller.stop_watching_key_presses() def set_on_keydown(self, func): self.input_controller.set_on_keydown(func) def set_on_keyup(self, func): self.input_controller.set_on_keyup(func)
def draw(self, screen: Screen): screen.draw_matrix(self.matrix, self.position) for i in range(len(self.current_shape.matrix)): for j in range(len(self.current_shape.matrix[i])): if self.current_shape.matrix[i][j]: screen.set(self.current_shape.position.x + j, self.current_shape.position.y + i, self.current_shape.char)
def get_empty_screen(self): self.terminal_size = os.get_terminal_size() return Screen(rows=self.terminal_size.lines, columns=self.terminal_size.columns)