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:
2026-10-13 13:58:35 +08:00
parent 625b2a8d51
commit ade4275813
8 changed files with 347 additions and 2 deletions
+53 -1
View File
@@ -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
+86
View File
@@ -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)
+10
View File
@@ -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")
+5
View File
@@ -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__":