Add the CarManager and car-shaped cars
Task: MIL-004#1 Task: MIL-004#2 Task: MIL-004#3 Task: MIL-004#4
This commit is contained in:
+53
-1
@@ -2,6 +2,58 @@
|
||||
@brief Creates and moves the cars.
|
||||
"""
|
||||
|
||||
import random
|
||||
from collections.abc import Callable
|
||||
from turtle import Turtle
|
||||
|
||||
from car_shape import shape_name
|
||||
from constants import (
|
||||
CAR_REMOVE_X,
|
||||
CAR_START_X,
|
||||
CAR_Y_LIMIT,
|
||||
COLORS,
|
||||
MOVE_INCREMENT,
|
||||
SPAWN_ODDS,
|
||||
STARTING_MOVE_DISTANCE,
|
||||
)
|
||||
|
||||
|
||||
class CarManager:
|
||||
"""@brief Placeholder, implemented in milestone MIL-004."""
|
||||
"""@brief Keeps the cars of the road: creates, moves and removes them."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
car_factory: Callable[[], Turtle] = Turtle,
|
||||
rng: random.Random | None = None,
|
||||
) -> None:
|
||||
"""@brief Create a manager with no cars.
|
||||
|
||||
@param car_factory Creates one car; replaced by a fake in tests.
|
||||
@param rng Random source; replaced by a seeded one in tests.
|
||||
"""
|
||||
self._car_factory = car_factory
|
||||
self._rng = rng or random.Random()
|
||||
self.all_cars: list[Turtle] = []
|
||||
self.move_distance = STARTING_MOVE_DISTANCE
|
||||
|
||||
def create_cars(self) -> None:
|
||||
"""@brief Create a car on one call in SPAWN_ODDS, at a random lane."""
|
||||
if self._rng.randint(1, SPAWN_ODDS) != 1:
|
||||
return
|
||||
car = self._car_factory()
|
||||
car.penup()
|
||||
car.shape(shape_name(self._rng.choice(COLORS)))
|
||||
car.goto(CAR_START_X, self._rng.randint(-CAR_Y_LIMIT, CAR_Y_LIMIT))
|
||||
self.all_cars.append(car)
|
||||
|
||||
def move_cars(self) -> None:
|
||||
"""@brief Move every car left and remove the ones that left the screen."""
|
||||
for car in self.all_cars:
|
||||
car.setx(car.xcor() - self.move_distance)
|
||||
for car in [car for car in self.all_cars if car.xcor() < CAR_REMOVE_X]:
|
||||
car.hideturtle()
|
||||
self.all_cars.remove(car)
|
||||
|
||||
def level_up(self) -> None:
|
||||
"""@brief Make the cars faster."""
|
||||
self.move_distance += MOVE_INCREMENT
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""@file car_shape.py
|
||||
@brief The drawing of a car, so that cars look like cars and not colour blocks.
|
||||
|
||||
A car is a compound turtle shape inside a 40 x 20 pixel footprint, front to the
|
||||
left: lower body, cabin, two windows, a headlight and two wheels. The geometry
|
||||
is plain data (see car_components) so it can be tested without a display.
|
||||
"""
|
||||
|
||||
from turtle import Shape, TurtleScreen
|
||||
|
||||
from constants import (
|
||||
CAR_GLASS_COLOR,
|
||||
CAR_LIGHT_COLOR,
|
||||
CAR_OUTLINE_COLOR,
|
||||
CAR_TYRE_COLOR,
|
||||
COLORS,
|
||||
)
|
||||
|
||||
## One polygon of a car: its points, fill colour and outline colour.
|
||||
Component = tuple[tuple[tuple[float, float], ...], str, str]
|
||||
|
||||
|
||||
def shape_name(color: str) -> str:
|
||||
"""@brief Name under which the car shape of a colour is registered.
|
||||
|
||||
@param color A turtle colour name from constants.COLORS.
|
||||
@return The registered shape name.
|
||||
"""
|
||||
return f"car-{color}"
|
||||
|
||||
|
||||
def _wheel(
|
||||
center_x: float, center_y: float, radius: float
|
||||
) -> tuple[tuple[float, float], ...]:
|
||||
"""@brief An octagon that stands in for a wheel."""
|
||||
offsets = (
|
||||
(1, 0),
|
||||
(0.7, 0.7),
|
||||
(0, 1),
|
||||
(-0.7, 0.7),
|
||||
(-1, 0),
|
||||
(-0.7, -0.7),
|
||||
(0, -1),
|
||||
(0.7, -0.7),
|
||||
)
|
||||
return tuple(
|
||||
(center_x + radius * dx, center_y + radius * dy) for dx, dy in offsets
|
||||
)
|
||||
|
||||
|
||||
def car_components(color: str) -> list[Component]:
|
||||
"""@brief The polygons that make up a car, drawn bottom to top.
|
||||
|
||||
@param color Fill colour of the body and the cabin.
|
||||
@return The components, all inside x -20..20 and y -10..10, front at -x.
|
||||
"""
|
||||
return [
|
||||
# lower body with a short sloped nose
|
||||
(((-20, -5), (20, -5), (20, 2), (-16, 2), (-20, -1)), color, CAR_OUTLINE_COLOR),
|
||||
# cabin
|
||||
(((-7, 2), (-3, 9), (10, 9), (14, 2)), color, CAR_OUTLINE_COLOR),
|
||||
# front and rear window
|
||||
(((-5, 2), (-2, 7), (2, 7), (2, 2)), CAR_GLASS_COLOR, CAR_OUTLINE_COLOR),
|
||||
(((4, 2), (4, 7), (9, 7), (12, 2)), CAR_GLASS_COLOR, CAR_OUTLINE_COLOR),
|
||||
# headlight at the front
|
||||
(
|
||||
((-20, -2), (-18, -2), (-18, 0), (-20, 0)),
|
||||
CAR_LIGHT_COLOR,
|
||||
CAR_OUTLINE_COLOR,
|
||||
),
|
||||
# wheels
|
||||
(_wheel(-11, -6, 4), CAR_TYRE_COLOR, CAR_OUTLINE_COLOR),
|
||||
(_wheel(11, -6, 4), CAR_TYRE_COLOR, CAR_OUTLINE_COLOR),
|
||||
]
|
||||
|
||||
|
||||
def register_car_shapes(screen: TurtleScreen) -> None:
|
||||
"""@brief Register one car shape per colour with the screen.
|
||||
|
||||
@param screen The turtle screen that will draw the cars.
|
||||
"""
|
||||
for color in COLORS:
|
||||
shape = Shape("compound")
|
||||
for polygon, fill, outline in car_components(color):
|
||||
shape.addcomponent(polygon, fill, outline)
|
||||
screen.register_shape(shape_name(color), shape)
|
||||
@@ -28,6 +28,16 @@ CAR_HEIGHT = 20
|
||||
SPAWN_ODDS = 6
|
||||
## No cars in the top and bottom strip of this height.
|
||||
SAFE_ZONE = 50
|
||||
## A car spawns just off the right edge and is removed once fully off the left.
|
||||
CAR_START_X = SCREEN_WIDTH // 2 + CAR_LENGTH // 2
|
||||
CAR_REMOVE_X = -(SCREEN_WIDTH // 2 + CAR_LENGTH)
|
||||
## Cars are placed at a y coordinate within -CAR_Y_LIMIT..CAR_Y_LIMIT.
|
||||
CAR_Y_LIMIT = SCREEN_HEIGHT // 2 - SAFE_ZONE
|
||||
## Colours of the non-body parts of a car.
|
||||
CAR_OUTLINE_COLOR = "black"
|
||||
CAR_GLASS_COLOR = "light sky blue"
|
||||
CAR_TYRE_COLOR = "black"
|
||||
CAR_LIGHT_COLOR = "gold"
|
||||
|
||||
## Scoreboard text.
|
||||
FONT = ("Courier", 24, "normal")
|
||||
|
||||
@@ -6,6 +6,7 @@ import time
|
||||
from turtle import Screen
|
||||
|
||||
from car_manager import CarManager
|
||||
from car_shape import register_car_shapes
|
||||
from constants import LOOP_DELAY, SCREEN_HEIGHT, SCREEN_WIDTH
|
||||
from player import Player
|
||||
from scoreboard import Scoreboard
|
||||
@@ -16,8 +17,10 @@ def main() -> None:
|
||||
screen = Screen()
|
||||
screen.setup(width=SCREEN_WIDTH, height=SCREEN_HEIGHT)
|
||||
screen.tracer(0)
|
||||
register_car_shapes(screen)
|
||||
|
||||
player = Player()
|
||||
car_manager = CarManager()
|
||||
screen.listen()
|
||||
screen.onkey(player.go_up, "Up")
|
||||
|
||||
@@ -25,6 +28,8 @@ def main() -> None:
|
||||
while game_is_on:
|
||||
time.sleep(LOOP_DELAY)
|
||||
screen.update()
|
||||
car_manager.create_cars()
|
||||
car_manager.move_cars()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user