Skip to content

Commit 1c6a41f

Browse files
authored
Merge pull request #55 from RedLion8399/feature/move-obstacles
Feature/move obstacles
2 parents 44347d5 + 7b20c39 commit 1c6a41f

File tree

5 files changed

+195
-15
lines changed

5 files changed

+195
-15
lines changed

src/counter.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ def __init__(self) -> None:
3838
self.frames: int = 0
3939
self.highscore: int = self.__load_highscore()
4040

41+
self._cactus_counter: int = 0
42+
self._cloud_counter: int = 0
43+
4144
def save_highscore(self) -> None:
4245
"""This method saves the current highscore to a file.
4346
@@ -66,6 +69,24 @@ def tick(self) -> None:
6669
"""Increase the frame counter by one every time this method is called."""
6770
self.frames += 1
6871

72+
def reset_cactus_counter(self) -> None:
73+
"""Set the current frame number when a new cactus is generated."""
74+
self._cactus_counter = self.frames
75+
76+
def reset_cloud_counter(self) -> None:
77+
"""Set the current frame number when a new cloud is generated."""
78+
self._cloud_counter = self.frames
79+
80+
@property
81+
def cloud_counter(self) -> int:
82+
"""Return the current frame number when a new cloud is generated."""
83+
return self.frames - self._cloud_counter
84+
85+
@property
86+
def cactus_counter(self) -> int:
87+
"""Return the current frame number when a new cactus is generated."""
88+
return self.frames - self._cactus_counter
89+
6990
@property
7091
def score(self) -> int:
7192
"""This method returns the current score of the player."""
@@ -85,3 +106,16 @@ def dino_running_status(self) -> bool:
85106
and can be difined by the user.
86107
"""
87108
return self.frames % 20 < 10
109+
110+
@property
111+
def bird_animation_status(self) -> bool:
112+
"""Returns the animation status of the bird.
113+
114+
While the bird is flying it has to different immages
115+
to change between in order to create a flying animation.
116+
The animation state changes every 40 frames.
117+
118+
Returns:
119+
bool: The aimaton has only two states so it can be displayed
120+
"""
121+
return self.frames % 40 < 20

src/dino.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def __init__(self) -> None:
5656

5757
super().__init__(self.DEFAULT_POSITION[0], self.DEFAULT_POSITION[1])
5858

59+
self.OBJECT_SPEED = 0
5960
self.running_image: tuple[list[pg.Surface], pg.Rect]
6061
self.sneaking_image: tuple[list[pg.Surface], pg.Rect]
6162
self.load_images()
@@ -151,20 +152,16 @@ def load_images(self) -> None:
151152
load_image("dino_sneaking.png")[0], (2, 1)
152153
)
153154

154-
def update(self, speed: float = 0) -> None:
155+
def update(self) -> None:
155156
"""Update the complete dino in the game.
156157
157158
This function updates the position of the dino in the game
158159
and controlls it's animation status.
159-
160-
Args:
161-
speed: The speed of the dino. The dino does not move so it is set to 0
162-
without any need to change it.
163160
"""
164161
if self.status == Status.RUNNING:
165162
self._run()
166163
if self.status == Status.JUMPING:
167164
self._jump()
168165
if self.status == Status.SNEAKING:
169166
self._sneak()
170-
super().update(0)
167+
super().update()

src/main.py

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77

88
# pylint: disable=no-member
99

10+
import random as rd
1011
import sys
1112

1213
import pygame as pg
1314

1415
from config import ColorTheme, config
1516
from counter import Counter
1617
from dino import Dino
17-
from obstacles import GameElement
18+
from obstacles import Bird, Cactus, Cloud, GameElement, Ground
1819

1920

2021
def main() -> None:
@@ -31,10 +32,15 @@ def main() -> None:
3132
config.frame_rate = 60
3233
config.init_screen()
3334

34-
obstacles: list[GameElement] = []
35+
game_objects: pg.sprite.Group[GameElement] = pg.sprite.Group()
36+
obstacle_list: list[GameElement] = []
37+
obstacle_group: pg.sprite.Group[GameElement] = pg.sprite.Group()
38+
cloud_list: list[GameElement] = []
39+
cloud_group: pg.sprite.Group[GameElement] = pg.sprite.Group()
3540

3641
counter: Counter = Counter()
3742
dino: Dino = Dino()
43+
ground: Ground = Ground()
3844

