Skip to content

Commit f0a5cc0

Browse files
authored
Major refactoring of Engine object code
1 parent bb4d23a commit f0a5cc0

File tree

7 files changed

+180
-89
lines changed

7 files changed

+180
-89
lines changed

Engine.py

Lines changed: 171 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -3,133 +3,216 @@
33
from dataclasses import dataclass
44
from lib.terminal_graphics import Terminal, ScreenData, Coords
55
from lib.sprites import Sprites
6+
from lib.parameters import MagicNumbers
67
import keyboard
78

8-
GROUND_WIDTH = 80
9-
109
class Ground():
1110
def __init__(self, screen: ScreenData, width: int, pos: Coords):
12-
super().__init__()
11+
super().__init__() # I don't know why this is here, but it's behavior looks good to me
1312
self.width = width
14-
self.sprite = Sprites.GROUND_SPRITE * self.width
1513
self.pos = pos
16-
self.xpos = pos.xpos
17-
self.ypos = pos.ypos
14+
self.xpos = pos.xpos # xpos short for X position
15+
self.ypos = pos.ypos # ypos short for Y position
1816
self.screen = screen
17+
18+
def place_ground(self, xpos: int, ypos: int):
19+
"""Place a ground block in each coordinate.
20+
21+
Args:
22+
xpos (int): The x position of the ground block.
23+
ypos (int): The y position of the ground block.
24+
25+
Returns:
26+
None"""
27+
# Place a ground block in each coordinate, reads as the following:
28+
# For each number in the range of the width: <--+
29+
# Place a ground block in coordnates: | number
30+
# X = Ground's root x position + the number from the for loop ---+
31+
for number in range(self.width):
32+
Terminal.place_sprite(
33+
self.screen,
34+
Sprites.GROUND_SPRITE,
35+
Coords(xpos + number, ypos)
36+
)
37+
38+
def get_collision_coords(self):
39+
"""
40+
Get the coordinates for collision detection with the ground.
41+
42+
Returns:
43+
List[Coords]: A list of coordinates representing the ground.
44+
"""
45+
# Initialize an empty list to hold the collision coordinates
46+
collision_coords = []
47+
48+
# Iterate over the range of the width
49+
for number in range(self.width):
50+
# Calculate the x coordinate for each number and add it to the list
51+
# The x coordinate is the ground's root x position plus the number
52+
collision_coords.append(Coords(self.xpos + number, self.ypos))
53+
54+
# Return the list of collision coordinates
55+
return collision_coords
1956

2057
def render(self):
21-
for i in range(self.width):
22-
self.screen[Coords(self.xpos + i, self.ypos)] = Sprites.GROUND_SPRITE
23-
collision_coords = [Coords(self.xpos + i, self.ypos) for i in range(self.width)]
58+
# Place ground tiles on the screen
59+
self.place_ground(self.xpos, self.ypos)
60+
collision_coords = self.get_collision_coords()
61+
2462
return self.screen, collision_coords
2563

26-
class Coin():
64+
# Please note that refactoring the other classes to inherit from another thing may be
65+
# bad for the program length, despite doing the exact same thing.
66+
class Coin(): # Also known as the Mother of All Objects
2767
def __init__(self, screen: ScreenData, pos: Coords):
68+
# Initialize the position of the coin
2869
self.pos = pos
70+
# Initialize the position of the coin one row above the current position
2971
self.secondary_pos = Coords(pos.xpos, pos.ypos - 1)
72+
# Initialize the screen to render the coin on
3073
self.screen = screen
74+
# Initialize a flag to indicate if the coin is hidden
3175
self.hide = False
76+
# Initialize a flag to indicate if the coin has been broken
3277
self.broken = False
78+
# Initialize a flag to indicate if the coin has been updated
3379
self.updated = False
80+
# Initialize a flag to indicate if the coin has been killed
3481
self.killed = False
3582

36-
def hide_coin(self):
37-
self.hide = True
38-
39-
def __del__(self):
40-
if not self.updated:
41-
self.screen[self.pos] = ' ' # Clear the coin's position from the screen
42-
self.updated = True
43-
self.broken, self.killed = True, True
44-
else: pass
83+
def hide_coin(self): self.hide = True
84+
# R.I.P __del__() dunder method, didn't even get used in the 1st place
4585

4686
def render(self, previous_pos: Coords = None):
47-
new_screen: ScreenData = {
48-
self.pos: Sprites.COIN_SPRITE if not self.hide else ' ' # 'C' represents the coin
49-
}
50-
self.screen.update(new_screen)
87+
# Determine the sprite to render based on the hide flag
88+
sprite_to_render = Sprites.COIN_SPRITE if not self.hide else ' '
89+
# Place the sprite on the screen at the current position
90+
Terminal.place_sprite(self.screen, sprite_to_render, self.pos)
91+
# Return the updated screen data
5192
return self.screen
5293

5394
class Powerup(Coin):
5495
def render(self, previous_pos: Coords = None):
55-
new_screen: ScreenData = {
56-
self.pos: Sprites.POWERUP_BLOCK_SPRITE if not self.hide else Sprites.HIT_POWERUP_BLOCK_SPRITE # '?' represents the powerup block
57-
}
58-
self.screen.update(new_screen)
96+
# Determine the sprite to render based on the hide flag
97+
if self.hide: sprite_to_render = Sprites.HIT_POWERUP_BLOCK_SPRITE # Hide block has been hit
98+
else: sprite_to_render = Sprites.POWERUP_BLOCK_SPRITE # Hide block has not been hit
99+
# If hide flag is in an unknown state
100+
if sprite_to_render is None: sprite_to_render = Sprites.UNKNOWN_BLOCK_SPRITE # This should never happen but just in case
101+
102+
# Render the sprite on the screen at the current position
103+
Terminal.place_sprite(self.screen, sprite_to_render, self.pos)
104+
# Return the updated screen data
59105
return self.screen
60106

61107
class StompableEnemy(Coin):
62108
def __init__(self, screen: ScreenData, pos: Coords):
109+
# Call the __init__ method of the parent class (Coin)
110+
# Pass in the screen and position arguments
63111
super().__init__(screen, pos)
112+
# Set the killed flag to False
64113
self.killed = False
65-
self.sidepos = (
66-
Coords(pos.xpos - 1, pos.ypos),
67-
Coords(pos.xpos + 1, pos.ypos),
68-
)
114+
115+
# Create a tuple of Coords objects representing the enemy's sides
116+
# These Coords objects are one space to the left and right of the enemy's current position
117+
118+
# Create a Coords object representing the enemy's secondary position
119+
# This is the enemy's position one row above its current position
69120
self.secondary_pos = Coords(pos.xpos, pos.ypos - 1)
121+
122+
# Store the previous position of the enemy
70123
self.previous_pos = pos
71124

72125
def move_towards_player(self, player_pos: Coords):
73-
if self.killed:
74-
return # Do not move if the enemy is killed
126+
if self.killed: return # Do not move if the enemy is killed
75127

76-
new_xpos = self.pos.xpos
77-
if self.pos.xpos < player_pos.xpos:
78-
new_xpos += 1
79-
elif self.pos.xpos > player_pos.xpos:
80-
new_xpos -= 1
81-
128+
new_xpos = self.calculate_new_xpos(player_pos) # Calculate the new position of the enemy
82129
new_ypos = self.pos.ypos
130+
83131
self.previous_pos = self.pos
84132
self.pos = Coords(new_xpos, new_ypos)
85-
self.sidepos = (
86-
Coords(new_xpos - 1, new_ypos),
87-
Coords(new_xpos + 1, new_ypos),
88-
)
133+
self.sidepos = self.get_side_positions(self.pos)
89134
self.secondary_pos = Coords(new_xpos, new_ypos - 1)
90135

91-
def render(self, previous_pos: Coords = None):
92-
new_screen: ScreenData = {}
136+
def render(self):
137+
# Determine the sprite to render based on the hide/kill flags
138+
sprite_to_use = Sprites.ENEMY1_SPRITE if (not self.hide) or (not self.killed) else ' '
93139

94-
if not self.hide:
95-
new_screen[self.pos] = Sprites.ENEMY1_SPRITE
96-
if self.previous_pos != self.pos:
97-
new_screen[self.previous_pos] = ' ' # Clear the previous position
98-
elif self.hide or self.killed:
99-
if not self.updated:
100-
new_screen[self.pos] = ' '
101-
self.updated = True
102-
self.screen.update(new_screen)
140+
# Render the enemy on the screen at its current position
141+
Terminal.place_sprite(self.screen, sprite_to_use, self.pos)
142+
143+
# If the enemy has moved to a new position, clear its previous position
144+
if self.previous_pos != self.pos:
145+
Terminal.place_sprite(self.screen, ' ', self.previous_pos)
146+
# If the enemy is hidden or killed, clear its current position
147+
if self.hide or self.killed: Terminal.place_sprite(self.screen, ' ', self.pos)
148+
149+
# Return the updated screen data
103150
return self.screen
104151

