Skip to content

Commit

Permalink
Split game logic into multiple sprites
Browse files Browse the repository at this point in the history
  • Loading branch information
sourabhv committed Jun 18, 2023
1 parent 7289d01 commit 53cea01
Show file tree
Hide file tree
Showing 17 changed files with 547 additions and 358 deletions.
435 changes: 77 additions & 358 deletions src/flappy.py

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions src/sprites/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from .background import Background
from .floor import Floor
from .game_over import GameOver
from .pipe import Pipe, Pipes
from .player import Player, PlayerMode
from .score import Score
from .sprite import Sprite
from .welcome_message import WelcomeMessage

__all__ = [
"Background",
"Floor",
"Pipe",
"Pipes",
"Player",
"Score",
"Sprite",
"WelcomeMessage",
]
10 changes: 10 additions & 0 deletions src/sprites/background.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .sprite import Sprite


class Background(Sprite):
def setup(self) -> None:
self.x = 0
self.y = 0

def tick(self) -> None:
self.screen.blit(self.images.background, (self.x, self.y))
20 changes: 20 additions & 0 deletions src/sprites/floor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from .sprite import Sprite


class Floor(Sprite):
def setup(self) -> None:
self.x = 0
self.y = self.window.play_area_height
# amount to shift on each tick
self.vel_x = 4
# amount by which floor can maximum shift to left
self.x_extra = (
self.images.base.get_width() - self.images.background.get_width()
)

def stop(self) -> None:
self.vel_x = 0

def tick(self) -> None:
self.x = -((-self.x + self.vel_x) % self.x_extra)
self.screen.blit(self.images.base, (self.x, self.y))
10 changes: 10 additions & 0 deletions src/sprites/game_over.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .sprite import Sprite


class GameOver(Sprite):
def setup(self) -> None:
self.x = int((self.window.width - self.images.gameover.get_width()) / 2)
self.y = int(self.window.height * 0.2)

def tick(self) -> None:
self.screen.blit(self.images.gameover, (self.x, self.y))
112 changes: 112 additions & 0 deletions src/sprites/pipe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import random
from typing import List

from pygame import Surface

from .sprite import Sprite


class Pipe(Sprite):
def setup(self) -> None:
self.x = 0
self.y = 0
self.set_image(self.images.pipe[0])
self.mid_x = self.x + self.images.pipe[0].get_width() / 2
# TODO: make this change with game progress
self.vel_x = -5

def set_image(self, image: Surface) -> None:
self.image = image
self.width = self.image.get_width()
self.height = self.image.get_height()

def tick(self) -> None:
self.x += self.vel_x
self.mid_x = self.x + self.images.pipe[0].get_width() / 2
self.screen.blit(self.image, (self.x, self.y))


class Pipes(Sprite):
upper: List[Pipe]
lower: List[Pipe]

def setup(self) -> None:
# TODO: make this change with game progress
self.pipe_gap = 120
self.top = 0
self.bottom = self.window.play_area_height
self.reset()

def reset(self) -> None:
self.upper = []
self.lower = []
self.spawn_initial_pipes()

def tick(self) -> None:
if self.can_spawn_more():
self.spawn_new_pipes()
self.remove_old_pipes()

for up_pipe, low_pipe in zip(self.upper, self.lower):
up_pipe.tick()
low_pipe.tick()

def stop(self) -> None:
for pipe in self.upper + self.lower:
pipe.vel_x = 0

def can_spawn_more(self) -> bool:
# has 1 or 2 pipe and first pipe is almost about to exit the screen
return 0 < len(self.upper) < 3 and 0 < self.upper[0].x < 5

def spawn_new_pipes(self):
# add new pipe when first pipe is about to touch left of screen
upper, lower = self.make_random_pipes()
self.upper.append(upper)
self.lower.append(lower)

def remove_old_pipes(self):
# remove first pipe if its out of the screen
if (
len(self.upper) > 0
and self.upper[0].x < -self.images.pipe[0].get_width()
):
self.upper.pop(0)
self.lower.pop(0)

def spawn_initial_pipes(self):
upper_1, lower_1 = self.make_random_pipes()
upper_1.x = self.window.width + 100
lower_1.x = self.window.width + 100

upper_2, lower_2 = self.make_random_pipes()
upper_2.x = self.window.width + 100 + (self.window.width / 2)
lower_2.x = self.window.width + 100 + (self.window.width / 2)

self.upper.append(upper_1)
self.upper.append(upper_2)

self.lower.append(lower_1)
self.lower.append(lower_2)

def make_random_pipes(self):
"""returns a randomly generated pipe"""
# y of gap between upper and lower pipe
base_y = self.window.play_area_height

gap_y = random.randrange(0, int(base_y * 0.6 - self.pipe_gap))
gap_y += int(base_y * 0.2)
pipe_height = self.images.pipe[0].get_height()
pipe_x = self.window.width + 10

upper_pipe = Pipe(*self._args)
upper_pipe.x = pipe_x
upper_pipe.y = gap_y - pipe_height
upper_pipe.set_image(self.images.pipe[0])

lower_pipe = Pipe(*self._args)
lower_pipe.x = pipe_x
lower_pipe.y = gap_y + self.pipe_gap
lower_pipe.set_image(self.images.pipe[1])

return upper_pipe, lower_pipe
193 changes: 193 additions & 0 deletions src/sprites/player.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
from enum import Enum
from itertools import cycle

import pygame

from ..utils import clamp, pixel_collision
from .floor import Floor
from .pipe import Pipe, Pipes
from .sprite import Sprite


class PlayerMode(Enum):
SHM = "SHM"
NORMAL = "NORMAL"
CRASH = "CRASH"


