WIP: Simulation dans une copie du monde #53

Closed
tipragot wants to merge 1 commit from simulation into main
3 changed files with 348 additions and 545 deletions
Showing only changes of commit d994449f1e - Show all commits

View file

@ -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.

View file

@ -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):

View file

@ -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