diff --git a/src/engine/ecs.py b/src/engine/ecs.py index 717eba1..d8fe78c 100644 --- a/src/engine/ecs.py +++ b/src/engine/ecs.py @@ -6,6 +6,7 @@ Pour moddifier le monde, on n'agis que sur les composants. """ +import copy from typing import Iterator, Optional, Sequence @@ -123,6 +124,22 @@ class World(Entity): self.__entities: dict[type[object], set[int]] = {} self.__next_id: int = 1 + def partial_copy( + self, *needed: type[object], ressources: Sequence[type[object]] = () + ): + """ + Renvoie une copie du monde avec seulement les composants demandé. + """ + new_world = World() + for need in needed: + for entity in self.query(need): + new_entity = Entity(new_world, entity.identifier) + new_entity.set(copy.deepcopy(self.get_component(entity, need))) + for ressource in ressources: + if ressource in self: + new_world.set(copy.deepcopy(self[ressource])) + return new_world + def new_entity(self) -> "Entity": """ Créer une nouvelle entité dans le monde et la renvoie. diff --git a/src/plugins/physics.py b/src/plugins/physics.py index 8879d4f..4d3368f 100644 --- a/src/plugins/physics.py +++ b/src/plugins/physics.py @@ -147,6 +147,7 @@ def move_entity(entity: Entity, movement: Vec2, disable_callback: bool = False): others = [ AABB.from_entity(other) for other in world.query(Solid, Position, Scale, Origin) ] + distance = 0.0 counter = 0 while movement.length > 0.0001 and counter < 50: t, normal, obstacle = aabb_to_aabbs(aabb, others, movement) @@ -154,6 +155,7 @@ def move_entity(entity: Entity, movement: Vec2, disable_callback: bool = False): step = movement else: step = movement * max(t - 0.000001, 0) + distance += step.length aabb.move(step) aabb.entity_position(entity) movement -= step @@ -163,18 +165,42 @@ def move_entity(entity: Entity, movement: Vec2, disable_callback: bool = False): if normal.y != 0: movement.y *= -1 entity[Velocity].y *= -1 - movement /= entity[Velocity] + movement /= entity[Velocity].length + stop = False if obstacle is not None and not disable_callback: if not entity.get( CollisionHandler, CollisionHandler(lambda e, o: True) ).callback(entity, obstacle): - break + stop = True if not obstacle.get( CollisionHandler, CollisionHandler(lambda e, o: True) ).callback(obstacle, entity): - break - movement *= entity[Velocity] + stop = True + movement *= entity.get(Velocity, Velocity(0)).length counter += 1 + if stop: + break + return distance + + +def simulate(entity: Entity, handler: Callable[[Entity], bool]) -> float: + """ + Simule une entitée jusqu'a ce que le handler retourne True. + """ + base_handler = entity.get( + CollisionHandler, CollisionHandler(lambda e, o: True) + ).callback + + def __handler(a: Entity, b: Entity): + if not base_handler(a, b): + return False + return not handler(b) + + entity.set(CollisionHandler(__handler)) + velocity = entity[Velocity].length + distance = move_entity(entity, entity[Velocity] * 1000) / velocity + entity.set(CollisionHandler(base_handler)) + return distance def __apply_velocity(world: World): diff --git a/src/scenes/game.py b/src/scenes/game.py index 5a1c14f..c002301 100644 --- a/src/scenes/game.py +++ b/src/scenes/game.py @@ -2,106 +2,146 @@ Le jeux principale. """ -from enum import Enum -from engine import Plugin, Scene +import random +from typing import Callable +from engine import CurrentScene, Plugin, Scene from engine.ecs import Entity, World from engine.math import Vec2 from plugins import render from plugins import physics from plugins.inputs import Held +from plugins.physics import CollisionHandler, Solid, Velocity from plugins.render import ( Origin, Position, Scale, SpriteBundle, - TextBundle, Text, + TextBundle, TextSize, ) from plugins.timing import Delta, Time -import random -from plugins.physics import CollisionHandler, Solid, Velocity -class GameMode(Enum): +PLAYER_WALL_DISTANCE = 150 + + +class Score(int): """ - Ressource qui definit le game mode choisi par l'utilisateur. - """ - - ONE = 0 - TWO = 1 - - -class RightWall: - """ - Composant qui marque une entité comme etant le mur de droite. + Ressource correspondant au score du joueur dans la partie. """ -class LeftWall: +class Wall: """ - Composant qui marque une entité comme etant le mur de gauche. + Composant marquant une entitée comme etant une mur. """ -class Player1: +class WinWall: """ - Composant qui marque une entité comme etant le joeuur 1. + Composant marquant une entitée comme etant un mur permettant de gagner. + """ + + @staticmethod + def is_my_side(wall: Entity, player: Entity) -> bool: + return abs(player[Position].x - wall[Position].x) <= PLAYER_WALL_DISTANCE + 1 + + +class Player: + """ + Composant marquant une entité comme étant un joueur. """ -class Player2: +class PlayerScore(int): """ - Composant qui marque une entité comme etant le joeuur 2. + Composant stockant le score d'un joueur. """ +class PlayerSpeed(float): + """ + Composant donnant la vitesse de deplacement d'un joueur. + """ + + +class PlayerTarget(float): + """ + Composant donnant l'objectif de déplacement d'un joueur. + """ + + +class AI: + """ + Composant définissant le comportement d'un joueur. + + A chaques mises à jour, le callback logic sera appelé pour calculer + la position y cible du joueur dans un monde virtuel. + """ + + def __init__(self, logic: Callable[[World, Entity], float]): + self.logic = logic + + class Ball: """ - Composant qui marque une entité comme etant une balle. + Composant marquant une entité comme étant une balle. """ + @staticmethod + def handler(ball: Entity, other: Entity): + """ + Gère les collisions de la balle. + """ + # On récupère le monde + world = ball.world -class Bounce: - """ - Composant qui marque une entité qui peux faire rebondir - """ + # Collision avec un joueur + if Player in other: + speed = ball[Velocity].length + ball[Velocity] = ball[Velocity].normalized + ball[Velocity].y = (ball[Position].y - other[Position].y) * 0.005 + ball[Velocity] = ball[Velocity].normalized * min((speed * 1.1), 1000.0) + world[Score] += 100 + # Collision avec un mur + elif Wall in other: + if WinWall in other: + for entity in world.query(Player, PlayerScore): + if not WinWall.is_my_side(other, entity): + if entity[AI].logic == bot_ai: + print("GAME OVER, score:", world[Score]) + world[CurrentScene] = ONE_PLAYER + return False + else: + world[Score] += 1000 + entity[PlayerScore] += 1 + ball.destroy() + if len(world.query(Ball)) == 0: + StartAnimation.start(world, 3) + return False + else: + world[Score] += 20 -class UpKey(str): - """ - Composant qui indique la touche pour faire monter le joueur - """ + return True - -class DownKey(str): - """ - Composant qui indique la touche pour faire descender le joueur - """ - - -class Speed(int): - """ - Composant qui represente la vitesse de l'entité. - """ - - -class Player1Score(int): - """ - Ressource qui represente le score du joueur 1. - """ - - -class Player2Score(int): - """ - Ressource qui represente le score du joueur 2. - """ - - -class Score: - """ - Composant qui marque l'entité comme étant l'affichage du score. - """ + @staticmethod + def spawn_ball(world: World): + """ + Fais apparaitre une balle au milieu de l'écran. + """ + world.new_entity().set( + SpriteBundle( + "ball.png", + 0, + position=Vec2(render.WIDTH / 2, render.HEIGHT / 2), + origin=Vec2(0.5), + ), + Velocity(random.choice([-1, 1]) * 300, random.randint(-1, 1) * 100), + CollisionHandler(Ball.handler), + Ball(), + ) class StartAnimation(float): @@ -109,506 +149,198 @@ class StartAnimation(float): Composant qui represente un le moment auxquel on a lancé l'animation du compte a rebours """ - -class TimeUntilBonus: - """ - ressource qui represente le temps restant avant d'avoir le bonus - """ - - def __init__(self, time: float, world: World): - self.time = time - self.started_time = int(world[Time]) - - def is_ended(self, world: World): - return world[Time] - self.started_time >= self.time - - def start(self, world: World): - self.started_time = world[Time] - - -class LastPlayerTurn: - """ - un composant qui represente le dernier joueur qui a joué. - """ - - -class HasBonus: - """ - un composant qui represente si l'entité a un bonus - """ - - def __init__(self, bonus: "Bonus", time: float, world: World): - self.bonus = bonus - self.time = time - - self.start_time = world[Time] - - def is_ended(self, world: World): - return world[Time] - self.start_time >= self.time - - def suppr_bonus_from_entity(self, entity: Entity): - match self.bonus: - case Bonus.MULTI: - pass - case Bonus.BIG: - entity[Scale] /= 2 - case Bonus.FAST: - entity[Speed] /= 1.5 - case Bonus.REVERSE: - entity[UpKey], entity[DownKey] = entity[DownKey], entity[UpKey] - - -class Bonus(Enum): - MULTI = 0 - BIG = 1 - FAST = 2 - REVERSE = 3 - @staticmethod - def aleatoire(): - type = random.randint(0, 3) - match type: - case 0: - return Bonus.MULTI - case 1: - return Bonus.BIG - case 2: - return Bonus.FAST - case _: - return Bonus.REVERSE - - @staticmethod - def get_texture(bonus: "Bonus"): - match bonus: - case Bonus.MULTI: - return "multi.png" - case Bonus.BIG: - return "big.png" - case Bonus.FAST: - return "fast.png" - case _: - return "reverse.png" - - -def __spawn_ellements(world: World): - """ - La fonction permet de initializer les ellements de la scene. - """ - - world.new_entity().set(SpriteBundle(("background.jpg"), -5)) - - world.set( - TimeUntilBonus(5, world), - ) - - # Mon mur de gauche - world.new_entity().set( - Origin(Vec2(1, 0)), - Scale(Vec2(10, render.HEIGHT)), - Position(Vec2(70, 0)), - Solid(), - LeftWall(), - CollisionHandler(__bounce_on_left_wall), - ) - - # Mon mur du RN - world.new_entity().set( - Origin(Vec2(0, 0)), - Scale(Vec2(10, render.HEIGHT)), - Position(Vec2(render.WIDTH - 70, 0)), - Solid(), - RightWall(), - CollisionHandler(__bounce_on_right_wall), - ) - - # Mon mur du bas - world.new_entity().set( - Origin(Vec2(0, 0)), - Scale(Vec2(render.WIDTH, 10)), - Position(Vec2(0, render.HEIGHT)), - Solid(), - ) - - # Mon mur du haut - world.new_entity().set( - Origin(Vec2(0, 1)), - Scale(Vec2(render.WIDTH, 10)), - Position(Vec2(0, 0)), - Solid(), - ) - - # Joueur 1 - world.new_entity().set( - SpriteBundle( - "player_1.png", - 0, - Vec2(100, render.HEIGHT / 2), - Vec2(44, 250), - Vec2(0.5), - ), - Solid(), - Player1(), - UpKey("z"), - DownKey("s"), - Speed(1000), - LastPlayerTurn(), - ) - - # Joueur 2 - world.new_entity().set( - SpriteBundle( - "player_2.png", - 0, - Vec2(render.WIDTH - 100, render.HEIGHT / 2), - Vec2(44, 250), - Vec2(0.5), - ), - Solid(), - Player2(), - (UpKey("up"), DownKey("down"), Speed(1000)) - if world[GameMode] == GameMode.TWO - else Speed(300), - ) - - __spawn_ball(world) - - # Initialisation des scores - world.set(Player1Score(0), Player2Score(0)) - - world.new_entity().set( - TextBundle( - "0 - 0", - 10, - 50, - position=Vec2(render.WIDTH / 2, 75), - origin=Vec2(0.5), - ), - Score(), - ) - - -def __spawn_bonus(world: World): - bonus = Bonus.aleatoire() - world.new_entity().set( - SpriteBundle( - Bonus.get_texture(bonus), - 3, - Vec2( - random.randint(200, render.WIDTH - 200), - random.randint(100, render.HEIGHT - 100), + def start(world: World, number: int): + world.new_entity().set( + TextBundle( + str(number), + 2, + 5000, + position=Vec2(render.WIDTH / 2, render.HEIGHT / 2), + origin=Vec2(0.5), ), - Vec2(70), - Vec2(0.5), - ), - bonus, - ) - - -def __spawn_ball(world: World): - """ - Fonction qui fait apparaitre une balle avec une velocitée aleatoire - """ - # random velocité - velocity = Vec2(2 * random.randint(100, 200), random.randint(100, 200)) - - # mouvement a droite ou a gauche - if random.randint(0, 1) == 0: - velocity.x = -velocity.x - - # Balle - world.new_entity().set( - SpriteBundle( - "ball.png", - 0, - Vec2(render.WIDTH / 2, render.HEIGHT / 2), - Vec2(40, 40), - Vec2(0.5), - ), - Ball(), - Velocity(velocity), - CollisionHandler(__collision_with_ball), - ) - - -def __collision_with_ball(a: Entity, b: Entity): - if Player1 in b or Player2 in b: - for player in a.world.query(LastPlayerTurn): - del player[LastPlayerTurn] - b.set(LastPlayerTurn()) - return __bounce_on_player(a, b) - return True - - -def __bonus_touched(ball: Entity, bonus: Entity): - player = ball.world.query(LastPlayerTurn).pop() - match bonus[Bonus]: - case Bonus.MULTI: - __spawn_ball(bonus.world) - __spawn_ball(bonus.world) - ball.world[TimeUntilBonus].start(ball.world) - case Bonus.BIG: - player[Scale] *= 2 - player.set(HasBonus(Bonus.BIG, 10, bonus.world)) - case Bonus.FAST: - player[Speed] *= 1.5 - player.set(HasBonus(Bonus.FAST, 10, bonus.world)) - case Bonus.REVERSE: - for entity in ball.world.query(UpKey): - if LastPlayerTurn in entity: - continue - entity[UpKey], entity[DownKey] = entity[DownKey], entity[UpKey] - - entity.set(HasBonus(Bonus.REVERSE, 10, bonus.world)) - - bonus.destroy() - return False - - -def __bounce_on_player(a: Entity, b: Entity): - """ - Fonction qui decrit se qui se passe lorque la ball entre en collision avec un joueur - """ - if Player1 in b or Player2 in b: - speed = a[Velocity].length - a[Velocity] = a[Velocity].normalized - a[Velocity].y = (a[Position].y - b[Position].y) * 0.005 - a[Velocity] = a[Velocity].normalized * min((speed * 1.1), 1000.0) - return True - - -def __bounce_on_left_wall(a: Entity, b: Entity): - """ - Fonction qui decrit se qui se passe lorque la ball entre en collision avec le mur de gauche - """ - if Ball in b: - world = a.world - world[Player2Score] += 1 - __update_scores(world, b) - return False - return True - - -def __bounce_on_right_wall(a: Entity, b: Entity): - """ - Fonction qui decrit se qui se passe lorque la ball entre en collision avec le mur de droite - """ - if Ball in b: - world = a.world - world[Player1Score] += 1 - __update_scores(world, b) - return False - return True - - -def __move_up(world: World): - """ - La fonction permet de faire bouger les entitees vers le haut. - """ - held = world[Held] - for entity in world.query(UpKey): - if entity[UpKey] in held: - entity[Position] = Vec2( - entity[Position].x, - (entity[Position].y - entity[Speed] * world[Delta]), - ) - - -def __move_down(world: World): - """ - La fonction permet de faire bouger les entitees vers le bas. - """ - held = world[Held] - for entity in world.query(DownKey): - if entity[DownKey] in held: - entity[Position] = Vec2( - entity[Position].x, - (entity[Position].y + entity[Speed] * world[Delta]), - ) - - -def __update_move(world: World): - """ - La fontion permet de faire bouger les entitees vers le haut ou vers le bas. - """ - __move_down(world) - __move_up(world) - - -def __simulate_wall_position(entity: Entity, component_type: type): - """ - Simule une entité afin de trouver lorsqu'elle entrera en collision avec une entité contenant un certain composant. - """ - simulation_entity = entity.world.new_entity() - - def __collision_handler(a: Entity, b: Entity): - entity[CollisionHandler].callback(a, b) - return component_type not in b - - simulation_entity.set( - Position(entity[Position]), - Scale(entity[Scale]), - Velocity(entity[Velocity]), - Origin(entity[Origin]), - CollisionHandler(__collision_handler), - ) - physics.move_entity(simulation_entity, entity[Velocity] * 500) - return simulation_entity - - -def _update_bot(world: World): - """ - Fonction qui update les mouvement du bot - """ - # On récupère la balle la plus proche du bot - ball_query = world.query(Position, Velocity, CollisionHandler) - if ball_query == set(): - return None - ball = max(ball_query, key=lambda entity: entity[Position].y) - - # On récupère le bot et le joueur - bot = world.query(Player2).pop() - player = world.query(Player1).pop() - - # On trouve l'endroit ou la balle va arriver sur le mur de droite - bot.remove(Solid) - right_wall_ball = __simulate_wall_position(ball, RightWall) - right_touch_height = right_wall_ball[Position].y - right_wall_ball.destroy() - bot.set(Solid()) - - # On teste différentes possitions pour voir laquelle la plus éloigné du joueur - # Mais seulement si la balle vas vers la droite car sinon elle touchera le mur - # de gauche sans intervention du bot - if ball[Velocity].x > 0: - bot_base_y = bot[Position].y - target: float = right_touch_height - better_distance = None - for offset in [-100, -50, 0, 50, 100]: - bot[Position].y = right_touch_height + offset - player.remove(Solid) - left_wall_ball = __simulate_wall_position(ball, LeftWall) - player.set(Solid()) - left_touch_height = left_wall_ball[Position].y - left_wall_ball.destroy() - if ( - better_distance is None - or abs(left_touch_height - player[Position].y) > better_distance - ): - better_distance = abs(left_touch_height - player[Position].y) - target = right_touch_height + offset - bot[Position].y = bot_base_y - else: - target = right_touch_height - - # On se déplace vers la meilleure option - diff = target - bot[Position].y - if abs(diff) > 10: - bot[Position].y += (diff / abs(diff)) * bot[Speed] * world[Delta] - - -def __check_bonus_collision(world: World): - """ - Fonction qui permet de voir si un bonus est entrée en collision avec une entité. - """ - - def __collision_handler(a: Entity, b: Entity): - if Bonus in b: - __bonus_touched(a, b) - return False - return True - - for entity in world.query(Bonus): - entity.set(Solid()) - for entity in world.query(Ball): - simulated_ball = world.new_entity() - simulated_ball.set( - Position(entity[Position]), - Scale(entity[Scale]), - Velocity(entity[Velocity]), - Origin(entity[Origin]), - CollisionHandler(__collision_handler), + StartAnimation(world[Time]), ) - physics.move_entity(simulated_ball, entity[Velocity] * world[Delta]) - simulated_ball.destroy() - for entity in world.query(Bonus): - entity.remove(Solid) + @staticmethod + def update(world: World): + """ + Fonction qui permet de mettre a jour l'animation du compte a rebours. + """ -def __update_scores(world: World, ball: Entity): - """ - La fontion permet de mettre a jour les scores. - """ + for animation in world.query(StartAnimation): + time = world[Time] - animation[StartAnimation] - # met a jour le score du joueur 1 et 2 - for panel in world.query(Score): - panel[Text] = f"{world[Player1Score]} - {world[Player2Score]}" - - ball.destroy() - - if world.query(Ball) == set(): - __animation(world, 3) - - -def __animation(world: World, number: int): - world.new_entity().set( - TextBundle( - str(number), - 2, - 5000, - position=Vec2(render.WIDTH / 2, render.HEIGHT / 2), - origin=Vec2(0.5), - ), - StartAnimation(world[Time]), - ) - - -def __update_animation(world: World): - """ - Fonction qui permet de mettre a jour l'animation du compte a rebours. - """ - - for animation in world.query(StartAnimation): - time = world[Time] - animation[StartAnimation] - - if animation[TextSize] > 700: - animation[TextSize] = 5000 / (25 * time + 1) - else: - animation[TextSize] -= 1000 * world[Delta] - if animation[TextSize] < 0: - if int(animation[Text]) > 1 or "space" in world[Held]: - __animation(world, int(animation[Text]) - 1) + if animation[TextSize] > 700: + animation[TextSize] = 5000 / (25 * time + 1) else: - # creation de la balle - __spawn_ball(world) - animation.destroy() + animation[TextSize] -= 1000 * world[Delta] + if animation[TextSize] < 0: + if int(animation[Text]) > 1 or "space" in world[Held]: + StartAnimation.start(world, int(animation[Text]) - 1) + else: + Ball.spawn_ball(world) + animation.destroy() -def __update_bonus_time(world: World): +class ScoreRenderer: """ - Fonction qui permet de mettre à jour les bonus. + Composant marquant l'entité d'affichage du score. """ - for player in world.query(HasBonus): - if not player[HasBonus].is_ended(world): - return None - player[HasBonus].suppr_bonus_from_entity(player) - del player[HasBonus] - world[TimeUntilBonus].start(world) - if world.query(Bonus) == set() and world.query(HasBonus) == set(): - if world[TimeUntilBonus].is_ended(world): - __spawn_bonus(world) + @staticmethod + def render(world: World): + """ + Met à jour le score. + """ + one_players = any(map(lambda e: e[AI].logic == bot_ai, world.query(AI))) + for entity in world.query(ScoreRenderer): + if one_players: + entity[Text] = str(world[Score]) + else: + entity[Text] = " - ".join( + map(lambda e: str(e[PlayerScore]), world.query(PlayerScore)) + ) + + +def __initialize(two: bool): + """ + Créer une initialisation du jeu. + """ + + def __initialize_inner(world: World): + """ + Fait apparaitre tous les elements de la partie. + """ + # Ajout du fond + world.new_entity().set(SpriteBundle("background.jpg", -5)) + + # Ajout des murs du monde + world.new_entity().set( + Position(0, 0), + Scale(render.WIDTH, 10), + Origin(0, 1), + Solid(), + Wall(), + ) + world.new_entity().set( + Position(0, render.HEIGHT), + Scale(render.WIDTH, 10), + Origin(0, 0), + Solid(), + Wall(), + ) + world.new_entity().set( + Position(PLAYER_WALL_DISTANCE - 1, 0), + Scale(10, render.HEIGHT), + Origin(1, 0), + Solid(), + Wall(), + WinWall(), + ) + world.new_entity().set( + Position(render.WIDTH - PLAYER_WALL_DISTANCE + 1, 0), + Scale(10, render.HEIGHT), + Origin(0, 0), + Solid(), + Wall(), + WinWall(), + ) + + # Ajout du joueur de gauche + world.new_entity().set( + SpriteBundle( + "player_1.png", + 0, + position=Vec2(PLAYER_WALL_DISTANCE, render.HEIGHT / 2), + origin=Vec2(1, 0.5), + ), + Solid(), + Player(), + PlayerScore(0), + PlayerSpeed(500), + AI(human_ai("z", "s")), + ) + + # Ajout du joueur de droite + world.new_entity().set( + SpriteBundle( + "player_2.png", + 0, + position=Vec2(render.WIDTH - PLAYER_WALL_DISTANCE, render.HEIGHT / 2), + origin=Vec2(0, 0.5), + ), + Solid(), + Player(), + PlayerScore(0), + PlayerSpeed(500), + AI(human_ai("up", "down") if two else bot_ai), + ) + + # Ajout d'une balle + Ball.spawn_ball(world) + + # On initialize le score + world[Score] = 0 + world.new_entity().set( + TextBundle( + "dsqd", 1, 100, position=Vec2(render.WIDTH / 2, 200), origin=Vec2(0.5) + ), + ScoreRenderer(), + ) + + return __initialize_inner + + +def __update_ai(world: World): + """ + Met à jour le comportement des joueurs. + """ + for entity in world.query(AI): + new_world = world.partial_copy( + Position, + Scale, + Origin, + Velocity, + Solid, + Player, + PlayerSpeed, + Ball, + Wall, + WinWall, + ressources=(Held,), + ) + entity[PlayerTarget] = entity[AI].logic( + new_world, Entity(new_world, entity.identifier) + ) + + +def __apply_target(world: World): + """ + Applique les déplacements cibles des joueurs. + """ + for entity in world.query(Player, PlayerTarget): + diff = entity[PlayerTarget] - entity[Position].y + if abs(diff) > entity[PlayerSpeed] * world[Delta]: + entity[Position].y += diff / abs(diff) * entity[PlayerSpeed] * world[Delta] + else: + entity[Position].y = entity[PlayerTarget] + if entity[Position].y - entity[Scale].y / 2 < 0: + entity[Position].y = entity[Scale].y / 2 + if entity[Position].y + entity[Scale].y / 2 > render.HEIGHT: + entity[Position].y = render.HEIGHT - entity[Scale].y / 2 __SCENE = Scene( - [__spawn_ellements], - [__update_move, __check_bonus_collision, __update_animation, __update_bonus_time], + [], + [__update_ai, __apply_target, StartAnimation.update, ScoreRenderer.render], [], ) ONE_PLAYER = ( Plugin( - [lambda world: world.set(GameMode.ONE)], - [_update_bot], + [__initialize(False)], + [], [], ) + __SCENE @@ -616,9 +348,37 @@ ONE_PLAYER = ( TWO_PLAYER = ( Plugin( - [lambda world: world.set(GameMode.TWO)], + [__initialize(True)], [], [], ) + __SCENE ) + + +def human_ai(up: str, down: str): + def __human_ai(world: World, player: Entity) -> float: + pressed = up in world[Held], down in world[Held] + match pressed: + case (True, False): + return 0 + case (False, True): + return render.HEIGHT + case _: + return player[Position].y + + return __human_ai + + +def bot_ai(world: World, player: Entity) -> float: + player.remove(Solid) + best_distance = float("inf") + best_y = player[Position].y + for entity in world.query(Ball): + distance = physics.simulate( + entity, lambda e: WinWall in e and WinWall.is_my_side(e, player) + ) + if distance < best_distance: + best_distance = distance + best_y = entity[Position].y + return best_y