Created
July 4, 2025 04:15
-
-
Save hsk/3e69853dc8bb42718b1f597544dc3908 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import pygame | |
| import math | |
| import time | |
| # Pygameの初期化 | |
| pygame.init() | |
| screen = pygame.display.set_mode((800, 600)) | |
| clock = pygame.time.Clock() | |
| font = pygame.font.Font(None, 36) # テキスト表示用フォント | |
| track_width = 50 | |
| # オフスクリーンサーフェス | |
| render_surface = pygame.Surface((800, 600)) | |
| # 車のクラス | |
| class Car: | |
| def __init__(self, x, y): | |
| self.x = x | |
| self.y = y | |
| self.angle = 0 # 車の向き(度) | |
| self.speed = 0 # 速度 | |
| self.max_speed = 2 # 最大速度 | |
| self.acceleration = 0.1 # 加速 | |
| self.deceleration = 0.05 # 減速 | |
| self.rotation_speed = 3 # 回転速度 | |
| self.prev_x = x # 前の位置 | |
| self.prev_y = y | |
| def update(self, keys): | |
| self.prev_x = self.x | |
| self.prev_y = self.y | |
| if keys[pygame.K_LEFT]: | |
| self.angle += self.rotation_speed | |
| if keys[pygame.K_RIGHT]: | |
| self.angle -= self.rotation_speed | |
| if keys[pygame.K_x]: | |
| self.speed = min(self.speed + self.acceleration, self.max_speed) | |
| elif keys[pygame.K_z]: | |
| self.speed = max(self.speed - self.acceleration, -self.max_speed / 2) | |
| else: | |
| if self.speed > 0: | |
| self.speed = max(self.speed - self.deceleration, 0) | |
| elif self.speed < 0: | |
| self.speed = min(self.speed + self.deceleration, 0) | |
| rad = math.radians(self.angle) | |
| self.x += self.speed * math.cos(rad) | |
| self.y -= self.speed * math.sin(rad) | |
| def get_points(self): | |
| length = 30 | |
| width = 15 | |
| return [ | |
| (self.x + length / 2 * math.cos(math.radians(self.angle)), | |
| self.y - length / 2 * math.sin(math.radians(self.angle))), | |
| (self.x + width / 2 * math.cos(math.radians(self.angle + 90)), | |
| self.y - width / 2 * math.sin(math.radians(self.angle + 90))), | |
| (self.x - length / 2 * math.cos(math.radians(self.angle)), | |
| self.y + length / 2 * math.sin(math.radians(self.angle))), | |
| (self.x - width / 2 * math.cos(math.radians(self.angle + 90)), | |
| self.y + width / 2 * math.sin(math.radians(self.angle + 90))) | |
| ] | |
| def check_collision(self, outer_polygon, inner_polygon): | |
| for point in self.get_points(): | |
| if not point_in_polygon(point, outer_polygon) or point_in_polygon(point, inner_polygon): | |
| return True | |
| return False | |
| def draw(self, surface): | |
| points = self.get_points() | |
| pygame.draw.polygon(surface, (255, 0, 0), points) | |
| width = 15 | |
| wing_points = [ | |
| points[0], | |
| (self.x + width / 3 * math.cos(math.radians(self.angle + 90)), | |
| self.y - width / 3 * math.sin(math.radians(self.angle + 90))), | |
| (self.x - width / 3 * math.cos(math.radians(self.angle + 90)), | |
| self.y + width / 3 * math.sin(math.radians(self.angle + 90))) | |
| ] | |
| pygame.draw.polygon(surface, (0, 0, 0), wing_points) | |
| # ポリゴンの内外判定 | |
| def point_in_polygon(point, polygon): | |
| x, y = point | |
| n = len(polygon) | |
| inside = False | |
| px, py = polygon[0] | |
| for i in range(n + 1): | |
| sx, sy = polygon[i % n] | |
| if y > min(py, sy): | |
| if y <= max(py, sy): | |
| if x <= max(px, sx): | |
| if py != sy: | |
| xinters = (y - py) * (sx - px) / (sy - py) + px | |
| if px == sx or x <= xinters: | |
| inside = not inside | |
| px, py = sx, sy | |
| return inside | |
| # 誘導弾風のパス生成 | |
| def generate_homing_path(checkpoints): | |
| path_points = [] | |
| missile_x, missile_y = checkpoints[0] | |
| missile_angle = 0 | |
| missile_speed = 5 | |
| max_turn_rate = 5 | |
| for i in range(len(checkpoints)): | |
| target = checkpoints[(i + 1) % len(checkpoints)] | |
| while math.hypot(missile_x - target[0], missile_y - target[1]) > 10: | |
| target_angle = math.degrees(math.atan2(target[1] - missile_y, target[0] - missile_x)) | |
| angle_diff = (target_angle - missile_angle + 180) % 360 - 180 | |
| turn = max(min(angle_diff, max_turn_rate), -max_turn_rate) | |
| missile_angle += turn | |
| rad = math.radians(missile_angle) | |
| missile_x += missile_speed * math.cos(rad) | |
| missile_y += missile_speed * math.sin(rad) | |
| path_points.append((missile_x, missile_y)) | |
| missile_x, missile_y = target | |
| return path_points | |
| # 台形変換(パース効果) | |
| # ラインごとのパース変換 | |
| def transform_perspective(surface): | |
| width, height = surface.get_size() | |
| warped_surface = pygame.Surface((800, 600), pygame.SRCALPHA) | |
| # 描画範囲:画面下(y=300)から上(y=600) | |
| for dest_y in range(height//2, height): | |
| # 横幅スケール:dest_y=300で0.5、dest_y=600で1.0(線形補間) | |
| z = height / dest_y | |
| t = 2 - z | |
| src_y = t * height | |
| if src_y < 0: src_y = 0 | |
| if src_y >= height: | |
| src_y = height - 1 | |
| scale = 0.5 + (1.0 - 0.5) * ((dest_y * 2 - height) / height) | |
| new_width = int(width * scale) | |
| if new_width <= 0: | |
| continue | |
| # ラインを抽出 | |
| line_rect = pygame.Rect(0, int(src_y), width, 1) | |
| line_surface = surface.subsurface(line_rect).copy() | |
| # ラインを横にスケール | |
| scaled_line = pygame.transform.scale(line_surface, (new_width, 1)) | |
| # 中央に配置 | |
| dest_x = 400 - new_width / 2 | |
| warped_surface.blit(scaled_line, (dest_x, dest_y)) | |
| warped_surface.set_at((int(t*100), dest_y), (255, 0, 0)) | |
| warped_surface.set_at((int(z*100), dest_y), (255, 0, 255)) | |
| warped_surface.set_at((100, dest_y), (255, 255, 0)) | |
| return warped_surface | |
| # コースとチェックポイントの描画 | |
| def draw_course(surface, checkpoints, active_checkpoint, path_points): | |
| outer_polygon = [] | |
| inner_polygon = [] | |
| for i, (x, y) in enumerate(path_points): | |
| next_point = path_points[(i + 1) % len(path_points)] | |
| angle = math.atan2(next_point[1] - y, next_point[0] - x) | |
| ox = x + track_width * math.cos(angle + math.pi / 2) | |
| oy = y + track_width * math.sin(angle + math.pi / 2) | |
| ix = x - track_width * math.cos(angle + math.pi / 2) | |
| iy = y - track_width * math.sin(angle + math.pi / 2) | |
| outer_polygon.append((ox, oy)) | |
| inner_polygon.append((ix, iy)) | |
| surface.fill((0, 255, 0)) | |
| pygame.draw.polygon(surface, (100, 100, 100), outer_polygon) | |
| pygame.draw.polygon(surface, (0, 255, 0), inner_polygon) | |
| pygame.draw.lines(surface, (255, 255, 255), True, path_points, 2) | |
| for i, (cx, cy) in enumerate(checkpoints): | |
| color = (255, 0, 0) if i == active_checkpoint else (255, 255, 255) | |
| pygame.draw.circle(surface, color, (int(cx), int(cy)), track_width+10, 1) | |
| return outer_polygon, inner_polygon | |
| # チェックポイント通過判定 | |
| def check_checkpoint(car, checkpoints, current_checkpoint): | |
| cx, cy = checkpoints[current_checkpoint] | |
| if math.hypot(car.x - cx, car.y - cy) < track_width + 10: | |
| return (current_checkpoint + 1) % len(checkpoints) | |
| return current_checkpoint | |
| # チェックポイント操作 | |
| def handle_checkpoint_interaction(event, checkpoints, selected_checkpoint, path_points): | |
| mouse_pos = pygame.mouse.get_pos() | |
| if event.type == pygame.MOUSEBUTTONDOWN: | |
| if event.button == 1: | |
| for i, (cx, cy) in enumerate(checkpoints): | |
| if math.hypot(mouse_pos[0] - cx, mouse_pos[1] - cy) < track_width / 2: | |
| return i, checkpoints, path_points | |
| min_dist = float('inf') | |
| closest_segment = -1 | |
| closest_t = 0 | |
| for i in range(len(path_points)): | |
| p1 = path_points[i] | |
| p2 = path_points[(i + 1) % len(path_points)] | |
| dx = p2[0] - p1[0] | |
| dy = p2[1] - p1[1] | |
| length_sq = dx * dx + dy * dy | |
| if length_sq == 0: | |
| dist = math.hypot(mouse_pos[0] - p1[0], mouse_pos[1] - p1[1]) | |
| t = 0 | |
| else: | |
| t = max(0, min(1, ((mouse_pos[0] - p1[0]) * dx + (mouse_pos[1] - p1[1]) * dy) / length_sq)) | |
| proj_x = p1[0] + t * dx | |
| proj_y = p1[1] + t * dy | |
| dist = math.hypot(mouse_pos[0] - proj_x, mouse_pos[1] - proj_y) | |
| if dist < min_dist and dist < 10: | |
| min_dist = dist | |
| closest_segment = i | |
| closest_t = t | |
| if closest_segment != -1: | |
| new_x = path_points[closest_segment][0] + closest_t * (path_points[(closest_segment + 1) % len(path_points)][0] - path_points[closest_segment][0]) | |
| new_y = path_points[closest_segment][1] + closest_t * (path_points[(closest_segment + 1) % len(path_points)][1] - path_points[closest_segment][1]) | |
| insert_idx = closest_segment // (len(path_points) // len(checkpoints)) + 1 | |
| checkpoints.insert(insert_idx, (new_x, new_y)) | |
| path_points = generate_homing_path(checkpoints) | |
| return None, checkpoints, path_points | |
| elif event.button == 3: | |
| if len(checkpoints) > 3: | |
| for i, (cx, cy) in enumerate(checkpoints): | |
| if math.hypot(mouse_pos[0] - cx, mouse_pos[1] - cy) < track_width / 2: | |
| checkpoints.pop(i) | |
| path_points = generate_homing_path(checkpoints) | |
| return None, checkpoints, path_points | |
| elif event.type == pygame.MOUSEBUTTONUP: | |
| if event.button == 1: | |
| return None, checkpoints, path_points | |
| elif event.type == pygame.MOUSEMOTION and selected_checkpoint is not None: | |
| checkpoints[selected_checkpoint] = mouse_pos | |
| path_points = generate_homing_path(checkpoints) | |
| return selected_checkpoint, checkpoints, path_points | |
| return selected_checkpoint, checkpoints, path_points | |
| def draw_text(text, pos, color=(255, 255, 255)): | |
| screen.blit(font.render(text, True, (0, 0, 0)), (pos[0]+2, pos[1]+2)) | |
| screen.blit(font.render(text, True, color), pos) | |
| def draw_textl(text, pos, color=(255, 255, 255)): | |
| surface = font.render(text, True, (0, 0, 0)) | |
| screen.blit(surface, (-surface.get_width() + pos[0]+2, pos[1]+2)) | |
| screen.blit(font.render(text, True, color), (-surface.get_width() + pos[0], pos[1])) | |
| # メインループ | |
| checkpoints = [(677, 506), (731, 125), (609, 326), (279, 374), (461, 189), (173, 76), (84, 448), (318, 510)] | |
| car = Car(checkpoints[-1][0], checkpoints[-1][1]) | |
| path_points = generate_homing_path(checkpoints) | |
| current_checkpoint = 0 | |
| lap_count = 0 | |
| lap_times = [] | |
| start_time = time.time() | |
| last_lap_time = start_time | |
| best_lap_time = float('inf') | |
| selected_checkpoint = None | |
| speed_adjust_cooldown = 0 | |
| is_perspective = False | |
| running = True | |
| while running: | |
| for event in pygame.event.get(): | |
| if event.type == pygame.QUIT: | |
| running = False | |
| selected_checkpoint, checkpoints, path_points = handle_checkpoint_interaction(event, checkpoints, selected_checkpoint, path_points) | |
| # キー入力 | |
| keys = pygame.key.get_pressed() | |
| car.update(keys) | |
| # 最大速度の調整 | |
| if speed_adjust_cooldown > 0: | |
| speed_adjust_cooldown -= 1 | |
| else: | |
| if keys[pygame.K_UP]: | |
| car.max_speed += 0.25 | |
| speed_adjust_cooldown = 10 | |
| if keys[pygame.K_DOWN]: | |
| car.max_speed = max(0, car.max_speed - 0.25) | |
| speed_adjust_cooldown = 10 | |
| if keys[pygame.K_c]: | |
| is_perspective = not is_perspective | |
| speed_adjust_cooldown = 10 | |
| # 衝突判定 | |
| outer_polygon, inner_polygon = draw_course(render_surface, checkpoints, current_checkpoint, path_points) | |
| if car.check_collision(outer_polygon, inner_polygon): | |
| car.speed *= 0.95 | |
| # チェックポイント通過判定 | |
| new_checkpoint = check_checkpoint(car, checkpoints, current_checkpoint) | |
| if new_checkpoint != current_checkpoint: | |
| current_checkpoint = new_checkpoint | |
| if current_checkpoint == 0: | |
| lap_count += 1 | |
| current_time = time.time() | |
| lap_time = current_time - last_lap_time | |
| lap_times.append(lap_time) | |
| if len(lap_times) > 10: | |
| lap_times.pop(0) | |
| last_lap_time = current_time | |
| best_lap_time = min(best_lap_time, lap_time) | |
| # 車をサーフェスに描画 | |
| car.draw(render_surface) | |
| # サーフェスを画面に描画 | |
| screen.fill((0, 0, 0)) # 背景クリア | |
| if is_perspective: | |
| transformed_surface = transform_perspective(render_surface) | |
| screen.blit(transformed_surface, (0, 0)) | |
| else: | |
| screen.blit(render_surface, (0, 0)) | |
| # 進捗とタイム表示 | |
| total_time = time.time() - start_time | |
| ypos = 10 | |
| draw_text(f"Laps: {lap_count}", (10, ypos)); ypos += 30 | |
| draw_text(f"Time: {total_time:.2f}s", (10, ypos)); ypos += 30 | |
| draw_text(f"Max Speed: {car.max_speed:.2f}", (10, ypos)); ypos += 30 | |
| # 過去10個のラップタイムを右側に表示 | |
| ypos = 10 | |
| draw_textl(f"Best Lap: {best_lap_time if best_lap_time != float('inf') else 0:.2f}s", (800-20, ypos), (255, 255, 0)); ypos += 30 | |
| for i, lap_time in enumerate(reversed(lap_times[-10:])): | |
| draw_textl(f"Lap {lap_count - i}: {lap_time:.2f}s", (800-20, ypos), (255, 255, 255) if lap_time != best_lap_time else (255, 255, 0)) | |
| ypos += 30 | |
| pygame.display.flip() | |
| clock.tick(60) | |
| pygame.quit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment