diff --git a/game/game.py b/game/game.py new file mode 100644 index 0000000..447cc88 --- /dev/null +++ b/game/game.py @@ -0,0 +1,142 @@ +import pygame as pg +from game.settings import * +from game.game_manager import Grid +from game.util import draw_text, get_fall_interval +from game.tetrominos import Tetrominos +import game.settings as settings +import sys + + +class Game: + def __init__(self, screen, clock): + self.screen = screen + self.clock = clock + self.width, self.height = self.screen.get_size() + + self.grid_surface = pg.Surface((g_width, g_height)) + + self.next_surface = pg.Surface((150, 150)) + self.hold_surface = pg.Surface((150, 150)) + + self.game_grid = Grid(rows, cols, cell_size) + + self.current_tetromino = Tetrominos(cols, cell_size) + self.next_tetromino = Tetrominos(cols, cell_size) + self.hold_tetromino = None + + self.hold_used = False # Flag to track if hold has been used for the current tetromino + + self.fall_timer = 0 + self.move_timer = 0 + self.move_interval = 100 + + def run(self): + """Main game loop.""" + self.playing = True + while self.playing: + self.clock.tick(fps) + self.handle_events() + self.update() + self.draw() + + def handle_events(self): + """Handle user input and events.""" + for event in pg.event.get(): + if event.type == pg.QUIT: + sys.exit() + elif event.type == pg.KEYDOWN: + if event.key == pg.K_UP: + rotated_shape = [list(row) for row in zip(*self.current_tetromino.shape[::-1])] + if not self.game_grid.check_collision(rotated_shape, self.current_tetromino.x, self.current_tetromino.y): + self.current_tetromino.shape = rotated_shape # Apply rotation + elif event.key == pg.K_SPACE: + self.current_tetromino.instant_drop(self.game_grid) # Perform instant drop + self.lock_tetromino() + elif event.key == pg.K_c: + self.handle_hold() + elif event.key == pg.K_ESCAPE: # Pause the game + self.playing = False + + def update(self): + """Update the game state.""" + current_time = pg.time.get_ticks() + fall_interval = get_fall_interval(settings.game_score) + + keys = pg.key.get_pressed() + + if keys[pg.K_RIGHT]: + if current_time - self.move_timer > self.move_interval: # Ensure delay between movements + if not self.game_grid.check_collision(self.current_tetromino.shape, self.current_tetromino.x + 1, self.current_tetromino.y): + self.current_tetromino.move(1, 0) # Move right + self.move_timer = current_time # Reset timer + elif keys[pg.K_LEFT]: + if current_time - self.move_timer > self.move_interval: # Ensure delay between movements + if not self.game_grid.check_collision(self.current_tetromino.shape, self.current_tetromino.x - 1, self.current_tetromino.y): + self.current_tetromino.move(-1, 0) # Move left + self.move_timer = current_time # Reset timer + + # Handle automatic falling based on timer + if current_time - self.fall_timer > fall_interval: + if not self.game_grid.check_collision(self.current_tetromino.shape, self.current_tetromino.x, self.current_tetromino.y + 1): + self.current_tetromino.move(0, 1) # Move down + else: + self.lock_tetromino() + + self.fall_timer = current_time + + def draw(self): + """Render the game elements.""" + self.screen.fill((138, 138, 138)) + self.grid_surface.fill((87, 87, 87)) + + # Draw the grid and tetromino + self.game_grid.draw_grid(self.grid_surface) + self.current_tetromino.draw(self.grid_surface) + + # Draw next and hold previews + self.next_surface.fill((0, 0, 0)) + self.next_tetromino.draw_next_tetromino(self.next_surface) + + self.hold_surface.fill((0, 0, 0)) + if self.hold_tetromino: + self.hold_tetromino.draw_next_tetromino(self.hold_surface) + + # Draw score + draw_text(self.screen, f'Score: {settings.game_score}', 30, self.width - 75, 100, (255, 255, 255)) + + # Render surfaces + self.screen.blit(self.grid_surface, (0, 0)) + self.screen.blit(self.next_surface, (self.width - 150, 200)) + self.screen.blit(self.hold_surface, (self.width - 150, 400)) + + pg.display.update() + + def lock_tetromino(self): + """Lock the current tetromino in place and spawn a new one.""" + self.game_grid.add_tetromino(self.current_tetromino) + self.game_grid.check_full_row() + self.current_tetromino = self.next_tetromino + self.next_tetromino = Tetrominos(cols, cell_size) + self.hold_used = False + + # Check for game over + if self.game_grid.check_collision(self.current_tetromino.shape, self.current_tetromino.x, self.current_tetromino.y): + self.show_game_over() + + def handle_hold(self): + """Handle the hold tetromino functionality.""" + if not self.hold_used: + if self.hold_tetromino is None: + self.hold_tetromino = self.current_tetromino + self.current_tetromino = self.next_tetromino + self.next_tetromino = Tetrominos(cols, cell_size) + else: + self.hold_tetromino, self.current_tetromino = self.current_tetromino, self.hold_tetromino + self.current_tetromino.x = cols // 2 + self.current_tetromino.y = 0 + self.hold_used = True + + def show_game_over(self): + """Handle game over screen.""" + settings.gamestate = False + self.playing = False diff --git a/game_manager.py b/game/game_manager.py similarity index 74% rename from game_manager.py rename to game/game_manager.py index 3ed48de..1d74120 100644 --- a/game_manager.py +++ b/game/game_manager.py @@ -1,29 +1,5 @@ import pygame -import settings - -def get_fall_interval(score): - """ - Calculate the fall interval based on the current score. - """ - base_interval = 1000 # Base interval in milliseconds - decrease_per_1000_points = 50 # Decrease in ms per 1000 points - min_interval = 100 # Minimum interval in milliseconds - - # Calculate the fall interval - interval = base_interval - (score // 400) * decrease_per_1000_points - - # Ensure the interval doesn't go below the minimum - return max(interval, min_interval) - -#game states -def draw_text(surface, text, size, x, y, color): - """Utility function to render text onto a surface.""" - font = pygame.font.Font(None, size) # Use default font - text_surface = font.render(text, True, color) - text_rect = text_surface.get_rect(center=(x, y)) - surface.blit(text_surface, text_rect) - - +import game.settings as settings class Grid(): def __init__(self, rows, cols, cell_size): diff --git a/game/menu.py b/game/menu.py new file mode 100644 index 0000000..e48ecfd --- /dev/null +++ b/game/menu.py @@ -0,0 +1,82 @@ +import pygame as pg +from game.settings import * +from game.util import draw_text +import sys + + +class StartMenu: + + def __init__(self, screen, clock): + self.screen = screen + self.clock = clock + self.screen_size = self.screen.get_size() + + + def run(self): + self.menu_running = True + while self.menu_running: + self.clock.tick(60) + self.update() + self.draw() + return True + + def update(self): + + for event in pg.event.get(): + if event.type == pg.QUIT: + pg.quit() + sys.exit() + if event.type == pg.KEYDOWN: + if event.key == pg.K_RETURN: + self.menu_running = False + if event.key == pg.K_ESCAPE: + pg.quit() + sys.exit() + + def draw(self): + self.screen.fill((0, 0, 0)) + + draw_text(self.screen, "TETRIS", 50, width // 2, HEIGHT // 2 - 50, (255, 255, 255)) + draw_text(self.screen, "Press ENTER to Start", 30, width // 2, HEIGHT // 2 + 50, (200, 200, 200)) + pg.display.update() + + +class GameMenu: + def __init__(self, screen, clock): + self.screen = screen + self.clock = clock + self.screen_size = self.screen.get_size() + self.playing = True + + def run(self): + self.menu_running = True + while self.menu_running: + self.clock.tick(60) + self.update() + self.draw() + return self.playing + + def update(self): + for event in pg.event.get(): + if event.type == pg.QUIT: + pg.quit() + sys.exit() + if event.type == pg.KEYDOWN: + if event.key == pg.K_RETURN: + self.menu_running = False + if event.key == pg.K_ESCAPE: + self.playing = False + self.menu_running = False + + def draw(self): + + self.screen.fill((0, 0, 0)) + + if gamestate: + draw_text(self.screen, "Game Paused", 50, width // 2, HEIGHT // 2 - 50, (255, 255, 255)) + draw_text(self.screen, "Press ESC to Resume", 30, width // 2, HEIGHT // 2 + 10, (200, 200, 200)) + else: + draw_text(self.screen, "Game Over", 50, WIDTH // 2, HEIGHT // 2 - 50, (255, 0, 0)) + draw_text(self.screen, "Press Enter to Restart", 30, WIDTH // 2, HEIGHT // 2 + 50, (200, 200, 200)) + + pg.display.update() \ No newline at end of file diff --git a/settings.py b/game/settings.py similarity index 74% rename from settings.py rename to game/settings.py index ba0e8eb..8f98a97 100644 --- a/settings.py +++ b/game/settings.py @@ -2,7 +2,7 @@ #screen size global height, width -height, width = 600, 300 +height, width = 600, 450 #grid surface g_height, g_width = 600, 300 @@ -10,9 +10,6 @@ g_height, g_width = 600, 300 #start surface size HEIGHT, WIDTH = 600, 300 -#state of the game -game_state = True - #score game_score = 0 @@ -23,4 +20,4 @@ cell_size = 30 fps = 60 global gamestate # Add game state variable -gamestate = "playing" +gamestate = True diff --git a/tetrominos.py b/game/tetrominos.py similarity index 100% rename from tetrominos.py rename to game/tetrominos.py diff --git a/game/util.py b/game/util.py new file mode 100644 index 0000000..c596760 --- /dev/null +++ b/game/util.py @@ -0,0 +1,23 @@ +import pygame + +def get_fall_interval(score): + """ + Calculate the fall interval based on the current score. + """ + base_interval = 1000 # Base interval in milliseconds + decrease_per_1000_points = 50 # Decrease in ms per 1000 points + min_interval = 100 # Minimum interval in milliseconds + + # Calculate the fall interval + interval = base_interval - (score // 400) * decrease_per_1000_points + + # Ensure the interval doesn't go below the minimum + return max(interval, min_interval) + +#game states +def draw_text(surface, text, size, x, y, color): + """Utility function to render text onto a surface.""" + font = pygame.font.Font(None, size) # Use default font + text_surface = font.render(text, True, color) + text_rect = text_surface.get_rect(center=(x, y)) + surface.blit(text_surface, text_rect) \ No newline at end of file diff --git a/main.py b/main.py index 57a038c..7c4db22 100644 --- a/main.py +++ b/main.py @@ -1,186 +1,34 @@ -import pygame, sys -from settings import * -from game_manager import Grid, draw_text, get_fall_interval -from tetrominos import Tetrominos -import settings - - - -def setup(): - pygame.init() - global screen - screen = pygame.display.set_mode((width, height)) - global clock - clock = pygame.time.Clock() - pygame.display.set_caption("Tetris") - icon_img = pygame.image.load('assets/icon.png') - pygame.display.set_icon(icon_img) - state_screen(game_state) - -def state_screen(game_state): - """Display the start screen.""" - width = 300 - screen = pygame.display.set_mode((width, height)) - - surface = pygame.Surface((WIDTH, HEIGHT)) - surface.fill((0, 0, 0)) # Black background - - if game_state: - # Draw text - draw_text(surface, "TETRIS", 50, WIDTH // 2, HEIGHT // 2 - 50, (255, 255, 255)) - draw_text(surface, "Press ENTER to Start", 30, WIDTH // 2, HEIGHT // 2 + 50, (200, 200, 200)) - - else: - draw_text(surface, "Game Over", 50, WIDTH // 2, HEIGHT // 2 - 50, (255, 0, 0)) - draw_text(surface, "Press Enter to Restart", 30, WIDTH // 2, HEIGHT // 2 + 50, (200, 200, 200)) - - # Display the surface - screen.blit(surface, (0, 0)) - pygame.display.update() - - # Wait for the player to press ENTER - while True: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - pygame.quit() - sys.exit() - elif event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN: - game_state = True - settings.game_score = 0 - main() - return # Exit the start screen - -def pause_menu(screen): - """Render the pause menu.""" - screen.fill((0, 0, 0)) # Black background - draw_text(screen, "Game Paused", 50, 450 // 2, HEIGHT // 2 - 50, (255, 255, 255)) - draw_text(screen, "Press ESC to Resume", 30, 450 // 2, HEIGHT // 2 + 10, (200, 200, 200)) - pygame.display.update() +import pygame +from game.menu import StartMenu, GameMenu +from game.game import Game +from game.settings import width, height def main(): - width = 450 + running = True + + pygame.init() screen = pygame.display.set_mode((width, height)) + clock = pygame.time.Clock() - grid_surface = pygame.Surface((g_width, g_height)) - next_surface = pygame.Surface((150, 150)) - hold_surface = pygame.Surface((150, 150)) + pygame.display.set_caption("Tetris") + icon_img = pygame.image.load('assets/icon.png') + pygame.display.set_icon(icon_img) - # Create the grid and tetromino - game_grid = Grid(rows, cols, cell_size) - current_tetromino = Tetrominos(cols, cell_size) - next_tetromino = Tetrominos(cols, cell_size) - hold_tetromino = None + start_menu = StartMenu(screen, clock) + game_menu = GameMenu(screen, clock) - hold_used = False # Flag to track if hold has been used for the current tetromino + game = Game(screen, clock) - fall_timer = 0 + while running: - move_timer = 0 - move_interval = 100 + playing = start_menu.run() - while True: - screen.fill((138, 138, 138)) - grid_surface.fill((87, 87, 87)) # Background color + while playing: + game.run() - current_time = pygame.time.get_ticks() + playing = game_menu.run() - fall_interval = get_fall_interval(settings.game_score) - - for event in pygame.event.get(): - if event.type == pygame.QUIT: - sys.exit() - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_UP and settings.gamestate == "playing": - rotated_shape = [list(row) for row in zip(*current_tetromino.shape[::-1])] - if not game_grid.check_collision(rotated_shape, current_tetromino.x, current_tetromino.y): - current_tetromino.shape = rotated_shape # Apply rotation - elif event.key == pygame.K_SPACE and settings.gamestate == "playing": - current_tetromino.instant_drop(game_grid) # Perform instant drop - game_grid.add_tetromino(current_tetromino) # Lock tetromino in place - game_grid.check_full_row() - current_tetromino = next_tetromino - next_tetromino = Tetrominos(cols, cell_size) - hold_used = False # Reset hold flag when a new tetromino spawns - elif event.key == pygame.K_c and settings.gamestate == "playing": - if not hold_used: - if hold_tetromino is None: - hold_tetromino = current_tetromino - current_tetromino = next_tetromino - next_tetromino = Tetrominos(cols, cell_size) - else: - hold_tetromino, current_tetromino = current_tetromino, hold_tetromino - current_tetromino.x = cols // 2 - current_tetromino.y = 0 - hold_used = True - - elif event.key == pygame.K_ESCAPE: # Pause the game - if settings.gamestate == "playing": - settings.gamestate = "paused" - elif settings.gamestate == "paused": - settings.gamestate = "playing" - - if settings.gamestate == "playing": - - keys = pygame.key.get_pressed() - if keys[pygame.K_RIGHT]: - if current_time - move_timer > move_interval: # Ensure delay between movements - if not game_grid.check_collision(current_tetromino.shape, current_tetromino.x + 1, current_tetromino.y): - current_tetromino.move(1, 0) # Move right - move_timer = current_time # Reset timer - elif keys[pygame.K_LEFT]: - if current_time - move_timer > move_interval: # Ensure delay between movements - if not game_grid.check_collision(current_tetromino.shape, current_tetromino.x - 1, current_tetromino.y): - current_tetromino.move(-1, 0) # Move left - move_timer = current_time # Reset timer - - # Handle automatic falling based on timer - if current_time - fall_timer > fall_interval: - if not game_grid.check_collision(current_tetromino.shape, current_tetromino.x, current_tetromino.y + 1): - current_tetromino.move(0, 1) # Move down - else: - # Collision detected, lock the tetromino in place - game_grid.add_tetromino(current_tetromino) - game_grid.check_full_row() - current_tetromino = next_tetromino - next_tetromino = Tetrominos(cols, cell_size) # Spawn a new tetromino - hold_used = False - - # Check if game is over - if game_grid.check_collision(current_tetromino.shape, current_tetromino.x, current_tetromino.y): - game_state = False - state_screen(game_state) - - fall_timer = current_time - - - # Draw the grid and tetromino - game_grid.draw_grid(grid_surface) - current_tetromino.draw(grid_surface) - - # Draw the next and hold tetromino previews - next_surface.fill((0, 0, 0)) # Clear the next surface - next_tetromino.draw_next_tetromino(next_surface) - - hold_surface.fill((0, 0, 0)) # Clear the hold surface - if hold_tetromino is not None: - hold_tetromino.draw_next_tetromino(hold_surface) - - # Draw score - draw_text(screen, f'Score: {settings.game_score}', 30, width - 75, 100, (255, 255, 255)) - - # Render the surfaces - screen.blit(grid_surface) - screen.blit(next_surface, (width - 150, 200)) - screen.blit(hold_surface, (width - 150, 400)) - - pygame.display.update() - clock.tick(fps) - - elif settings.gamestate == "paused": - pause_menu(screen) - - -if __name__ == '__main__': - setup() \ No newline at end of file +if __name__ == "__main__": + main() \ No newline at end of file