3945
def get_input() -> None:
4046
"""This function gets the input from the user.
@@ -55,13 +61,83 @@ def game_over() -> None:
5561
def update() -> None:
5662
"""Update all grafic representations of the game elements."""
5763
dino.update()
64+
ground.update()
65+
cloud_group.update()
66+
obstacle_group.update()
67+
game_objects.update()
68+
69+
def update_obstacles(obstacles: list[GameElement]) -> list[GameElement]:
70+
"""Update all grafic representations of the game elements.
71+
72+
Checks if the obstacles are still on the screen and
73+
removes them if they are not from the list with possible collisions.
74+
"""
75+
return [obstacle for obstacle in obstacles if obstacle.alive()] # type: ignore
76+
77+
def update_clouds(clouds: list[GameElement]) -> list[GameElement]:
78+
"""Update all grafic representations of the cloud elements.
79+
80+
Checks if the clouds are still on the screen and
81+
removes them if they are not from the list with possible collisions.
82+
"""
83+
return [cloud for cloud in clouds if cloud.alive()]
84+
85+
def spawn_objects() -> None:
86+
"""Spawn a new objects on the screen.
87+
88+
Several different object types exist and are spawned.
89+
1. Obstacles
90+
2. Ground
91+
3. Clouds
92+
93+
The number of obstacles that can exist paart of the game
94+
can go up to 3 but only with a certain probability.
95+
96+
The ground is always present.
97+
98+
Clouds can also exist up to 3 but they are counted
99+
separately from the obstacles.
100+
"""
101+
102+
# Obstacles
103+
if len(obstacle_list) < 3:
104+
# Only spawn obstacles every 20 frames
105+
if counter.cactus_counter > 20:
106+
# Chose between cactus and bird
107+
# Cacti are double the chance than birds
108+
match rd.randint(0, 400):
109+
case 0 | 1 | 2 | 3:
110+
obstacle_list.append(Cactus())
111+
case 4:
112+
obstacle_list.append(Bird())
113+
case _:
114+
pass
115+
116+
for obstacle in obstacle_list:
117+
obstacle_group.add(obstacle)
118+
119+
# Clouds
120+
if len(cloud_list) < 3:
121+
# Only spawn clouds every 100 frames
122+
if counter.cloud_counter > 100:
123+
if not rd.randint(0, 1000):
124+
cloud_list.append(Cloud())
125+
126+
for cloud in cloud_list:
127+
cloud_group.add(cloud)
128+
129+
# Ground
130+
game_objects.add(ground)
58131

59132
while True:
133+
obstacle_list = update_obstacles(obstacle_list)
134+
cloud_list = update_clouds(cloud_list)
135+
spawn_objects()
60136
get_input()
61137
counter.tick()
62138
update()
63139

64-
if dino.check_collision(obstacles):
140+
if dino.check_collision(obstacle_list):
65141
break
66142

67143
pg.display.flip()

src/obstacles.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
the basic elements of the game.
44
"""
55

6+
# pylint: disable=invalid-name
7+
68
import random as rd
79

810
import pygame as pg
@@ -25,16 +27,20 @@ def __init__(self, x_position: float, y_position: float) -> None:
2527
self.rect: pg.Rect
2628
self.current_image: pg.Surface
2729
self.counter: Counter = Counter()
30+
self.OBJECT_SPEED: float = config.object_speed
2831

29-
def update(self, speed: float = config.object_speed) -> None:
32+
def update(self) -> None:
3033
"""Update the position of the element in the game."""
31-
self.move(speed)
34+
self.move()
3235
self.rect.update((self.x_position, self.y_position), self.rect.size)
3336
config.window.blit(self.current_image, self.rect)
3437

35-
def move(self, speed: float) -> None:
38+
if self.rect.right <= 0:
39+
self.kill()
40+
41+
def move(self) -> None:
3642
"""Move the element in the game."""
37-
self.x_position -= speed
43+
self.x_position -= self.OBJECT_SPEED
3844

3945

4046
class Cactus(GameElement):
@@ -78,6 +84,18 @@ def __init__(self) -> None:
7884
self.image = seperate_images(load_image("birds.png")[0], (2, 1))
7985
self.rect = self.image[1]
8086

