diff --git a/assets/cyan_block.png b/assets/cyan_block.png new file mode 100644 index 0000000..3ee211a Binary files /dev/null and b/assets/cyan_block.png differ diff --git a/assets/magenta_block.png b/assets/magenta_block.png new file mode 100644 index 0000000..7db8d9e Binary files /dev/null and b/assets/magenta_block.png differ diff --git a/assets/yellow_block.png b/assets/yellow_block.png new file mode 100644 index 0000000..6cbafd5 Binary files /dev/null and b/assets/yellow_block.png differ diff --git a/bricks.py b/bricks.py new file mode 100644 index 0000000..d4434ab --- /dev/null +++ b/bricks.py @@ -0,0 +1,78 @@ +import pygame +import enums +from random import choice + +BRICKS = [ + " X " + "\n" + "XXX" + "\n" + " X ", + " X" + "\n" + "XXX", + "X " + "\n" + "XXX", + "X", + "XXXX", + "XX" + "\n" + "XX", + " XX" + "\n" + "XX ", + "XX " + "\n" + " XX", + "X X" + "\n" + "XXX", + "XXX" + "\n" + " X " +] + +TILE_SIZE = 48 + +class Brick: + def __init__(self, brick, state = enums.BrickState.Next): + self.layout = [] + self.color = choice(list(enums.BrickColor)) + self.set_state(state) + self.angle = 0 + self.direction = None + self.load_brick(brick) + self.get_image() + self.block_image = pygame.transform.scale(self.img, (48, 48)) if self.img else None + self.draw_brick() + + def get_image(self): + match self.color: + case enums.BrickColor.Magenta: + self.img = pygame.image.load("assets/magenta_block.png").convert_alpha() + case enums.BrickColor.Cyan: + self.img = pygame.image.load("assets/cyan_block.png").convert_alpha() + case enums.BrickColor.Yellow: + self.img = pygame.image.load("assets/yellow_block.png").convert_alpha() + case _: + self.img = pygame.image.load("assets/magenta_block.png").convert_alpha() + + def load_brick(self, brick): + self.layout = [l for l in BRICKS[brick].splitlines()] + + self.rows = len(self.layout) + self.cols = len(self.layout[0]) + self.width = self.cols * TILE_SIZE + self.height = self.rows * TILE_SIZE + + def draw_brick(self): + self.brick = pygame.Surface((self.width, self.height)) + for y, row in enumerate(self.layout): + for x, char in enumerate(row): + if char == "X": + self.brick.blit(self.block_image, (1 + x * TILE_SIZE, 1 + y * TILE_SIZE)) + + def is_current(self): + return True if self.state == enums.BrickState.Current else False + + def update(self): + pass + + def rotate(self): + print("Rotating") + + def set_state(self, state): + self.state = state + print(f"State set to {self.state}") + + def move_right(self): + print("Moving right") + + def move_left(self): + print("Moving left") + + def drop(self): + print("Dropping") \ No newline at end of file diff --git a/dropnext.py b/dropnext.py new file mode 100644 index 0000000..f68bb14 --- /dev/null +++ b/dropnext.py @@ -0,0 +1,15 @@ +import pygame + +class DropNext(): + def __init__(self, width, height): + self.dropnext = pygame.Surface((width, height)) + self.width = width + self.height = height + self.dropnext.fill((0, 0, 0)) # Fill with black + + def draw(self, screen, tile_size): + screen.blit(self.dropnext, (tile_size * 15, tile_size * 2)) + + def draw_block(self, brick): + self.dropnext.blit(brick, ((self.width - brick.get_width()) / 2, (self.height - brick.get_height()) / 2)) + \ No newline at end of file diff --git a/dropzone.py b/dropzone.py new file mode 100644 index 0000000..6a2036b --- /dev/null +++ b/dropzone.py @@ -0,0 +1,12 @@ +import pygame + +class DropZone(): + def __init__(self, width, height): + self.dropzone = pygame.Surface((width, height)) + self.width = width + self.height = height + self.dropzone.fill((0, 0, 0)) # Fill with black + + def draw(self, screen, tile_size): + screen.blit(self.dropzone, (tile_size * 4, tile_size * 1)) + \ No newline at end of file diff --git a/enums.py b/enums.py new file mode 100644 index 0000000..a9dba35 --- /dev/null +++ b/enums.py @@ -0,0 +1,19 @@ +from enum import Enum + +class BrickColor(Enum): + White = (255, 255, 255) + Red = (255, 0, 0) + Green = (0, 255, 0) + Blue = (0, 0, 255) + Magenta = (255, 0, 255) + Yellow = (255, 255, 0) + Cyan = (0, 255, 255) + +class BrickState(Enum): + Current = 0 + Next = 1 + +class BrickDirection(Enum): + Left = 0 + Right = 1 + Dropped = 2 \ No newline at end of file diff --git a/hud.py b/hud.py new file mode 100644 index 0000000..e3e3569 --- /dev/null +++ b/hud.py @@ -0,0 +1,76 @@ +import pygame +import os +from enums import BrickColor + +class Hud: + def __init__(self, screen_width, screen_height, tile_size, font_size=36, highscore_file="highscore.txt"): + self.score = 0 + self.lines = 0 + self.level = 0 + self.highscore_file = highscore_file + self.highscore = self.load_highscore() + + self.font = pygame.font.Font(None, font_size) + self.color = BrickColor.Red.value + self.screen_width = screen_width + self.screen_height = screen_height + self.tile_size = tile_size + + def load_highscore(self): + if os.path.exists(self.highscore_file): + with open(self.highscore_file, 'r') as file: + try: + return int(file.read()) + except ValueError: + return 0 + return 0 + + def save_highscore(self): + with open(self.highscore_file, 'w') as file: + file.write(str(self.highscore)) + + def add_points(self, points): + self.score += points + if self.score > self.highscore: + self.highscore = self.score + self.save_highscore() + + def add_lines(self, lines): + self.lines += lines + if lines > 2: + self.add_points(lines * lines * 100) + else: + self.add_points(lines * 100) + + def level_up(self): + self.level += 1 + + def reset(self, reset_score=True): + if reset_score: + self.score = 0 + + def draw(self, screen): + # Score (top-left) + score_text = self.font.render(f"Score:", True, self.color) + score = self.font.render(f"{self.score}", True, self.color) + screen.blit(score_text, (self.tile_size / 2, self.tile_size)) + screen.blit(score,(self.tile_size / 2, self.tile_size + 24)) + + lines_text = self.font.render(f"Lines:", True, self.color) + lines = self.font.render(f"{self.lines}", True, self.color) + screen.blit(lines_text, (self.tile_size / 2, self.tile_size * 2 + 24)) + screen.blit(lines, (self.tile_size / 2, self.tile_size * 3)) + + level_text = self.font.render(f"Level:", True, self.color) + level = self.font.render(f"{self.level}", True, self.color) + screen.blit(level_text, (self.tile_size / 2, self.tile_size * 4)) + screen.blit(level, (self.tile_size / 2, self.tile_size * 4 + 24)) + + # Highscore (top-center) + highscore_text = self.font.render(f"High Score:", True, self.color) + highscore = self.font.render(f"{self.highscore}", True, self.color) + screen.blit(highscore_text, (self.tile_size / 2, self.tile_size * 5 + 24)) + screen.blit(highscore, (self.tile_size / 2, self.tile_size * 6)) + + next_text = self.font.render("Next:", True, self.color) + screen.blit(next_text, (self.tile_size * 15, self.tile_size)) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..82aa9c8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pygame==2.6.1 +pygamecontrols==0.1.9 diff --git a/tetris.py b/tetris.py new file mode 100644 index 0000000..77ded75 --- /dev/null +++ b/tetris.py @@ -0,0 +1,175 @@ +import pygame +import pygameControls as PC +import enums +from bricks import Brick, BRICKS, TILE_SIZE +from random import randrange +from dropzone import DropZone +from dropnext import DropNext +from hud import Hud + +__version__ = "0.0.1" + +# Constants +HAT_REPEAT_DELAY = 0 # milliseconds before first repeat +HAT_REPEAT_INTERVAL = 200 # milliseconds between repeats +RUMBLE_TIMEOUT = 200 + +class Tetris: + def __init__(self): + self.setup() + self.running = True + + while self.running: + self.main_loop() + + pygame.quit() + + def setup(self): + pygame.init() + pygame.key.set_repeat(200) + self.joystick_count = pygame.joystick.get_count() + + self.joysticks = {} + + self.screen_width, self.screen_height = (20 * TILE_SIZE), (20 * TILE_SIZE) + self.screen = pygame.display.set_mode((self.screen_width, self.screen_height)) + pygame.display.set_caption("Tetris " + __version__) + + self.current = Brick(brick = randrange(0, len(BRICKS)), state = enums.BrickState.Current) + print(self.current.layout) + print(self.current.color) + self.next = Brick(brick = randrange(0, len(BRICKS))) + print(self.next.layout) + print(self.next.color) + + self.hud = Hud(self.screen_width, self.screen_height, TILE_SIZE) + self.dropzone = DropZone(width = TILE_SIZE * 10, height = TILE_SIZE * 18) + self.dropnext = DropNext(width = TILE_SIZE * 4, height = TILE_SIZE * 4) + + self.clock = pygame.time.Clock() + self.rumble_timer = pygame.time.get_ticks() + + def main_loop(self): + if self.joysticks: + if pygame.time.get_ticks() - self.rumble_timer > RUMBLE_TIMEOUT: + self.joysticks[self.joy.get_instance_id()].controllers[0].stop_rumble() + + self.screen.fill(enums.BrickColor.Cyan.value) + + self.hud.draw(self.screen) + self.dropzone.draw(self.screen, TILE_SIZE) + self.dropnext.draw(self.screen, TILE_SIZE) + self.dropnext.draw_block(self.next.brick) + + self.handle_input() + + pygame.display.flip() + self.clock.tick(60) + + def handle_hat_repeat(self): + now = pygame.time.get_ticks() + if self.hat_direction != (0, 0): + if self.hat_first_press: + if now - self.hat_timer >= HAT_REPEAT_DELAY: + self.hat_timer = now + self.hat_first_press = False + self.post_hat_repeat_event() + else: + if now - self.hat_timer >= HAT_REPEAT_INTERVAL: + self.hat_timer = now + self.post_hat_repeat_event() + + def post_hat_repeat_event(self): + pygame.event.post(pygame.event.Event(pygame.USEREVENT, { + "type_name": "JOYHATREPEAT", + "value": self.hat_direction + })) + + def handle_input(self): + for event in pygame.event.get(): + match event.type: + case pygame.QUIT: + self.running = False + case pygame.KEYDOWN: + match event.key: + case pygame.K_RIGHT: + if self.current.direction == enums.BrickDirection.Dropped: + break + self.current.direction = enums.BrickDirection.Right + self.current.move_right() + case pygame.K_LEFT: + if self.current.direction == enums.BrickDirection.Dropped: + break + self.current.direction = enums.BrickDirection.Left + self.current.move_left() + case pygame.K_UP: + if self.current.direction == enums.BrickDirection.Dropped: + break + self.current.rotate() + case pygame.K_DOWN: + if self.current.direction == enums.BrickDirection.Dropped: + break + self.current.drop() + case pygame.JOYHATMOTION: + self.hat_direction = event.value + self.hat_x, self.hat_y = self.hat_direction + self.hat_timer = pygame.time.get_ticks() + self.hat_first_press = True + + if self.hat_x == 1: + if self.current.direction == enums.BrickDirection.Dropped: + break + self.current.direction = enums.BrickDirection.Right + self.current.move_right() + elif self.hat_x == -1: + if self.current.direction == enums.BrickDirection.Dropped: + break + self.current.direction = enums.BrickDirection.Left + self.current.move_left() + elif self.hat_y == 1: + if self.current.direction == enums.BrickDirection.Dropped: + break + self.current.rotate() + elif self.hat_y == -1: + if self.current.direction == enums.BrickDirection.Dropped: + break + self.current.drop() + + case pygame.USEREVENT: + if event.dict.get("type_name") == "JOYHATREPEAT": + match event.dict['value']: + case (1, 0): + if self.current.direction == enums.BrickDirection.Dropped: + break + self.current.direction = enums.BrickDirection.Right + self.current.move_right() + case (-1, 0): + if self.current.direction == enums.BrickDirection.Dropped: + break + self.current.direction = enums.BrickDirection.Left + self.current.move_left() + case (0, 1): + if self.current.direction == enums.BrickDirection.Dropped: + break + self.current.rotate() + case (0, -1): + if self.current.direction == enums.BrickDirection.Dropped: + break + self.current.drop() + + # Handle hotplugging + case pygame.JOYDEVICEADDED: + # This event will be generated when the program starts for every + # joystick, filling up the list without needing to create them manually. + self.joy = pygame.joystick.Joystick(event.device_index) + self.joysticks[self.joy.get_instance_id()] = PC.controller.Controllers(self.joy) + + case pygame.JOYDEVICEREMOVED: + del self.joysticks[event.instance_id] + print(f"Joystick {event.instance_id} disconnected") + + def exit(self): + self.running = False + +if __name__ == "__main__": + game = Tetris() \ No newline at end of file