152+
def get_side_positions(self, pos: Coords):
153+
sidepos = (
154+
Coords(pos.xpos - 1, pos.ypos), # Left side
155+
Coords(pos.xpos + 1, pos.ypos), # Right side
156+
)
157+
return sidepos
158+
159+
def calculate_new_xpos(self, player_pos: Coords):
160+
# Calculate the new position of the enemy
161+
# If the enemy is on the left side of the player, move it to the right
162+
# If the enemy is on the right side of the player, move it to the left
163+
# If the enemy is already at the player's position, keep its position
164+
new_xpos = self.pos.xpos
165+
if self.pos.xpos < player_pos.xpos:
166+
new_xpos += 1
167+
elif self.pos.xpos > player_pos.xpos:
168+
new_xpos -= 1
169+
return new_xpos
170+
105171
class Fireball(Coin):
106172
def __init__(self, screen: ScreenData, pos: Coords, direction: int, enemies: tuple[StompableEnemy]):
173+
# Initialize the Fireball object with the necessary parameters
107174
super().__init__(screen, pos)
108-
self.direction = direction # Store the direction of the fireball
109-
self.creation_time = time.time() # Record the creation time of the fireball
175+
# Store the direction of the fireball
176+
self.direction = direction
177+
# Set the hit flag to False
110178
self.hit = False
111-
self.old_pos = None # Initialize old position as None
179+
# Initialize old position as None
180+
self.old_pos = None
181+
# Store the enemies that the fireball can collide with
112182
self.enemies = enemies
113183

114184
def render(self, previous_pos: Coords = None):
115-
new_screen: ScreenData = {}
116-
if self.hit:
117-
new_screen[self.pos] = ' '
118-
elif not self.old_pos:
119-
new_screen[self.pos] = Sprites.FIREBALL_RIGHT_SPRITE if self.direction == 1 else Sprites.FIREBALL_LEFT_SPRITE # Render the fireball at its current position
120-
elif self.pos != self.old_pos:
121-
new_screen[self.pos] = Sprites.FIREBALL_RIGHT_SPRITE if self.direction == 1 else Sprites.FIREBALL_LEFT_SPRITE # Render the fireball at its new position
122-
new_screen[self.old_pos] = ' ' # Clear the fireball's previous position
123-
if self.pos == self.old_pos: new_screen[self.pos] = ' ' # Clear the fireball's current position
124-
if self.pos.xpos == 0: new_screen[self.pos] = ' '
125-
if self.pos.xpos == (GROUND_WIDTH - 1): new_screen[self.pos] = ' '
185+
# Render the fireball on the screen at its current position
186+
Terminal.place_sprite(self.screen, Sprites.FIREBALL_RIGHT_SPRITE if self.direction == 1 else Sprites.FIREBALL_LEFT_SPRITE, self.pos)
187+
188+
# If the fireball has hit something, clear its position
189+
if self.hit: Terminal.place_sprite(self.screen, ' ', self.pos)
190+
191+
# If the fireball has moved to a new position, clear its previous position
192+
elif self.old_pos and self.pos != self.old_pos:
193+
Terminal.place_sprite(self.screen, ' ', self.old_pos)
194+
Terminal.place_sprite(self.screen, Sprites.FIREBALL_RIGHT_SPRITE if self.direction == 1 else Sprites.FIREBALL_LEFT_SPRITE, self.pos)
195+
196+
# If the fireball is at its old position, clear its position
197+
elif self.pos == self.old_pos:
198+
Terminal.place_sprite(self.screen, ' ', self.pos)
199+
200+
# If the fireball is at the edge of the screen, clear its position
201+
if self.pos.xpos == 0:
202+
Terminal.place_sprite(self.screen, ' ', self.pos)
203+
if self.pos.xpos == (MagicNumbers.GROUND_WIDTH - 1):
204+
Terminal.place_sprite(self.screen, ' ', self.pos)
205+
206+
# Check for collisions with enemies
126207
for enemy in self.enemies:
127208
for pos in enemy.sidepos:
128209
if (pos == self.pos) and (not enemy.killed):
129210
self.hit = True
130-
new_screen[pos] = ' '
211+
enemy.killed = True
212+
Terminal.place_sprite(self.screen, ' ', enemy.pos)
213+
Terminal.place_sprite(self.screen, ' ', pos)
131214

132-
new_screen[Coords(5, 1)] = f'Last fireball position: {self.pos.xpos}, {self.pos.ypos}'
215+
new_screen = {Coords(5, 1): f'Last fireball position: {self.pos.xpos}, {self.pos.ypos}'}
133216
self.screen.update(new_screen)
134217
return self.screen
135218

@@ -143,30 +226,29 @@ def next_pos(self):
143226
self.pos = Coords(new_xpos, new_ypos) # Create a new Coords instance with the updated position
144227
self.secondary_pos = Coords(new_xpos, new_ypos - 1) # Update secondary position accordingly
145228

