diff --git a/README.md b/README.md index c9aff93..7657691 100644 --- a/README.md +++ b/README.md @@ -1 +1,332 @@ -# Racing-game \ No newline at end of file +""" +Top-down Racing Game (single file) +Requires: pygame +Install: pip install pygame +Run: python racing_game.py + +Controls: + - Up / W : accelerate + - Down / S : brake / reverse + - Left / A : steer left + - Right / D : steer right + - R : restart race + - ESC or window close : quit +""" +# Ensure pygame is installed: pip install pygame +import sys +import math +import random +import pygame +from pygame import Vector2 # type: ignore + +pygame.init() +WIDTH, HEIGHT = 1000, 700 +SCREEN = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Top-down Racing Demo") +CLOCK = pygame.time.Clock() +FONT = pygame.font.SysFont(None, 24) + +# Colors +GREEN = (61, 174, 64) +ROAD = (50, 50, 50) +LINE = (255, 255, 255) +START_LINE = (255, 0, 0) +SKY = (120, 200, 255) + +# Track parameters +TRACK_CENTER = Vector2(WIDTH // 2, HEIGHT // 2 - 20) +OUTER_RX, OUTER_RY = 380, 220 +INNER_RX, INNER_RY = 200, 90 + +# Game settings +NUM_OPPONENTS = 3 +LAPS_TO_FINISH = 3 + +def create_track_surface(): + surf = pygame.Surface((WIDTH, HEIGHT)) + surf.fill(GREEN) + # draw outer track (road) + pygame.draw.ellipse(surf, ROAD, ( + TRACK_CENTER.x - OUTER_RX, TRACK_CENTER.y - OUTER_RY, OUTER_RX * 2, OUTER_RY * 2 + )) + # cut out inner grass to make ring + pygame.draw.ellipse(surf, GREEN, ( + TRACK_CENTER.x - INNER_RX, TRACK_CENTER.y - INNER_RY, INNER_RX * 2, INNER_RY * 2 + )) + # draw finish/start line at top center of inner ellipse + line_w = 4 + # compute top point of inner ellipse + top_x = TRACK_CENTER.x + top_y = TRACK_CENTER.y - INNER_RY + pygame.draw.line(surf, START_LINE, (top_x - 20, top_y), (top_x + 20, top_y), line_w) + # small white dashes on outer boundary for visuals + for a in range(0, 360, 12): + ra = math.radians(a) + x = TRACK_CENTER.x + math.cos(ra) * (OUTER_RX + INNER_RX) / 2 + y = TRACK_CENTER.y + math.sin(ra) * (OUTER_RY + INNER_RY) / 2 + pygame.draw.circle(surf, (200, 200, 200), (int(x), int(y)), 2) + return surf + +TRACK_SURF = create_track_surface() +TRACK_PIXELS = pygame.PixelArray(TRACK_SURF) # for quick color checks (read-only usage) + +def is_on_road(pos): + x, y = int(pos.x), int(pos.y) + if 0 <= x < WIDTH and 0 <= y < HEIGHT: + return TRACK_SURF.get_at((x, y))[:3] == ROAD + return False + +def angle_to(target, current_pos): + v = target - current_pos + return math.degrees(math.atan2(-v.y, v.x)) % 360 + +class Car: + def __init__(self, color, pos, angle=0.0, max_speed=4.0): + self.color = color + self.pos = Vector2(pos) + self.angle = angle # degrees, 0 = right + self.speed = 0.0 + self.max_speed = max_speed + self.length = 28 + self.width = 14 + self.acceleration = 0.12 + self.brake_decel = 0.2 + self.friction = 0.03 + self.steer_speed = 3.5 # degrees per frame at full lock + self.laps = 0 + self.previous_cross = False + self.progress = 0.0 # angular progress on track for ranking + + def rect_points(self): + # return polygon points for rotated rectangle (for debugging/collision visuals) + rad = math.radians(self.angle) + cos, sin = math.cos(rad), math.sin(rad) + l, w = self.length / 2, self.width / 2 + corners = [ + Vector2(cos * l - sin * w, sin * l + cos * w), + Vector2(-cos * l - sin * w, -sin * l + cos * w), + Vector2(-cos * l + sin * w, -sin * l - cos * w), + Vector2(cos * l + sin * w, sin * l - cos * w), + ] + return [self.pos + p for p in corners] + + def update(self): + # movement integration + rad = math.radians(self.angle) + self.pos += Vector2(math.cos(rad), -math.sin(rad)) * self.speed + + # friction + if self.speed > 0: + self.speed = max(0.0, self.speed - self.friction) + elif self.speed < 0: + self.speed = min(0.0, self.speed + self.friction) + + # check road/grass slowdown + if not is_on_road(self.pos): + # on grass => heavy slowdown + self.speed *= 0.94 + # reduce steering + # (we don't change steer here; controllers apply steer with penalty) + # keep on screen + self.pos.x = max(0, min(WIDTH - 1, self.pos.x)) + self.pos.y = max(0, min(HEIGHT - 1, self.pos.y)) + + # compute angular progress around track center (for lap progress) + dx = self.pos.x - TRACK_CENTER.x + dy = self.pos.y - TRACK_CENTER.y + angle = math.degrees(math.atan2(-dy, dx)) % 360 + # convert to a progress metric (0...1) around the track + self.progress = angle / 360.0 + + # finish line crossing detection: compare above/below top y of inner ellipse + top_y = TRACK_CENTER.y - INNER_RY + # crossing occurs when y <= top_y +/- a small tolerance and x near center + x_near = abs(self.pos.x - TRACK_CENTER.x) < INNER_RX * 0.6 + crossing = (self.pos.y < top_y + 6) and x_near + if crossing and not self.previous_cross: + # when crossing heading roughly forward (y increasing after crossing) + self.laps += 1 + self.previous_cross = crossing + + def draw(self, surf): + # draw rotated rectangle + points = self.rect_points() + pg_points = [(p.x, p.y) for p in points] + pygame.draw.polygon(surf, self.color, pg_points) + # small nose to indicate forward + rad = math.radians(self.angle) + nose = self.pos + Vector2(math.cos(rad), -math.sin(rad)) * (self.length/2 + 4) + pygame.draw.circle(surf, (0,0,0), (int(nose.x), int(nose.y)), 3) + +class PlayerCar(Car): + def handle_input(self, keys): + # acceleration / braking + if keys[pygame.K_UP] or keys[pygame.K_w]: + self.speed += self.acceleration + if keys[pygame.K_DOWN] or keys[pygame.K_s]: + self.speed -= self.brake_decel + # steering scaled by speed + steer_amount = self.steer_speed * (max(0.2, abs(self.speed) / self.max_speed)) + if keys[pygame.K_LEFT] or keys[pygame.K_a]: + self.angle += steer_amount + if keys[pygame.K_RIGHT] or keys[pygame.K_d]: + self.angle -= steer_amount + # clamp + self.speed = max(-self.max_speed/2, min(self.max_speed, self.speed)) + +class AICar(Car): + def __init__(self, color, pos, waypoints, angle=0.0, max_speed=3.7): + super().__init__(color, pos, angle, max_speed) + self.waypoints = waypoints + self.current_wp = 0 + + def update_ai(self): + target = self.waypoints[self.current_wp] + vec_to = target - self.pos + dist = vec_to.length() + desired_angle = math.degrees(math.atan2(-vec_to.y, vec_to.x)) % 360 + # angle difference + diff = (desired_angle - self.angle + 180) % 360 - 180 + # steer toward desired angle + steer = max(-self.steer_speed, min(self.steer_speed, -diff)) + # steering effectiveness lowered on grass + if not is_on_road(self.pos): + steer *= 0.5 + self.angle += steer * 0.6 # AI steering smoothing + # throttle control + if abs(diff) < 30: + self.speed += self.acceleration * (1.2 if is_on_road(self.pos) else 0.3) + else: + # slow down for sharp turns + self.speed -= self.brake_decel * 0.8 + # reach waypoint + if dist < 30: + self.current_wp = (self.current_wp + 1) % len(self.waypoints) + # clamp speed + self.speed = max(-self.max_speed/3, min(self.max_speed, self.speed)) + +# create circular waypoints around TRACK_CENTER for AI +def generate_waypoints(n=24, radius_x=(OUTER_RX+INNER_RX)/2, radius_y=(OUTER_RY+INNER_RY)/2, jitter=10): + pts = [] + for i in range(n): + a = math.radians(i * (360.0 / n)) + r_x = radius_x + random.uniform(-jitter, jitter) + r_y = radius_y + random.uniform(-jitter, jitter) + x = TRACK_CENTER.x + math.cos(a) * r_x + y = TRACK_CENTER.y + math.sin(a) * r_y + pts.append(Vector2(x, y)) + return pts + +WAYPOINTS = generate_waypoints(32) + +def reset_race(): + # player start just above start line, facing right + player = PlayerCar((0, 120, 230), Vector2(TRACK_CENTER.x - 40, TRACK_CENTER.y - INNER_RY - 30), angle=0.0, max_speed=5.0) + opponents = [] + for i in range(NUM_OPPONENTS): + offset = (i + 1) * 40 + pos = Vector2(TRACK_CENTER.x + offset, TRACK_CENTER.y - INNER_RY - 30) + color = (200 - i*40, 50 + i*50, 50 + i*30) + ai = AICar(color, pos, WAYPOINTS, angle=0.0, max_speed=4.2 - i*0.4) + opponents.append(ai) + return player, opponents + +player, opponents = reset_race() + +def draw_hud(surf, player, opponents, finished=False): + lines = [ + f"Speed: {player.speed:.1f}", + f"Laps: {min(player.laps, LAPS_TO_FINISH)}/{LAPS_TO_FINISH}", + ] + for i, line in enumerate(lines): + txt = FONT.render(line, True, (0,0,0)) + surf.blit(txt, (10, 10 + i*22)) + # ranking by laps then progress + racers = [player] + opponents + def rank_key(c): + return (c.laps, c.progress) + racers_sorted = sorted(racers, key=rank_key, reverse=True) + for i, r in enumerate(racers_sorted): + label = "You" if r is player else f"CPU{i}" + stat = f"{i+1}. {label} L{r.laps} P{int(r.speed)}" + txt = FONT.render(stat, True, (0,0,0)) + surf.blit(txt, (WIDTH - 220, 10 + i*22)) + if finished: + msg = FONT.render("Race finished! Press R to restart.", True, (0,0,0)) + surf.blit(msg, (WIDTH//2 - 120, 20)) + +def draw_instructions(surf): + lines = [ + "Controls: Arrow keys or WASD to drive. R to restart.", + "Drive on the gray road. Red line = start/finish.", + f"Complete {LAPS_TO_FINISH} laps to finish." + ] + for i, t in enumerate(lines): + txt = FONT.render(t, True, (10,10,10)) + surf.blit(txt, (10, HEIGHT - 70 + i*20)) + +def mainloop(): + global player, opponents + running = True + finished = False + finish_timer = 0 + while running: + dt = CLOCK.tick(60) / 1000.0 + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + running = False + if event.key == pygame.K_r: + player, opponents = reset_race() + finished = False + finish_timer = 0 + + keys = pygame.key.get_pressed() + if not finished: + player.handle_input(keys) + player.update() + for ai in opponents: + ai.update_ai() + ai.update() + + # check finish condition + if not finished and player.laps >= LAPS_TO_FINISH: + finished = True + finish_timer = pygame.time.get_ticks() + + # draw scene + SCREEN.fill(SKY) + SCREEN.blit(TRACK_SURF, (0, 0)) + + # optional: draw waypoints for debug (comment out later) + # for wp in WAYPOINTS: + # pygame.draw.circle(SCREEN, (0,0,0), (int(wp.x), int(wp.y)), 3) + + # draw racers in order by their y for simple painter's ordering + all_cars = opponents + [player] + all_cars.sort(key=lambda c: c.pos.y) # draws from top to bottom + for car in all_cars: + car.draw(SCREEN) + + draw_hud(SCREEN, player, opponents, finished) + draw_instructions(SCREEN) + + # if finished show winner info + if finished: + winner = sorted([player] + opponents, key=lambda c: (c.laps, c.progress), reverse=True)[0] + if winner is player: + msg = "You won! Press R to race again." + else: + msg = "CPU won. Press R to try again." + big = pygame.font.SysFont(None, 40).render(msg, True, (255, 0, 0)) + SCREEN.blit(big, (WIDTH//2 - big.get_width()//2, HEIGHT//2 - 20)) + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + mainloop() \ No newline at end of file diff --git a/__pycache__/README.cpython-312.pyc b/__pycache__/README.cpython-312.pyc new file mode 100644 index 0000000..af4bb31 Binary files /dev/null and b/__pycache__/README.cpython-312.pyc differ