87+
def update(self) -> None:
88+
"""Updates the whole Bird element in the game.
89+
90+
Bird has two different immages to change between in order to create
91+
a flying animation. The animation state changes every 40 frames.
92+
"""
93+
if self.counter.bird_animation_status:
94+
self.current_image = self.image[0][0]
95+
else:
96+
self.current_image = self.image[0][1]
97+
super().update()
98+
8199

82100
class Cloud(GameElement):
83101
"""This class represents a Cloud element in the game.
@@ -88,6 +106,7 @@ class Cloud(GameElement):
88106
def __init__(self) -> None:
89107
super().__init__(config.display_scale[0], rd.randint(50, 100))
90108
self.current_image, self.rect = load_image("cloud.png")
109+
self.OBJECT_SPEED = 1
91110

92111

93112
class Ground(GameElement):
@@ -105,3 +124,29 @@ def __init__(self) -> None:
105124

106125
self.immage_1, self.rect_1 = load_image("ground.png")
107126
self.immage_2, self.rect_2 = load_image("ground.png")
127+
128+
self.rect_1.bottomleft = (0, config.display_scale[1])
129+
self.rect_2.bottomleft = (self.rect_1.right, config.display_scale[1])
130+
131+
def update(self) -> None:
132+
"""Move the ground in the game.
133+
134+
The ground moves as every other element in the game.
135+
Different from the other elements, the ground exists
136+
at every time of the game.
137+
That means two different ground images are needed wich are
138+
swiched every time one reaches the end.
139+
If the right end of on of them cross the window border,
140+
the other image is atteched at it's right side.
141+
"""
142+
if self.rect_1.right <= 0:
143+
self.rect_1.left = self.rect_2.right
144+
145+
if self.rect_2.right <= 0:
146+
self.rect_2.left = self.rect_1.right
147+
148+
self.rect_1.move_ip(-self.OBJECT_SPEED, 0)
149+
self.rect_2.move_ip(-self.OBJECT_SPEED, 0)
150+
151+
config.window.blit(self.immage_1, self.rect_1)
152+
config.window.blit(self.immage_2, self.rect_2)

tests/test_obstacles.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,37 @@ def test_init(self):
2323

2424
def test_move(self):
2525
game_element = GameElement(0, 0)
26-
game_element.move(9)
26+
game_element.OBJECT_SPEED = 9
27+
game_element.move()
2728
self.assertEqual(game_element.x_position, -9)
2829

2930
def test_update(self):
3031
game_element = GameElement(0, 0)
3132
game_element.current_image = pg.Surface((100, 100))
3233
game_element.rect = pg.Rect(0, 0, 100, 100)
33-
game_element.update(5)
34+
game_element.OBJECT_SPEED = 5
35+
game_element.update()
3436
self.assertEqual(game_element.x_position, -5)
3537
self.assertEqual(game_element.y_position, 0)
3638

39+
def test_update_kill(self):
40+
game_element = GameElement(0, 0)
41+
elements: pg.sprite.Group[GameElement] = pg.sprite.Group() # type: ignore
42+
elements.add(game_element)
43+
game_element.current_image = pg.Surface((100, 100))
44+
game_element.rect = pg.Rect(0, 0, 100, 100)
45+
game_element.update()
46+
self.assertEqual(game_element.alive(), False)
47+
48+
def test_update_alive(self):
49+
game_element = GameElement(100, 0)
50+
elements: pg.sprite.Group[GameElement] = pg.sprite.Group() # type: ignore
51+
elements.add(game_element)
52+
game_element.current_image = pg.Surface((100, 100))
53+
game_element.rect = pg.Rect(0, 0, 100, 100)
54+
game_element.update()
55+
self.assertEqual(game_element.alive(), True)
56+
3757

3858
class TestCactus(unittest.TestCase):
3959
def setUp(self) -> None:
@@ -79,6 +99,14 @@ def test_init_images(self):
7999
bird = Bird()
80100
self.assertIsInstance(bird.image, tuple)
81101

102+
def test_bird_update_animation(self):
103+
bird = Bird()
104+
bird.update()
105+
self.assertIs(bird.current_image, bird.image[0][0])
106+
bird.counter.frames = 22
107+
bird.update()
108+
self.assertIs(bird.current_image, bird.image[0][1])
109+
82110

83111
class TestCloud(unittest.TestCase):
84112
def setUp(self) -> None:

0 commit comments

Comments
 (0)