from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from .objects.pong.PongPlayer import PongPlayer from .objects.pong.PongGame import PongGame from .objects.pong.Ball import Ball from .objects.pong.Point import Point from .objects.pong.Segment import Segment from . import config import math import asyncio from asgiref.sync import SyncToAsync VERTICALLY = 1 NORMAL = 2 def get_sign(num: float) -> int: if (num == 0): return 0 if (num > 0): return 1 if (num < 0): return -1 def get_derive(segment: Segment) -> float: if (segment.start.x == segment.stop.x): return None return (segment.stop.y - segment.start.y) / (segment.stop.x - segment.start.x) def get_intercept(derive: float, point: Point) -> float: if (derive is None): return None return point.y - (point.x * derive) def get_constant(segment: Segment) -> float: return segment.start.x def identify(segment: Segment) -> str: if (segment.start.x == segment.stop.x): return VERTICALLY return NORMAL def get_interception(segment1: Segment, segment2: Segment): if (identify(segment1) == VERTICALLY and identify(segment2) == VERTICALLY): return None # because of in matematics world y = 10 is above y = 5 and on a display it is inverted I invert the coordonate inverted_segment1 = Segment(Point(segment1.start.x, config.MAP_SIZE_Y - segment1.start.y), Point(segment1.stop.x, config.MAP_SIZE_Y - segment1.stop.y)) inverted_segment2 = Segment(Point(segment2.start.x, config.MAP_SIZE_Y - segment2.start.y), Point(segment2.stop.x, config.MAP_SIZE_Y - segment2.stop.y)) if (identify(segment1) == NORMAL and identify(segment2) == NORMAL): # representation m * x + p m1 = get_derive(inverted_segment1) m2 = get_derive(inverted_segment2) p1 = get_intercept(m1, inverted_segment1.start) p2 = get_intercept(m2, inverted_segment2.start) # m1 * x + p1 = m2 * x + p2 # m1 * x = m2 * x + p2 -p1 # m1 * x - m2 * x = p1 - p2 # x * (m1 - m2) = p1 - p2 # x = (p1 - p2) / (m1 - m2) if (m1 == m2): return None # reinvert x: float = (p1 - p2) / (m1 - m2) * (-1) y: float = config.MAP_SIZE_Y - (m1 * x + p1) else: if (identify(inverted_segment1) == VERTICALLY): constant: float = get_constant(inverted_segment1) m: float = get_derive(inverted_segment2) p: float = get_intercept(m, inverted_segment2.start) else: constant: float = get_constant(inverted_segment2) m: float = get_derive(inverted_segment1) p: float = get_intercept(m, inverted_segment1.start) x: float = constant y: float = config.MAP_SIZE_Y - (m * x + p) impact: Point = Point(x, y) return impact def get_impact_data(segments: list[Segment], ball: Ball) -> dict: cos: float = round(math.cos(ball.angle), 6) sin: float = round(math.sin(ball.angle), 6) inc_x: float = (-1) * get_sign(cos) * (config.STROKE_THICKNESS + config.BALL_SIZE / 2) inc_y: float = get_sign(sin) * (config.STROKE_THICKNESS + config.BALL_SIZE / 2) point: Point = Point(ball.position.location.x + cos, ball.position.location.y - sin) ball_segment = Segment(ball.position.location, point) closest: dict = None for segment in segments: segment_with_padding = segment.copy() segment_with_padding.start.x += inc_x segment_with_padding.stop.x += inc_x segment_with_padding.start.y += inc_y segment_with_padding.stop.y += inc_y impact: Point = get_interception(segment_with_padding, ball_segment) if (impact is None): continue diff_x: float = ball.position.location.x - impact.x if (get_sign(diff_x) == get_sign(cos) and cos != 0): continue diff_y: float = (ball.position.location.y - impact.y) if (get_sign(diff_y) != get_sign(sin) and sin != 0): continue distance: float = impact.distance(ball.position.location) if (closest is None or distance < closest.get("distance")): closest = { "inc_x": inc_x, "inc_y": inc_y, "impact": impact, "segment": segment, "distance": distance, } return closest def wall_collision(ball_angle: float, wall: Segment) -> float: wall_angle: float = wall.angle() cos: float = math.cos(wall_angle) * -1 sin: float = math.sin(wall_angle) wall_angle: float = math.atan2(sin, cos) incident_angle: float = ball_angle - wall_angle reflection_angle: float = wall_angle - incident_angle return reflection_angle async def paddle_collision(game: PongGame, impact: Point, player: PongPlayer, inc_x: float, inc_y: float): diff_x: float = player.rail.stop.x - player.rail.start.x diff_y: float = player.rail.stop.y - player.rail.start.y paddle_center_x: float = player.rail.start.x + diff_x * player.position.location paddle_center_y: float = player.rail.start.y + diff_y * player.position.location paddle_center: Point = Point(paddle_center_x, paddle_center_y) rail_length: float = player.rail.length() paddle_length: float = rail_length * config.PADDLE_RATIO start_x: float = paddle_center.x - (diff_x * (paddle_length / 2 / rail_length)) start_y: float = paddle_center.y - (diff_y * (paddle_length / 2 / rail_length)) stop_x: float = paddle_center.x + (diff_x * (paddle_length / 2 / rail_length)) stop_y: float = paddle_center.y + (diff_y * (paddle_length / 2 / rail_length)) start: Point = Point(start_x, start_y) stop: Point = Point(stop_x, stop_y) paddle: Segment = Segment(start, stop) hit_point: Point = Point(impact.x - inc_x, impact.y - inc_y) if (not paddle.is_on(hit_point)): await SyncToAsync(game.goal)(player) return None paddle_angle: float = paddle.angle() normal: float = paddle_angle - math.pi / 2 start_distance: float = paddle.start.distance(impact) stop_distance: float = paddle.stop.distance(impact) hit_percent: float = (start_distance) / (start_distance + stop_distance) hit_percent = round(hit_percent, 1) new_angle: float = normal + (math.pi * 0.85) * (hit_percent - 0.5) return new_angle async def collision(game: PongGame, impact_data: dict) -> bool: segment: Segment = impact_data.get("segment") player_hitted = None for player in game.players: if (not player.is_connected()): continue if (player.rail is segment): player_hitted = player break angle: float if (player_hitted is None): angle = wall_collision(game.ball.angle, segment) else: angle = await paddle_collision(game, impact_data.get("impact"), player_hitted, impact_data.get("inc_x"), impact_data.get("inc_y")) if (angle is None): return False game.ball.speed += config.BALL_SPEED_INC game.ball.angle = angle return True async def update_ball(game: PongGame, impact_data: dict) -> None: distance: float = impact_data.get("distance") time_before_impact: float = distance / game.ball.speed await asyncio.sleep(time_before_impact) hit: bool = await collision(game, impact_data) if (hit == False): await asyncio.sleep(0.1) # delay to create frontend animation game.ball.reset() else: game.ball.position.location = impact_data.get("impact") await SyncToAsync(game.broadcast)("update_ball", game.ball.to_dict()) async def render_ball(game: PongGame): while True: segments: list[Segment] = [player.rail for player in game.players] + game.walls impact_data: dict = get_impact_data(segments, game.ball) await update_ball(game, impact_data) async def render_players(game: PongGame): while True: for player in game._updated_players: await SyncToAsync(game.broadcast)("update_player", player.to_dict(), [player]) game._updated_players.clear() await asyncio.sleep(1 / config.SERVER_TPS) async def render(game: PongGame): routine_ball = asyncio.create_task(render_ball(game)) routine_players = asyncio.create_task(render_players(game)) while(True): if (game.stopped): routine_ball.cancel() routine_players.cancel() return await asyncio.sleep(0.05) def routine(game: PongGame): asyncio.run(render(game))