146-
# Check collision with ground border
147-
if self.pos.xpos >= (GROUND_WIDTH) or self.pos.xpos < 0:
229+
return self.ground_border_collision_check()
230+
231+
def ground_border_collision_check(self):
232+
if self.pos.xpos >= (MagicNumbers.GROUND_WIDTH) or self.pos.xpos < 0:
148233
self.screen[self.old_pos] = ' ' # Clear the character on the old position
149234
return True # Indicate collision with ground border
150-
return False # Indicate no collision
151-
152-
def check_lifetime(self):
153-
# Check if the fireball has existed for more than 3 seconds
154-
return time.time() - self.creation_time > 3
235+
return False
155236

156237
class Brick(Coin):
157238
def render(self, previous_pos: Coords = None):
158-
new_screen: ScreenData = {}
239+
"""
240+
Render the brick on the screen at its current position.
241+
"""
242+
# If the brick is not hidden, render it
159243
if not self.hide:
160-
new_screen: ScreenData = {
161-
self.pos: Sprites.BRICK_SPRITE
162-
}
244+
Terminal.place_sprite(self.screen, Sprites.BRICK_SPRITE, self.pos)
245+
# If the brick is hidden or broken, clear its position
163246
elif self.hide or self.broken:
247+
# If the brick has not been updated yet, clear its position
164248
if not self.updated:
165-
new_screen: ScreenData = {
166-
self.pos: ' '
167-
}
249+
Terminal.place_sprite(self.screen, ' ', self.pos)
168250
self.updated = True
169-
self.screen.update(new_screen)
251+
# Return the updated screen data
170252
return self.screen
171253

172254
class Player():
@@ -198,7 +280,7 @@ def __init__(
198280
for enemy in self.enemies: self.enemy_coords.append(enemy.pos)
199281

200282
self.coins_collected: int = 0
201-
self.powerstate = 0
283+
self.powerstate = MagicNumbers.STARTING_POWERSTATE
202284
self.fire_cooldown = 0
203285

204286
class Left: pass
@@ -265,7 +347,8 @@ def update_position(self):
265347
if powcoord == Coords(self.pos.xpos, new_ypos):
266348
new_ypos = self.pos.ypos + 1
267349
self.velocity_y = 0
268-
self.powerstate += 1 if not self.powerups[id].hide else 0
350+
if (not self.powerups[id].hide) and (self.powerstate < 2):
351+
self.powerstate += 1
269352
self.powerups[id].hide_coin()
270353
elif brick_collision:
271354
for id, brkcoord in enumerate(self.brick_coords):
@@ -334,7 +417,7 @@ def enemy_check(self, enemies: tuple[StompableEnemy]):
334417
def main():
335418
screen: ScreenData = {}
336419

337-
level_ground = Ground(screen, GROUND_WIDTH, Coords(0, 10))
420+
level_ground = Ground(screen, MagicNumbers.GROUND_WIDTH, Coords(0, 10))
338421
screen.update(level_ground.render()[0])
339422

340423
coins: tuple = (
480 Bytes
Binary file not shown.
1.09 KB
Binary file not shown.
4.1 KB
Binary file not shown.

lib/parameters.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class MagicNumbers:
2+
GROUND_WIDTH = 80
3+
STARTING_POWERSTATE = 2

lib/sprites.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
class Sprites:
22
GROUND_SPRITE = '\033[48;2;222;73;4m\033[38;2;97;49;0m▒\033[0m'
33
POWERUP_BLOCK_SPRITE = '\033[48;2;255;197;39m\033[38;2;255;255;255m?\033[0m'
4+
UNKNOWN_BLOCK_SPRITE = '\033[48;2;255;197;39m\033[38;2;255;255;255mX\033[0m'
45
HIT_POWERUP_BLOCK_SPRITE = '\033[48;2;255;123;31m \033[0m'
56
BRICK_SPRITE = '\033[48;2;28;28;28m\033[38;2;222;75;3m▒\033[0m'
67
COIN_SPRITE = '\033[38;2;255;197;38m$\033[0m'

lib/terminal_graphics.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,8 @@ def update_screen(sd: ScreenData):
4747
key: Coords; value: str
4848
Terminal.move_cursor(key.xpos + 1, key.ypos + 1) # +1 to account for 1-indexed cursor positions
4949
sys.stdout.write(value)
50-
sys.stdout.flush()
50+
sys.stdout.flush()
51+
52+
@staticmethod
53+
def place_sprite(sd: ScreenData, sprite: str, pos: Coords): # Function created for readability
54+
sd.update({pos: sprite}) # Place the sprite at the given position

0 commit comments

Comments
 (0)