class Player(Sprite):
def setup(self) -> None:
self.img_idx = 0
self.img_gen = cycle([0, 1, 2, 1])
self.frame = 0
self.crashed = False
self.crash_entity = None
self.width = self.images.player[0].get_width()
self.height = self.images.player[0].get_height()
self.reset_pos()
self.set_mode(PlayerMode.SHM)

def set_mode(self, mode: PlayerMode) -> None:
self.mode = mode
if mode == PlayerMode.NORMAL:
self.reset_vals_normal()
self.sounds.wing.play()
elif mode == PlayerMode.SHM:
self.reset_vals_shm()
elif mode == PlayerMode.CRASH:
self.sounds.hit.play()
if self.crash_entity == "pipe":
self.sounds.die.play()
self.reset_vals_crash()

def reset_pos(self) -> None:
self.x = int(self.window.width * 0.2)
self.y = int(
(self.window.height - self.images.player[0].get_height()) / 2
)
self.mid_x = self.x + self.width / 2
self.mid_y = self.y + self.height / 2

def reset_vals_normal(self) -> None:
self.vel_y = -9 # player's velocity along Y axis
self.max_vel_y = 10 # max vel along Y, max descend speed
self.min_vel_y = -8 # min vel along Y, max ascend speed
self.acc_y = 1 # players downward acceleration

self.rot = 45 # player's current rotation
self.vel_rot = -3 # player's rotation speed
self.rot_min = -90 # player's min rotation angle
self.rot_max = 20 # player's max rotation angle

self.flap_acc = -9 # players speed on flapping
self.flapped = False # True when player flaps

def reset_vals_shm(self) -> None:
self.vel_y = 1 # player's velocity along Y axis
self.max_vel_y = 4 # max vel along Y, max descend speed
self.min_vel_y = -4 # min vel along Y, max ascend speed
self.acc_y = 0.5 # players downward acceleration

self.rot = 0 # player's current rotation
self.vel_rot = 0 # player's rotation speed
self.rot_min = 0 # player's min rotation angle
self.rot_max = 0 # player's max rotation angle

self.flap_acc = 0 # players speed on flapping
self.flapped = False # True when player flaps

def reset_vals_crash(self) -> None:
self.acc_y = 2
self.vel_y = 7
self.max_vel_y = 15

def update_img_idx(self):
self.frame += 1
if self.frame % 5 == 0:
self.img_idx = next(self.img_gen)

def tick_shm(self) -> None:
if self.vel_y >= self.max_vel_y or self.vel_y <= self.min_vel_y:
self.acc_y *= -1
self.vel_y += self.acc_y
self.y += self.vel_y

self.mid_x = self.x + self.width / 2
self.mid_y = self.y + self.height / 2

def tick_normal(self) -> None:
if self.vel_y < self.max_vel_y and not self.flapped:
self.vel_y += self.acc_y
if self.flapped:
self.flapped = False

self.y += min(
self.vel_y, self.window.play_area_height - self.y - self.height
)

self.mid_x = self.x + self.width / 2
self.mid_y = self.y + self.height / 2

def tick_crash(self) -> None:
if self.y + self.height < self.window.play_area_height - 1:
self.y += min(
self.vel_y, self.window.play_area_height - self.y - self.height
)

# player velocity change
if self.vel_y < self.max_vel_y:
self.vel_y += self.acc_y

# rotate only when it's a pipe crash
if self.crash_entity != "floor":
self.rotate()

def rotate(self) -> None:
self.rot = clamp(self.rot + self.vel_rot, self.rot_min, self.rot_max)

def tick(self) -> None:
self.update_img_idx()
if self.mode == PlayerMode.SHM:
self.tick_shm()
elif self.mode == PlayerMode.NORMAL:
self.tick_normal()
self.rotate()
elif self.mode == PlayerMode.CRASH:
self.tick_crash()

self.draw_player()

def draw_player(self) -> None:
player_surface = pygame.transform.rotate(
self.images.player[self.img_idx], self.rot
)
self.screen.blit(player_surface, (self.x, self.y))

def flap(self) -> None:
self.vel_y = self.flap_acc
self.flapped = True
self.rot = 45
self.sounds.wing.play()

def crossed(self, pipe: Pipe) -> bool:
return pipe.mid_x <= self.mid_x < pipe.mid_x + 4

def collided(self, pipes: Pipes, floor: Floor) -> bool:
"""returns True if player collides with base or pipes."""

# if player crashes into ground
if self.y + self.height >= floor.y - 1:
self.crashed = True
self.crash_entity = "floor"
return True
else:
p_rect = pygame.Rect(self.x, self.y, self.width, self.height)

for u_pipe, l_pipe in zip(pipes.upper, pipes.lower):
# upper and lower pipe rects
u_pipe_rect = pygame.Rect(
u_pipe.x, u_pipe.y, u_pipe.width, u_pipe.height
)
l_pipe_rect = pygame.Rect(
l_pipe.x, l_pipe.y, l_pipe.width, l_pipe.height
)

# player and upper/lower pipe hitmasks
p_hit_mask = self.hit_mask.player[self.img_idx]
u_hit_mask = self.hit_mask.pipe[0]
l_hit_mask = self.hit_mask.pipe[1]

# if bird collided with upipe or lpipe
u_collide = pixel_collision(
p_rect, u_pipe_rect, p_hit_mask, u_hit_mask
)
l_collide = pixel_collision(
p_rect, l_pipe_rect, p_hit_mask, l_hit_mask
)

if u_collide or l_collide:
self.crashed = True
self.crash_entity = "pipe"
return True

return False
Loading

0 comments on commit 53cea01

Please sign in to comment.