La librairie Pygame ne fonctionne pas présente dans capytale.

IL faut donc utiliser thonny.

Pygame, c’est une bibliothèque Python qui sert à créer facilement des jeux 2D et des animations interactives.

Elle fournit des “briques” prêtes à l’emploi pour :

  • Fenêtre + boucle de jeu : créer une fenêtre, la rafraîchir à une certaine vitesse (FPS).
  • Événements : lire le clavier, la souris, fermer la fenêtre (pygame.event.get()).
  • Affichage 2D : dessiner des formes (rectangles, cercles…), afficher des images (sprites), gérer la transparence.
  • Collision : grâce aux rectangles (pygame.Rect) et à colliderect.
  • Texte : afficher du texte avec des polices (pygame.font).
  • Temps : mesurer le temps et rendre le mouvement fluide avec Clock et dt.
  • Sons et musique : jouer des effets sonores (pygame.mixer.Sound) et de la musique (pygame.mixer.music).

Le principe central : la “boucle de jeu”

Presque tous les programmes Pygame suivent ce schéma :

  1. Lire les événements (clavier, quitter…)
  2. Mettre à jour la position des objets (joueur, ennemis…)
  3. Dessiner la scène (fond, murs, sprites, score…)
  4. Afficher (pygame.display.flip())
  5. Limiter les FPS (clock.tick(60))

Pré-requis

  1. Installer pygame : pip install pygame
  2. Avoir un dossier assets dans le même répertoire que le code python

Télécharger le dossier assets

import pygame
pygame.init()
LARGEUR, HAUTEUR = 800, 500
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption('1) Fenêtre Pygame')
horloge = pygame.time.Clock()
en_cours = True
while en_cours:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            en_cours = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
            en_cours = False
    ecran.fill((0, 0, 0))
    pygame.display.flip()
    horloge.tick(60)
pygame.quit()

import pygame
pygame.init()
LARGEUR, HAUTEUR = 800, 500
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption('2) Couleurs')
horloge = pygame.time.Clock()
couleur_fond = (20, 20, 30)
en_cours = True
while en_cours:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            en_cours = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                en_cours = False
            elif event.key == pygame.K_1:
                couleur_fond = (30, 30, 40)
            elif event.key == pygame.K_2:
                couleur_fond = (60, 20, 20)
            elif event.key == pygame.K_3:
                couleur_fond = (20, 60, 30)
    ecran.fill(couleur_fond)
    pygame.display.flip()
    horloge.tick(60)
pygame.quit()

import pygame
pygame.init()
LARGEUR, HAUTEUR = 800, 500
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption('3) Rectangle')
horloge = pygame.time.Clock()
rectangle = pygame.Rect(0, 0, 80, 60)
rectangle.center = (LARGEUR//2, HAUTEUR//2)
en_cours = True
while en_cours:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            en_cours = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
            en_cours = False
    ecran.fill((20, 20, 30))
    pygame.draw.rect(ecran, (80, 140, 255), rectangle, border_radius=10)
    pygame.display.flip()
    horloge.tick(60)
pygame.quit()

import pygame
pygame.init()
LARGEUR, HAUTEUR = 800, 500
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption('4) Rectangle au clavier')
horloge = pygame.time.Clock()
rectangle = pygame.Rect(0, 0, 80, 60)
rectangle.center = (LARGEUR//2, HAUTEUR//2)
vitesse = 280.0  # pixels par seconde
en_cours = True
while en_cours:
    dt = horloge.tick(60) / 1000.0
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            en_cours = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
            en_cours = False
    touches = pygame.key.get_pressed()
    dx, dy = 0, 0
    if touches[pygame.K_LEFT]: dx -= 1
    if touches[pygame.K_RIGHT]: dx += 1
    if touches[pygame.K_UP]: dy -= 1
    if touches[pygame.K_DOWN]: dy += 1
    rectangle.x += int(dx * vitesse * dt)
    rectangle.y += int(dy * vitesse * dt)
    # Empêcher de sortir de la fenêtre
    rectangle.clamp_ip(pygame.Rect(0, 0, LARGEUR, HAUTEUR))
    ecran.fill((20, 20, 30))
    pygame.draw.rect(ecran, (80, 140, 255), rectangle, border_radius=10)
    pygame.display.flip()
pygame.quit()

import pygame
from pathlib import Path
pygame.init()
LARGEUR, HAUTEUR = 800, 500
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption('5) Pac-Man au clavier')
horloge = pygame.time.Clock()

# Charger les images
dossier_assets = Path('assets')
fichiers = ['pacman_0.png', 'pacman_1.png', 'pacman_2.png']
frames = []
for nom in fichiers:
    img = pygame.image.load(str(dossier_assets / nom)).convert_alpha()
    img = pygame.transform.smoothscale(img, (64, 64))
    frames.append(img)

def frames_orientees(direction):
    dx, dy = direction
    if (dx, dy) == (1, 0):
        return frames
    if (dx, dy) == (-1, 0):
        return [pygame.transform.flip(f, True, False) for f in frames]
    if (dx, dy) == (0, -1):
        return [pygame.transform.rotate(f, 90) for f in frames]
    if (dx, dy) == (0, 1):
        return [pygame.transform.rotate(f, -90) for f in frames]
    return frames

# Pac-Man (dictionnaire)
pacman = {
    'x': LARGEUR//2,
    'y': HAUTEUR//2,
    'vitesse': 240.0,
    'direction_aff': (1, 0),
    'frame': 0,
    'temps_anim': 0.0,
}

en_cours = True
while en_cours:
    dt = horloge.tick(60) / 1000.0
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            en_cours = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
            en_cours = False

    touches = pygame.key.get_pressed()
    dx, dy = 0, 0
    if touches[pygame.K_LEFT]: dx -= 1
    if touches[pygame.K_RIGHT]: dx += 1
    if touches[pygame.K_UP]: dy -= 1
    if touches[pygame.K_DOWN]: dy += 1

    # Déplacement
    pacman['x'] += dx * pacman['vitesse'] * dt
    pacman['y'] += dy * pacman['vitesse'] * dt

    # Limites fenêtre (pour éviter de sortir)
    pacman['x'] = max(32, min(LARGEUR-32, pacman['x']))
    pacman['y'] = max(32, min(HAUTEUR-32, pacman['y']))

    bouge = (dx != 0 or dy != 0)
    if bouge:
        pacman['direction_aff'] = (dx, dy)
        pacman['temps_anim'] += dt
        if pacman['temps_anim'] >= 1.0/12:
            pacman['temps_anim'] = 0.0
            pacman['frame'] = (pacman['frame'] + 1) % len(frames)
    else:
        pacman['frame'] = 0
        pacman['temps_anim'] = 0.0

    ecran.fill((20, 20, 30))
    fr = frames_orientees(pacman['direction_aff'])
    sprite = fr[pacman['frame']]
    rect = sprite.get_rect(center=(int(pacman['x']), int(pacman['y'])))
    ecran.blit(sprite, rect.topleft)
    pygame.display.flip()

pygame.quit()

import pygame
pygame.init()
LABYRINTHE = [
    '####################',
    '#........#.........#',
    '#.######.#.#####...#',
    '#.#....#.#.....#...#',
    '#.#.##.#.#####.#...#',
    '#...##.#.....#.#...#',
    '###.##.#####.#.###.#',
    '#......#.....#.....#',
    '####################',
]
grille = [list(l) for l in LABYRINTHE]
H = len(grille)
W = len(grille[0])
LARGEUR, HAUTEUR = 900, 520
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption('6) Labyrinthe')
horloge = pygame.time.Clock()
marge = 30
taille_tuile = min((LARGEUR - 2*marge)//W, (HAUTEUR - 2*marge)//H)
taille_tuile = max(16, int(taille_tuile))
ox = (LARGEUR - W*taille_tuile)//2
oy = (HAUTEUR - H*taille_tuile)//2
en_cours = True
while en_cours:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            en_cours = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
            en_cours = False
    ecran.fill((15, 15, 20))
    for y in range(H):
        for x in range(W):
            r = pygame.Rect(ox + x*taille_tuile, oy + y*taille_tuile, taille_tuile, taille_tuile)
            if grille[y][x] == '#':
                pygame.draw.rect(ecran, (70, 70, 85), r)
            else:
                pygame.draw.rect(ecran, (25, 25, 32), r)
    pygame.display.flip()
    horloge.tick(60)
pygame.quit()

import pygame
from pathlib import Path
pygame.init()

LABYRINTHE = [
    '####################',
    '#........#.........#',
    '#.######.#.#####...#',
    '#.#....#.#.....#...#',
    '#.#.##.#.#####.#...#',
    '#...##.#.....#.#...#',
    '###.##.#####.#.###.#',
    '#......#.....#.....#',
    '####################',
]
grille = [list(l) for l in LABYRINTHE]
H = len(grille)
W = len(grille[0])

LARGEUR, HAUTEUR = 900, 520
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption('7) Pac-Man dans un labyrinthe (clavier)')
horloge = pygame.time.Clock()

marge = 30
taille_tuile = min((LARGEUR - 2*marge)//W, (HAUTEUR - 2*marge)//H)
taille_tuile = max(16, int(taille_tuile))
ox = (LARGEUR - W*taille_tuile)//2
oy = (HAUTEUR - H*taille_tuile)//2

def rect_tuile(x_case, y_case):
    return pygame.Rect(ox + x_case*taille_tuile, oy + y_case*taille_tuile, taille_tuile, taille_tuile)

# Préparer la liste des murs (Rect)
murs = []
for y in range(H):
    for x in range(W):
        if grille[y][x] == '#':
            murs.append(rect_tuile(x, y))

# Charger Pac-Man
dossier_assets = Path('assets')
fichiers = ['pacman_0.png', 'pacman_1.png', 'pacman_2.png']
frames_base = []
for nom in fichiers:
    img = pygame.image.load(str(dossier_assets / nom)).convert_alpha()
    img = pygame.transform.smoothscale(img, (int(taille_tuile*0.95), int(taille_tuile*0.95)))
    frames_base.append(img)

def frames_orientees(direction):
    dx, dy = direction
    if (dx, dy) == (1, 0):
        return frames_base
    if (dx, dy) == (-1, 0):
        return [pygame.transform.flip(f, True, False) for f in frames_base]
    if (dx, dy) == (0, -1):
        return [pygame.transform.rotate(f, 90) for f in frames_base]
    if (dx, dy) == (0, 1):
        return [pygame.transform.rotate(f, -90) for f in frames_base]
    return frames_base

def rect_pacman(px, py):
    # hitbox un peu plus petite que la tuile
    s = int(taille_tuile*0.78)
    return pygame.Rect(int(px - s/2), int(py - s/2), s, s)

def collision_mur(r):
    return any(r.colliderect(m) for m in murs)

def dessiner_labyrinthe():
    for y in range(H):
        for x in range(W):
            r = rect_tuile(x, y)
            if grille[y][x] == '#':
                pygame.draw.rect(ecran, (70, 70, 85), r)
            else:
                pygame.draw.rect(ecran, (25, 25, 32), r)

# Pac-Man (dictionnaire)
pacman = {
    'x': ox + 1.5*taille_tuile,
    'y': oy + 1.5*taille_tuile,
    'vitesse': 220.0,
    'direction_aff': (1, 0),
    'frame': 0,
    'temps_anim': 0.0,
}

en_cours = True
while en_cours:
    dt = horloge.tick(60) / 1000.0
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            en_cours = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
            en_cours = False

    touches = pygame.key.get_pressed()
    dx, dy = 0, 0
    if touches[pygame.K_LEFT]: dx -= 1
    if touches[pygame.K_RIGHT]: dx += 1
    if touches[pygame.K_UP]: dy -= 1
    if touches[pygame.K_DOWN]: dy += 1

    vx = dx * pacman['vitesse'] * dt
    vy = dy * pacman['vitesse'] * dt

    if dx != 0 or dy != 0:
        pacman['direction_aff'] = (dx, dy)

    # Déplacement séparé X puis Y (plus simple pour gérer les collisions)
    nx = pacman['x'] + vx
    if not collision_mur(rect_pacman(nx, pacman['y'])):
        pacman['x'] = nx
    ny = pacman['y'] + vy
    if not collision_mur(rect_pacman(pacman['x'], ny)):
        pacman['y'] = ny

    # Animation
    bouge = (dx != 0 or dy != 0)
    if bouge:
        pacman['temps_anim'] += dt
        if pacman['temps_anim'] >= 1.0/12:
            pacman['temps_anim'] = 0.0
            pacman['frame'] = (pacman['frame'] + 1) % len(frames_base)
    else:
        pacman['frame'] = 0
        pacman['temps_anim'] = 0.0

    ecran.fill((15, 15, 20))
    dessiner_labyrinthe()
    fr = frames_orientees(pacman['direction_aff'])
    sprite = fr[pacman['frame']]
    rect_sprite = sprite.get_rect(center=(int(pacman['x']), int(pacman['y'])))
    ecran.blit(sprite, rect_sprite.topleft)
    pygame.display.flip()

pygame.quit()

import pygame
from pathlib import Path

pygame.init()

LABYRINTHE = [
    "####################",
    "#P.......#.........#",
    "#.######.#.#####...#",
    "#.#....#.#.....#...#",
    "#.#.##.#.#####.#...#",
    "#...##.#.....#.#...#",
    "###.##.#####.#.###.#",
    "#......#.....#.....#",
    "##################D#",
]

grille = [list(l) for l in LABYRINTHE]
H = len(grille)
W = len(grille[0])

LARGEUR, HAUTEUR = 900, 520
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption("8) Pastilles + score")
horloge = pygame.time.Clock()
police = pygame.font.SysFont(None, 28)

marge = 30
taille_tuile = min((LARGEUR - 2*marge)//W, (HAUTEUR - 2*marge)//H)
taille_tuile = max(16, int(taille_tuile))
ox = (LARGEUR - W*taille_tuile)//2
oy = (HAUTEUR - H*taille_tuile)//2

def rect_tuile(x_case, y_case):
    return pygame.Rect(ox + x_case*taille_tuile, oy + y_case*taille_tuile, taille_tuile, taille_tuile)

# Murs + positions spéciales
murs = []
case_depart = None
case_porte = None
for y in range(H):
    for x in range(W):
        c = grille[y][x]
        if c == "#":
            murs.append(rect_tuile(x, y))
        elif c == "P":
            case_depart = (x, y)
            grille[y][x] = "."
        elif c == "D":
            case_porte = (x, y)
            grille[y][x] = "."

# Pastilles (toutes les cases de sol sauf départ/porte)
pastilles = set()
for y in range(H):
    for x in range(W):
        if grille[y][x] == "." and (x, y) not in (case_depart, case_porte):
            pastilles.add((x, y))

# Charger Pac-Man
dossier_assets = Path("assets")
fichiers = ["pacman_0.png", "pacman_1.png", "pacman_2.png"]
frames_base = []
for nom in fichiers:
    img = pygame.image.load(str(dossier_assets / nom)).convert_alpha()
    img = pygame.transform.smoothscale(img, (int(taille_tuile*0.95), int(taille_tuile*0.95)))
    frames_base.append(img)

def frames_orientees(direction):
    dx, dy = direction
    if (dx, dy) == (1, 0):
        return frames_base
    if (dx, dy) == (-1, 0):
        return [pygame.transform.flip(f, True, False) for f in frames_base]
    if (dx, dy) == (0, -1):
        return [pygame.transform.rotate(f, 90) for f in frames_base]
    if (dx, dy) == (0, 1):
        return [pygame.transform.rotate(f, -90) for f in frames_base]
    return frames_base

def rect_pacman(px, py):
    s = int(taille_tuile*0.78)
    return pygame.Rect(int(px - s/2), int(py - s/2), s, s)

def collision_mur(r):
    return any(r.colliderect(m) for m in murs)

def case_depuis_position(px, py):
    return (int((px - ox)//taille_tuile), int((py - oy)//taille_tuile))

# Pac-Man
pacman = {
    "x": ox + (case_depart[0] + 0.5)*taille_tuile,
    "y": oy + (case_depart[1] + 0.5)*taille_tuile,
    "vitesse": 220.0,
    "direction_aff": (1, 0),
    "frame": 0,
    "temps_anim": 0.0,
}
score = 0

def dessiner_labyrinthe():
    for y in range(H):
        for x in range(W):
            r = rect_tuile(x, y)
            if grille[y][x] == "#":
                pygame.draw.rect(ecran, (70, 70, 85), r)
            else:
                pygame.draw.rect(ecran, (25, 25, 32), r)
    # Porte (orange)
    r = rect_tuile(case_porte[0], case_porte[1]).inflate(-10, -10)
    pygame.draw.rect(ecran, (220, 120, 60), r, border_radius=10)

def dessiner_pastilles():
    rayon = max(2, taille_tuile//8)
    for (x, y) in pastilles:
        cx = ox + x*taille_tuile + taille_tuile//2
        cy = oy + y*taille_tuile + taille_tuile//2
        pygame.draw.circle(ecran, (240, 200, 60), (cx, cy), rayon)

en_cours = True
while en_cours:
    dt = horloge.tick(60) / 1000.0

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            en_cours = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
            en_cours = False

    touches = pygame.key.get_pressed()
    dx, dy = 0, 0
    if touches[pygame.K_LEFT]: dx -= 1
    if touches[pygame.K_RIGHT]: dx += 1
    if touches[pygame.K_UP]: dy -= 1
    if touches[pygame.K_DOWN]: dy += 1

    vx = dx * pacman["vitesse"] * dt
    vy = dy * pacman["vitesse"] * dt
    if dx != 0 or dy != 0:
        pacman["direction_aff"] = (dx, dy)

    nx = pacman["x"] + vx
    if not collision_mur(rect_pacman(nx, pacman["y"])):
        pacman["x"] = nx
    ny = pacman["y"] + vy
    if not collision_mur(rect_pacman(pacman["x"], ny)):
        pacman["y"] = ny

    # Collecte
    case_p = case_depuis_position(pacman["x"], pacman["y"])
    if case_p in pastilles:
        pastilles.remove(case_p)
        score += 1

    # Animation
    bouge = (dx != 0 or dy != 0)
    if bouge:
        pacman["temps_anim"] += dt
        if pacman["temps_anim"] >= 1.0/12:
            pacman["temps_anim"] = 0.0
            pacman["frame"] = (pacman["frame"] + 1) % len(frames_base)
    else:
        pacman["frame"] = 0
        pacman["temps_anim"] = 0.0

    # Dessin
    ecran.fill((15, 15, 20))
    dessiner_labyrinthe()
    dessiner_pastilles()

    fr = frames_orientees(pacman["direction_aff"])
    sprite = fr[pacman["frame"]]
    ecran.blit(sprite, sprite.get_rect(center=(int(pacman["x"]), int(pacman["y"]))))

    ecran.blit(police.render(f"Score : {score}", True, (235,235,245)), (18, 14))
    pygame.display.flip()

pygame.quit()

import pygame
from pathlib import Path

pygame.init()
try:
    pygame.mixer.init()
except Exception:
    pass

LABYRINTHE = [
    "####################",
    "#P.................#",
    "#.######.#.#####...#",
    "#.#....#.#.....#...#",
    "#.#.##.#.#####.#...#",
    "#...##.#.....#.#...#",
    "###.##.#####.#.###.#",
    "#......#.....#.....#",
    "##################D#",
]

grille = [list(l) for l in LABYRINTHE]
H = len(grille)
W = len(grille[0])

LARGEUR, HAUTEUR = 900, 520
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption("9) Sons : chomp + victoire")
horloge = pygame.time.Clock()
police = pygame.font.SysFont(None, 28)

marge = 30
taille_tuile = min((LARGEUR - 2*marge)//W, (HAUTEUR - 2*marge)//H)
taille_tuile = max(16, int(taille_tuile))
ox = (LARGEUR - W*taille_tuile)//2
oy = (HAUTEUR - H*taille_tuile)//2

def rect_tuile(x_case, y_case):
    return pygame.Rect(ox + x_case*taille_tuile, oy + y_case*taille_tuile, taille_tuile, taille_tuile)

murs = []
case_depart = None
case_porte = None
for y in range(H):
    for x in range(W):
        c = grille[y][x]
        if c == "#":
            murs.append(rect_tuile(x, y))
        elif c == "P":
            case_depart = (x, y); grille[y][x] = "."
        elif c == "D":
            case_porte = (x, y); grille[y][x] = "."

pastilles = set((x,y) for y in range(H) for x in range(W) if grille[y][x]=="." and (x,y) not in (case_depart, case_porte))

dossier_assets = Path("assets")

def charger_son(nom, volume=0.6):
    try:
        if pygame.mixer.get_init() and (dossier_assets / nom).exists():
            s = pygame.mixer.Sound(str(dossier_assets / nom))
            s.set_volume(volume)
            return s
    except Exception:
        pass
    return None

son_chomp = charger_son("chomp.wav", 0.6)
son_victoire = charger_son("win.wav", 0.8)

# Sprites Pac-Man
fichiers = ["pacman_0.png", "pacman_1.png", "pacman_2.png"]
frames_base = []
for nom in fichiers:
    img = pygame.image.load(str(dossier_assets / nom)).convert_alpha()
    img = pygame.transform.smoothscale(img, (int(taille_tuile*0.95), int(taille_tuile*0.95)))
    frames_base.append(img)

def frames_orientees(direction):
    dx, dy = direction
    if (dx, dy) == (1, 0): return frames_base
    if (dx, dy) == (-1, 0): return [pygame.transform.flip(f, True, False) for f in frames_base]
    if (dx, dy) == (0, -1): return [pygame.transform.rotate(f, 90) for f in frames_base]
    if (dx, dy) == (0, 1): return [pygame.transform.rotate(f, -90) for f in frames_base]
    return frames_base

def rect_pacman(px, py):
    s = int(taille_tuile*0.78)
    return pygame.Rect(int(px - s/2), int(py - s/2), s, s)

def collision_mur(r):
    return any(r.colliderect(m) for m in murs)

def case_depuis_position(px, py):
    return (int((px - ox)//taille_tuile), int((py - oy)//taille_tuile))

pacman = {
    "x": ox + (case_depart[0] + 0.5)*taille_tuile,
    "y": oy + (case_depart[1] + 0.5)*taille_tuile,
    "vitesse": 220.0,
    "direction_aff": (1, 0),
    "frame": 0,
    "temps_anim": 0.0,
}
score = 0
message = ""
victoire_jouee = False

def dessiner_labyrinthe():
    for y in range(H):
        for x in range(W):
            r = rect_tuile(x, y)
            pygame.draw.rect(ecran, (70,70,85) if grille[y][x]=="#" else (25,25,32), r)
    r = rect_tuile(case_porte[0], case_porte[1]).inflate(-10, -10)
    pygame.draw.rect(ecran, (220,120,60), r, border_radius=10)

def dessiner_pastilles():
    rayon = max(2, taille_tuile//8)
    for (x, y) in pastilles:
        cx = ox + x*taille_tuile + taille_tuile//2
        cy = oy + y*taille_tuile + taille_tuile//2
        pygame.draw.circle(ecran, (240,200,60), (cx, cy), rayon)

en_cours = True
while en_cours:
    dt = horloge.tick(60) / 1000.0

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            en_cours = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
            en_cours = False

    touches = pygame.key.get_pressed()
    dx, dy = 0, 0
    if touches[pygame.K_LEFT]: dx -= 1
    if touches[pygame.K_RIGHT]: dx += 1
    if touches[pygame.K_UP]: dy -= 1
    if touches[pygame.K_DOWN]: dy += 1

    vx = dx * pacman["vitesse"] * dt
    vy = dy * pacman["vitesse"] * dt
    if dx != 0 or dy != 0:
        pacman["direction_aff"] = (dx, dy)

    nx = pacman["x"] + vx
    if not collision_mur(rect_pacman(nx, pacman["y"])):
        pacman["x"] = nx
    ny = pacman["y"] + vy
    if not collision_mur(rect_pacman(pacman["x"], ny)):
        pacman["y"] = ny

    case_p = case_depuis_position(pacman["x"], pacman["y"])
    if case_p in pastilles:
        pastilles.remove(case_p)
        score += 1
        if son_chomp: son_chomp.play()

    if (len(pastilles) == 0) and (not victoire_jouee):
        victoire_jouee = True
        message = "Bravo ! Toutes les pastilles sont mangées."
        if son_victoire: son_victoire.play()

    bouge = (dx != 0 or dy != 0)
    if bouge:
        pacman["temps_anim"] += dt
        if pacman["temps_anim"] >= 1.0/12:
            pacman["temps_anim"] = 0.0
            pacman["frame"] = (pacman["frame"] + 1) % len(frames_base)
    else:
        pacman["frame"] = 0
        pacman["temps_anim"] = 0.0

    ecran.fill((15,15,20))
    dessiner_labyrinthe()
    dessiner_pastilles()

    fr = frames_orientees(pacman["direction_aff"])
    sprite = fr[pacman["frame"]]
    ecran.blit(sprite, sprite.get_rect(center=(int(pacman["x"]), int(pacman["y"]))))

    ecran.blit(police.render(f"Score : {score}", True, (235,235,245)), (18,14))
    if message:
        ecran.blit(police.render(message, True, (245,245,245)), (18,40))

    pygame.display.flip()

pygame.quit()

import pygame
import random
from pathlib import Path

pygame.init()
try:
    pygame.mixer.init()
except Exception:
    pass

LABYRINTHE = [
    "####################",
    "#P.......#.........#",
    "#.######.#.#####...#",
    "#.#....#.#.....#...#",
    "#.#.##.#.#####.#...#",
    "#...##.#.....#.#...#",
    "###.##.#####.#.###.#",
    "#......#.....#..E..#",
    "##################D#",
]

grille = [list(l) for l in LABYRINTHE]
H = len(grille)
W = len(grille[0])

LARGEUR, HAUTEUR = 900, 520
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption("10) Ennemi + collision")
horloge = pygame.time.Clock()
police = pygame.font.SysFont(None, 28)

marge = 30
taille_tuile = min((LARGEUR - 2*marge)//W, (HAUTEUR - 2*marge)//H)
taille_tuile = max(16, int(taille_tuile))
ox = (LARGEUR - W*taille_tuile)//2
oy = (HAUTEUR - H*taille_tuile)//2

def rect_tuile(x_case, y_case):
    return pygame.Rect(ox + x_case*taille_tuile, oy + y_case*taille_tuile, taille_tuile, taille_tuile)

def centre_case(x_case, y_case):
    return (ox + (x_case + 0.5)*taille_tuile, oy + (y_case + 0.5)*taille_tuile)

# Parse
murs = []
case_depart = None
case_porte = None
case_ennemi = None
for y in range(H):
    for x in range(W):
        c = grille[y][x]
        if c == "#":
            murs.append(rect_tuile(x, y))
        elif c == "P":
            case_depart = (x, y); grille[y][x] = "."
        elif c == "D":
            case_porte = (x, y); grille[y][x] = "."
        elif c == "E":
            case_ennemi = (x, y); grille[y][x] = "."

def est_mur_case(x, y):
    return grille[y][x] == "#"

pastilles = set((x,y) for y in range(H) for x in range(W) if grille[y][x]=="." and (x,y) not in (case_depart, case_porte))

# Sons
dossier_assets = Path("assets")
def charger_son(nom, volume=0.6):
    try:
        if pygame.mixer.get_init() and (dossier_assets / nom).exists():
            s = pygame.mixer.Sound(str(dossier_assets / nom))
            s.set_volume(volume)
            return s
    except Exception:
        pass
    return None

son_chomp = charger_son("chomp.wav", 0.6)
son_mort = charger_son("death.wav", 0.7)

# Pac-Man sprites
fichiers = ["pacman_0.png", "pacman_1.png", "pacman_2.png"]
frames_base = []
for nom in fichiers:
    img = pygame.image.load(str(dossier_assets / nom)).convert_alpha()
    img = pygame.transform.smoothscale(img, (int(taille_tuile*0.95), int(taille_tuile*0.95)))
    frames_base.append(img)

def frames_orientees(direction):
    dx, dy = direction
    if (dx, dy) == (1, 0): return frames_base
    if (dx, dy) == (-1, 0): return [pygame.transform.flip(f, True, False) for f in frames_base]
    if (dx, dy) == (0, -1): return [pygame.transform.rotate(f, 90) for f in frames_base]
    if (dx, dy) == (0, 1): return [pygame.transform.rotate(f, -90) for f in frames_base]
    return frames_base

def rect_pacman(px, py):
    s = int(taille_tuile*0.78)
    return pygame.Rect(int(px - s/2), int(py - s/2), s, s)

def collision_mur(r):
    return any(r.colliderect(m) for m in murs)

def case_depuis_position(px, py):
    return (int((px - ox)//taille_tuile), int((py - oy)//taille_tuile))

# Pac-Man
px0, py0 = centre_case(*case_depart)
pacman = {"x": px0, "y": py0, "vitesse": 220.0, "direction_aff": (1,0), "frame": 0, "temps_anim": 0.0}
score = 0

# Ennemi (déplacement sur la grille)
DIRECTIONS = [(1,0), (-1,0), (0,1), (0,-1)]
OPPOSE = {(1,0):(-1,0), (-1,0):(1,0), (0,1):(0,-1), (0,-1):(0,1)}
ennemi = {"case": case_ennemi, "direction": random.choice(DIRECTIONS), "progression": 0.0, "vitesse_cases": 4.5}

def direction_possible(case, direction):
    x, y = case
    dx, dy = direction
    nx, ny = x + dx, y + dy
    if nx < 0 or nx >= W or ny < 0 or ny >= H:
        return False
    return not est_mur_case(nx, ny)

def mettre_a_jour_ennemi(dt):
    distance = ennemi["vitesse_cases"] * dt
    eps = 1e-6
    while distance > 0:
        if ennemi["progression"] <= eps:
            if not direction_possible(ennemi["case"], ennemi["direction"]):
                choix = DIRECTIONS[:]
                random.shuffle(choix)
                opp = OPPOSE.get(ennemi["direction"])
                if opp in choix:
                    choix.remove(opp)
                    choix.append(opp)
                for d in choix:
                    if direction_possible(ennemi["case"], d):
                        ennemi["direction"] = d
                        break
        restant = 1.0 - ennemi["progression"]
        pas = min(distance, restant)
        ennemi["progression"] += pas
        distance -= pas
        if ennemi["progression"] >= 1.0 - eps:
            x, y = ennemi["case"]
            dx, dy = ennemi["direction"]
            ennemi["case"] = (x+dx, y+dy)
            ennemi["progression"] = 0.0

def position_ennemi_pixels():
    x, y = ennemi["case"]
    dx, dy = ennemi["direction"]
    p = ennemi["progression"]
    return (ox + (x + 0.5 + dx*p)*taille_tuile, oy + (y + 0.5 + dy*p)*taille_tuile)

def dessiner_labyrinthe():
    for y in range(H):
        for x in range(W):
            r = rect_tuile(x, y)
            pygame.draw.rect(ecran, (70,70,85) if grille[y][x]=="#" else (25,25,32), r)
    r = rect_tuile(case_porte[0], case_porte[1]).inflate(-10, -10)
    pygame.draw.rect(ecran, (220,120,60), r, border_radius=10)

def dessiner_pastilles():
    rayon = max(2, taille_tuile//8)
    for (x, y) in pastilles:
        cx = ox + x*taille_tuile + taille_tuile//2
        cy = oy + y*taille_tuile + taille_tuile//2
        pygame.draw.circle(ecran, (240,200,60), (cx, cy), rayon)

etat = "jeu"
message = ""

en_cours = True
while en_cours:
    dt = horloge.tick(60) / 1000.0

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            en_cours = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                en_cours = False
            if event.key == pygame.K_r and etat != "jeu":
                # reset
                px0, py0 = centre_case(*case_depart)
                pacman.update({"x": px0, "y": py0, "direction_aff": (1,0), "frame": 0, "temps_anim": 0.0})
                score = 0
                pastilles = set((x,y) for y in range(H) for x in range(W) if grille[y][x]=="." and (x,y) not in (case_depart, case_porte))
                ennemi.update({"case": case_ennemi, "direction": random.choice(DIRECTIONS), "progression": 0.0})
                etat = "jeu"
                message = ""

    if etat == "jeu":
        touches = pygame.key.get_pressed()
        dx, dy = 0, 0
        if touches[pygame.K_LEFT]: dx -= 1
        if touches[pygame.K_RIGHT]: dx += 1
        if touches[pygame.K_UP]: dy -= 1
        if touches[pygame.K_DOWN]: dy += 1

        vx = dx * pacman["vitesse"] * dt
        vy = dy * pacman["vitesse"] * dt
        if dx != 0 or dy != 0:
            pacman["direction_aff"] = (dx, dy)

        nx = pacman["x"] + vx
        if not collision_mur(rect_pacman(nx, pacman["y"])):
            pacman["x"] = nx
        ny = pacman["y"] + vy
        if not collision_mur(rect_pacman(pacman["x"], ny)):
            pacman["y"] = ny

        case_p = case_depuis_position(pacman["x"], pacman["y"])
        if case_p in pastilles:
            pastilles.remove(case_p)
            score += 1
            if son_chomp: son_chomp.play()

        bouge = (dx != 0 or dy != 0)
        if bouge:
            pacman["temps_anim"] += dt
            if pacman["temps_anim"] >= 1.0/12:
                pacman["temps_anim"] = 0.0
                pacman["frame"] = (pacman["frame"] + 1) % len(frames_base)
        else:
            pacman["frame"] = 0
            pacman["temps_anim"] = 0.0

        # Ennemi + collision
        mettre_a_jour_ennemi(dt)
        ex, ey = position_ennemi_pixels()
        dxp = pacman["x"] - ex
        dyp = pacman["y"] - ey
        if (dxp*dxp + dyp*dyp) < (0.45*taille_tuile)**2:
            etat = "perdu"
            message = "Perdu ! (ennemi) — R pour recommencer"
            if son_mort: son_mort.play()

    # Dessin
    ecran.fill((15,15,20))
    dessiner_labyrinthe()
    dessiner_pastilles()

    ex, ey = position_ennemi_pixels()
    pygame.draw.circle(ecran, (220,60,60), (int(ex), int(ey)), max(5, taille_tuile//3))

    fr = frames_orientees(pacman["direction_aff"])
    sprite = fr[pacman["frame"]]
    ecran.blit(sprite, sprite.get_rect(center=(int(pacman["x"]), int(pacman["y"]))))

    ecran.blit(police.render(f"Score : {score}", True, (235,235,245)), (18,14))
    if message:
        ecran.blit(police.render(message, True, (245,245,245)), (18,40))

    pygame.display.flip()

pygame.quit()

import pygame
import random
from pathlib import Path

pygame.init()
try:
    pygame.mixer.init()
except Exception:
    pass

LABYRINTHE = [
    "####################",
    "#P.......#.....K...#",
    "#.######.#.#####...#",
    "#.#....#.#.....#...#",
    "#.#.##.#.#####.#...#",
    "#...##.#.....#.#...#",
    "###.##.#####.#.###.#",
    "#......#.....#..E..#",
    "##################D#",
]

grille = [list(l) for l in LABYRINTHE]
H = len(grille)
W = len(grille[0])

LARGEUR, HAUTEUR = 900, 520
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption("12) Jeu complet Pac-Man (1 niveau)")
horloge = pygame.time.Clock()
police = pygame.font.SysFont(None, 28)

marge = 30
taille_tuile = min((LARGEUR - 2*marge)//W, (HAUTEUR - 2*marge)//H)
taille_tuile = max(16, int(taille_tuile))
ox = (LARGEUR - W*taille_tuile)//2
oy = (HAUTEUR - H*taille_tuile)//2

def rect_tuile(x_case, y_case):
    return pygame.Rect(ox + x_case*taille_tuile, oy + y_case*taille_tuile, taille_tuile, taille_tuile)

def centre_case(x_case, y_case):
    return (ox + (x_case + 0.5)*taille_tuile, oy + (y_case + 0.5)*taille_tuile)

# Parse
murs = []
case_depart = None
case_porte = None
case_ennemi = None
case_cle = None
for y in range(H):
    for x in range(W):
        c = grille[y][x]
        if c == "#":
            murs.append(rect_tuile(x, y))
        elif c == "P":
            case_depart = (x, y); grille[y][x] = "."
        elif c == "D":
            case_porte = (x, y); grille[y][x] = "."
        elif c == "E":
            case_ennemi = (x, y); grille[y][x] = "."
        elif c == "K":
            case_cle = (x, y); grille[y][x] = "."

def est_mur_case(x, y):
    return grille[y][x] == "#"

def creer_pastilles():
    return set((x,y) for y in range(H) for x in range(W) if grille[y][x]=="." and (x,y) not in (case_depart, case_porte, case_cle))

# Sons + musique
dossier_assets = Path("assets")

def charger_son(nom, volume=0.6):
    try:
        if pygame.mixer.get_init() and (dossier_assets / nom).exists():
            s = pygame.mixer.Sound(str(dossier_assets / nom))
            s.set_volume(volume)
            return s
    except Exception:
        pass
    return None

son_chomp = charger_son("chomp.wav", 0.6)
son_cle = charger_son("key.wav", 0.7)
son_porte = charger_son("door.wav", 0.7)
son_mort = charger_son("death.wav", 0.7)
son_victoire = charger_son("win.wav", 0.8)

musique = dossier_assets / "music.ogg"
if pygame.mixer.get_init() and musique.exists():
    try:
        pygame.mixer.music.load(str(musique))
        pygame.mixer.music.set_volume(0.25)
        pygame.mixer.music.play(-1)
    except Exception:
        pass

# Sprites Pac-Man
fichiers = ["pacman_0.png", "pacman_1.png", "pacman_2.png"]
frames_base = []
for nom in fichiers:
    img = pygame.image.load(str(dossier_assets / nom)).convert_alpha()
    img = pygame.transform.smoothscale(img, (int(taille_tuile*0.95), int(taille_tuile*0.95)))
    frames_base.append(img)

def frames_orientees(direction):
    dx, dy = direction
    if (dx, dy) == (1, 0): return frames_base
    if (dx, dy) == (-1, 0): return [pygame.transform.flip(f, True, False) for f in frames_base]
    if (dx, dy) == (0, -1): return [pygame.transform.rotate(f, 90) for f in frames_base]
    if (dx, dy) == (0, 1): return [pygame.transform.rotate(f, -90) for f in frames_base]
    return frames_base

def rect_pacman(px, py):
    s = int(taille_tuile*0.78)
    return pygame.Rect(int(px - s/2), int(py - s/2), s, s)

def collision_mur(r):
    return any(r.colliderect(m) for m in murs)

def case_depuis_position(px, py):
    return (int((px - ox)//taille_tuile), int((py - oy)//taille_tuile))

# Ennemi (grid)
DIRECTIONS = [(1,0), (-1,0), (0,1), (0,-1)]
OPPOSE = {(1,0):(-1,0), (-1,0):(1,0), (0,1):(0,-1), (0,-1):(0,1)}

def direction_possible(case, direction):
    x, y = case
    dx, dy = direction
    nx, ny = x + dx, y + dy
    if nx < 0 or nx >= W or ny < 0 or ny >= H:
        return False
    return not est_mur_case(nx, ny)

def mettre_a_jour_ennemi(ennemi, dt):
    distance = ennemi["vitesse_cases"] * dt
    eps = 1e-6
    while distance > 0:
        if ennemi["progression"] <= eps:
            if not direction_possible(ennemi["case"], ennemi["direction"]):
                choix = DIRECTIONS[:]
                random.shuffle(choix)
                opp = OPPOSE.get(ennemi["direction"])
                if opp in choix:
                    choix.remove(opp); choix.append(opp)
                for d in choix:
                    if direction_possible(ennemi["case"], d):
                        ennemi["direction"] = d
                        break
        restant = 1.0 - ennemi["progression"]
        pas = min(distance, restant)
        ennemi["progression"] += pas
        distance -= pas
        if ennemi["progression"] >= 1.0 - eps:
            x, y = ennemi["case"]
            dx, dy = ennemi["direction"]
            ennemi["case"] = (x+dx, y+dy)
            ennemi["progression"] = 0.0

def position_ennemi_pixels(ennemi):
    x, y = ennemi["case"]
    dx, dy = ennemi["direction"]
    p = ennemi["progression"]
    return (ox + (x + 0.5 + dx*p)*taille_tuile, oy + (y + 0.5 + dy*p)*taille_tuile)

def dessiner_labyrinthe(porte_ouverte):
    for y in range(H):
        for x in range(W):
            r = rect_tuile(x, y)
            pygame.draw.rect(ecran, (70,70,85) if grille[y][x]=="#" else (25,25,32), r)
    r = rect_tuile(case_porte[0], case_porte[1]).inflate(-10, -10)
    couleur = (80,220,140) if porte_ouverte else (220,120,60)
    pygame.draw.rect(ecran, couleur, r, border_radius=10)

def dessiner_pastilles(pastilles):
    rayon = max(2, taille_tuile//8)
    for (x, y) in pastilles:
        cx = ox + x*taille_tuile + taille_tuile//2
        cy = oy + y*taille_tuile + taille_tuile//2
        pygame.draw.circle(ecran, (240,200,60), (cx, cy), rayon)

def dessiner_cle(inventaire):
    if inventaire["cle"] == 0:
        cx = ox + case_cle[0]*taille_tuile + taille_tuile//2
        cy = oy + case_cle[1]*taille_tuile + taille_tuile//2
        pygame.draw.rect(ecran, (80,220,140), pygame.Rect(cx-6, cy-6, 12, 12), border_radius=3)

def reinitialiser():
    px0, py0 = centre_case(*case_depart)
    pac = {"x": px0, "y": py0, "vitesse": 220.0, "direction_aff": (1,0), "frame": 0, "temps_anim": 0.0}
    inv = {"cle": 0}
    pts = creer_pastilles()
    sc = 0
    ene = {"case": case_ennemi, "direction": random.choice(DIRECTIONS), "progression": 0.0, "vitesse_cases": 4.5}
    return pac, inv, pts, sc, ene

pacman, inventaire, pastilles, score, ennemi = reinitialiser()
etat = "jeu"
message = ""

en_cours = True
while en_cours:
    dt = horloge.tick(60) / 1000.0

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            en_cours = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                en_cours = False
            if event.key == pygame.K_r and etat != "jeu":
                pacman, inventaire, pastilles, score, ennemi = reinitialiser()
                etat = "jeu"
                message = ""

    porte_ouverte = (inventaire["cle"] > 0) and (len(pastilles) == 0)

    if etat == "jeu":
        touches = pygame.key.get_pressed()
        dx, dy = 0, 0
        if touches[pygame.K_LEFT]: dx -= 1
        if touches[pygame.K_RIGHT]: dx += 1
        if touches[pygame.K_UP]: dy -= 1
        if touches[pygame.K_DOWN]: dy += 1

        vx = dx * pacman["vitesse"] * dt
        vy = dy * pacman["vitesse"] * dt
        if dx != 0 or dy != 0:
            pacman["direction_aff"] = (dx, dy)

        nx = pacman["x"] + vx
        if not collision_mur(rect_pacman(nx, pacman["y"])):
            pacman["x"] = nx
        ny = pacman["y"] + vy
        if not collision_mur(rect_pacman(pacman["x"], ny)):
            pacman["y"] = ny

        case_p = case_depuis_position(pacman["x"], pacman["y"])

        if case_p in pastilles:
            pastilles.remove(case_p)
            score += 1
            if son_chomp: son_chomp.play()

        if inventaire["cle"] == 0 and case_p == case_cle:
            inventaire["cle"] = 1
            if son_cle: son_cle.play()

        porte_ouverte = (inventaire["cle"] > 0) and (len(pastilles) == 0)
        if porte_ouverte and case_p == case_porte:
            etat = "gagne"
            message = "Victoire ! Appuie sur R pour rejouer"
            if son_porte: son_porte.play()
            if son_victoire: son_victoire.play()

        # Animation
        bouge = (dx != 0 or dy != 0)
        if bouge:
            pacman["temps_anim"] += dt
            if pacman["temps_anim"] >= 1.0/12:
                pacman["temps_anim"] = 0.0
                pacman["frame"] = (pacman["frame"] + 1) % len(frames_base)
        else:
            pacman["frame"] = 0
            pacman["temps_anim"] = 0.0

        # Ennemi + collision
        mettre_a_jour_ennemi(ennemi, dt)
        ex, ey = position_ennemi_pixels(ennemi)
        dxp = pacman["x"] - ex
        dyp = pacman["y"] - ey
        if (dxp*dxp + dyp*dyp) < (0.45*taille_tuile)**2:
            etat = "perdu"
            message = "Perdu ! Appuie sur R pour recommencer"
            if son_mort: son_mort.play()

    # Dessin
    ecran.fill((15,15,20))
    dessiner_labyrinthe(porte_ouverte)
    dessiner_pastilles(pastilles)
    dessiner_cle(inventaire)

    ex, ey = position_ennemi_pixels(ennemi)
    pygame.draw.circle(ecran, (220,60,60), (int(ex), int(ey)), max(5, taille_tuile//3))

    fr = frames_orientees(pacman["direction_aff"])
    sprite = fr[pacman["frame"]]
    ecran.blit(sprite, sprite.get_rect(center=(int(pacman["x"]), int(pacman["y"]))))

    hud = f"Score : {score}   Clé : {inventaire['cle']}   Pastilles : {len(pastilles)}"
    ecran.blit(police.render(hud, True, (235,235,245)), (18,14))
    if message:
        ecran.blit(police.render(message, True, (245,245,245)), (18,40))

    pygame.display.flip()

pygame.quit()

import pygame
import random
from pathlib import Path

# ============================================================
# 1) INITIALISATION DE PYGAME
# ============================================================

pygame.init()
try:
    pygame.mixer.init()  # audio (peut échouer sur certaines machines)
except Exception:
    pass

# ============================================================
# 2) DESCRIPTION DU NIVEAU (LABYRINTHE)
#    # : mur
#    . : sol (où on peut marcher)
#    P : position de départ du joueur
#    K : clé
#    E : ennemi
#    D : porte de sortie
#
# CORRECTION DU BUG "LABYRINTHE FERMÉ"
# - Dans la version d'origine, le labyrinthe était séparé en 2 zones non connectées :
#   Pac-Man démarrait dans une zone, mais la clé + la porte étaient dans l'autre,
#   donc impossible de gagner.
# - Ici, on a créé un passage en remplaçant un seul '#' par '.' (ligne 4).
# ============================================================

LABYRINTHE = [
    "####################",
    "#P.......#.....K...#",
    "#.######.#.#####...#",
    "#.#....#.......#...#",  # <-- ouverture (à la place du # en colonne 10)
    "#.#.##.#.#####.#...#",
    "#...##.#.....#.#...#",
    "###.##.#####.#.###.#",
    "#......#.....#..E..#",
    "##################D#",
]

# On transforme chaque ligne (str) en liste de caractères modifiables
grille = [list(ligne) for ligne in LABYRINTHE]
H = len(grille)       # hauteur en cases
W = len(grille[0])    # largeur en cases

# ============================================================
# 3) FENÊTRE / AFFICHAGE
# ============================================================

LARGEUR, HAUTEUR = 900, 520
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption("12) Jeu complet Pac-Man (1 niveau)")
horloge = pygame.time.Clock()
police = pygame.font.SysFont(None, 28)

# Marges et taille des tuiles (cases) en pixels
marge = 30
taille_tuile = min((LARGEUR - 2 * marge) // W, (HAUTEUR - 2 * marge) // H)
taille_tuile = max(16, int(taille_tuile))  # sécurité : jamais trop petit

# Décalage pour centrer le labyrinthe dans la fenêtre
ox = (LARGEUR - W * taille_tuile) // 2
oy = (HAUTEUR - H * taille_tuile) // 2

def rect_tuile(x_case: int, y_case: int) -> pygame.Rect:
    """Rectangle en pixels correspondant à la case (x_case, y_case)."""
    return pygame.Rect(
        ox + x_case * taille_tuile,
        oy + y_case * taille_tuile,
        taille_tuile,
        taille_tuile
    )

def centre_case(x_case: int, y_case: int) -> tuple[float, float]:
    """Coordonnées (pixels) du centre de la case (x_case, y_case)."""
    return (
        ox + (x_case + 0.5) * taille_tuile,
        oy + (y_case + 0.5) * taille_tuile
    )

# ============================================================
# 4) PARSING DU LABYRINTHE : on repère murs, départ, porte, ennemi, clé
# ============================================================

murs: list[pygame.Rect] = []
case_depart = None
case_porte = None
case_ennemi = None
case_cle = None

for y in range(H):
    for x in range(W):
        c = grille[y][x]
        if c == "#":
            murs.append(rect_tuile(x, y))  # mur : on stocke son rectangle pour les collisions
        elif c == "P":
            case_depart = (x, y)
            grille[y][x] = "."  # on remet du sol à la place de 'P'
        elif c == "D":
            case_porte = (x, y)
            grille[y][x] = "."
        elif c == "E":
            case_ennemi = (x, y)
            grille[y][x] = "."
        elif c == "K":
            case_cle = (x, y)
            grille[y][x] = "."

def est_mur_case(x: int, y: int) -> bool:
    """Vrai si la case (x, y) est un mur."""
    return grille[y][x] == "#"

# ============================================================
# 5) PASTILLES (PIÈCES) À RAMASSER
#
# CORRECTION "TROP DE PIÈCES"
# - Avant : une pastille sur TOUTES les cases '.' (donc énormément).
# - Maintenant : on met une pastille sur ~1 case sur 2 en damier (moins long,
#   mais on conserve l'idée d'exploration).
#
# IMPORTANT : on ne met des pastilles que sur les cases réellement accessibles
# depuis le départ, pour éviter une situation où il resterait des pastilles
# dans une zone isolée (ce qui bloquerait la victoire).
# ============================================================

DIRECTIONS = [(1, 0), (-1, 0), (0, 1), (0, -1)]

def cases_accessibles_depuis(depart: tuple[int, int]) -> set[tuple[int, int]]:
    """Retourne l'ensemble des cases atteignables depuis 'depart' (BFS)."""
    a_visiter = [depart]
    vues = {depart}
    while a_visiter:
        x, y = a_visiter.pop(0)
        for dx, dy in DIRECTIONS:
            nx, ny = x + dx, y + dy
            if 0 <= nx < W and 0 <= ny < H and not est_mur_case(nx, ny):
                if (nx, ny) not in vues:
                    vues.add((nx, ny))
                    a_visiter.append((nx, ny))
    return vues

def creer_pastilles() -> set[tuple[int, int]]:
    """Crée l'ensemble des positions de pastilles à ramasser."""
    accessibles = cases_accessibles_depuis(case_depart)

    pastilles = set()
    for y in range(H):
        for x in range(W):
            # Une pastille peut exister uniquement sur du sol ('.')
            if grille[y][x] != ".":
                continue

            # On évite les cases spéciales
            if (x, y) in (case_depart, case_porte, case_cle):
                continue

            # Sécurité : uniquement dans la zone atteignable
            if (x, y) not in accessibles:
                continue

            # Réduction : "damier" => environ 50% des cases seulement
            if (x + y) % 2 == 0:
                pastilles.add((x, y))

    return pastilles

# ============================================================
# 6) SONS + MUSIQUE
# ============================================================

dossier_assets = Path("assets")

def charger_son(nom: str, volume: float = 0.6):
    """Charge un son si possible, sinon renvoie None."""
    try:
        if pygame.mixer.get_init() and (dossier_assets / nom).exists():
            s = pygame.mixer.Sound(str(dossier_assets / nom))
            s.set_volume(volume)
            return s
    except Exception:
        pass
    return None

son_chomp = charger_son("chomp.wav", 0.6)
son_cle = charger_son("key.wav", 0.7)
son_porte = charger_son("door.wav", 0.7)
son_mort = charger_son("death.wav", 0.7)
son_victoire = charger_son("win.wav", 0.8)

musique = dossier_assets / "music.ogg"
if pygame.mixer.get_init() and musique.exists():
    try:
        pygame.mixer.music.load(str(musique))
        pygame.mixer.music.set_volume(0.25)
        pygame.mixer.music.play(-1)  # boucle infinie
    except Exception:
        pass

# ============================================================
# 7) SPRITES PAC-MAN (images)
# ============================================================

fichiers = ["pacman_0.png", "pacman_1.png", "pacman_2.png"]
frames_base = []
for nom in fichiers:
    img = pygame.image.load(str(dossier_assets / nom)).convert_alpha()
    img = pygame.transform.smoothscale(
        img,
        (int(taille_tuile * 0.95), int(taille_tuile * 0.95))
    )
    frames_base.append(img)

def frames_orientees(direction: tuple[int, int]) -> list[pygame.Surface]:
    """Retourne la liste d'images tournée selon la direction affichée."""
    dx, dy = direction
    if (dx, dy) == (1, 0):
        return frames_base
    if (dx, dy) == (-1, 0):
        return [pygame.transform.flip(f, True, False) for f in frames_base]
    if (dx, dy) == (0, -1):
        return [pygame.transform.rotate(f, 90) for f in frames_base]
    if (dx, dy) == (0, 1):
        return [pygame.transform.rotate(f, -90) for f in frames_base]
    return frames_base

def rect_pacman(px: float, py: float) -> pygame.Rect:
    """Rectangle de collision du Pac-Man (plus petit que la case)."""
    s = int(taille_tuile * 0.78)
    return pygame.Rect(int(px - s / 2), int(py - s / 2), s, s)

def collision_mur(r: pygame.Rect) -> bool:
    """Vrai si le rectangle r touche au moins un mur."""
    return any(r.colliderect(m) for m in murs)

def case_depuis_position(px: float, py: float) -> tuple[int, int]:
    """Transforme des coordonnées pixels en coordonnées de case (x, y)."""
    return (int((px - ox) // taille_tuile), int((py - oy) // taille_tuile))

# ============================================================
# 8) ENNEMI (MOUVEMENT SUR LA GRILLE)
# ============================================================

OPPOSE = {(1, 0): (-1, 0), (-1, 0): (1, 0), (0, 1): (0, -1), (0, -1): (0, 1)}

def direction_possible(case: tuple[int, int], direction: tuple[int, int]) -> bool:
    """Vrai si on peut aller de 'case' dans 'direction' sans sortir ni toucher un mur."""
    x, y = case
    dx, dy = direction
    nx, ny = x + dx, y + dy
    if nx < 0 or nx >= W or ny < 0 or ny >= H:
        return False
    return not est_mur_case(nx, ny)

def mettre_a_jour_ennemi(ennemi: dict, dt: float) -> None:
    """
    Met à jour la position de l'ennemi.
    - ennemi["case"] : la case actuelle (grille)
    - ennemi["direction"] : direction actuelle (dx, dy)
    - ennemi["progression"] : avancement (0.0 -> 1.0) pour passer à la case suivante
    - ennemi["vitesse_cases"] : nombre de cases par seconde
    """
    distance = ennemi["vitesse_cases"] * dt  # en "cases"
    eps = 1e-6

    while distance > 0:
        # Si l'ennemi est pile au centre d'une case, on peut décider de tourner
        if ennemi["progression"] <= eps:
            if not direction_possible(ennemi["case"], ennemi["direction"]):
                # Mur devant : on choisit une direction possible
                choix = DIRECTIONS[:]
                random.shuffle(choix)

                # On évite de faire demi-tour si possible (mais on le garde en dernier recours)
                opp = OPPOSE.get(ennemi["direction"])
                if opp in choix:
                    choix.remove(opp)
                    choix.append(opp)

                for d in choix:
                    if direction_possible(ennemi["case"], d):
                        ennemi["direction"] = d
                        break

        # Avancer vers la case suivante
        restant = 1.0 - ennemi["progression"]     # ce qu'il reste à parcourir avant la prochaine case
        pas = min(distance, restant)              # on avance au max jusqu'à la prochaine case
        ennemi["progression"] += pas
        distance -= pas

        # Si on a atteint (ou dépassé) la case suivante, on "valide" le déplacement
        if ennemi["progression"] >= 1.0 - eps:
            x, y = ennemi["case"]
            dx, dy = ennemi["direction"]
            ennemi["case"] = (x + dx, y + dy)
            ennemi["progression"] = 0.0

def position_ennemi_pixels(ennemi: dict) -> tuple[float, float]:
    """Calcule la position en pixels de l'ennemi (interpolée entre 2 cases)."""
    x, y = ennemi["case"]
    dx, dy = ennemi["direction"]
    p = ennemi["progression"]
    return (
        ox + (x + 0.5 + dx * p) * taille_tuile,
        oy + (y + 0.5 + dy * p) * taille_tuile
    )

# ============================================================
# 9) DESSIN (LABYRINTHE, PASTILLES, CLÉ)
# ============================================================

def dessiner_labyrinthe(porte_ouverte: bool) -> None:
    """Dessine les murs et le sol + la porte (ouverte/fermée)."""
    for y in range(H):
        for x in range(W):
            r = rect_tuile(x, y)
            pygame.draw.rect(
                ecran,
                (70, 70, 85) if grille[y][x] == "#" else (25, 25, 32),
                r
            )

    # La porte est un rectangle plus petit dans sa case
    r = rect_tuile(case_porte[0], case_porte[1]).inflate(-10, -10)
    couleur = (80, 220, 140) if porte_ouverte else (220, 120, 60)
    pygame.draw.rect(ecran, couleur, r, border_radius=10)

def dessiner_pastilles(pastilles: set[tuple[int, int]]) -> None:
    """Dessine toutes les pastilles encore présentes."""
    rayon = max(2, taille_tuile // 8)
    for (x, y) in pastilles:
        cx = ox + x * taille_tuile + taille_tuile // 2
        cy = oy + y * taille_tuile + taille_tuile // 2
        pygame.draw.circle(ecran, (240, 200, 60), (cx, cy), rayon)

def dessiner_cle(inventaire: dict) -> None:
    """Dessine la clé si elle n'a pas encore été ramassée."""
    if inventaire["cle"] == 0:
        cx = ox + case_cle[0] * taille_tuile + taille_tuile // 2
        cy = oy + case_cle[1] * taille_tuile + taille_tuile // 2
        pygame.draw.rect(
            ecran,
            (80, 220, 140),
            pygame.Rect(cx - 6, cy - 6, 12, 12),
            border_radius=3
        )

# ============================================================
# 10) RÉINITIALISATION (RECOMMENCER UNE PARTIE)
# ============================================================

def reinitialiser():
    """Remet le jeu dans son état de départ."""
    px0, py0 = centre_case(*case_depart)

    pac = {
        "x": px0,
        "y": py0,
        "vitesse": 220.0,          # pixels par seconde
        "direction_aff": (1, 0),   # direction affichée (sprite)
        "frame": 0,                # image actuelle (animation)
        "temps_anim": 0.0,         # accumulateur de temps pour l'animation
    }

    inv = {"cle": 0}

    pts = creer_pastilles()
    sc = 0

    ene = {
        "case": case_ennemi,
        "direction": random.choice(DIRECTIONS),
        "progression": 0.0,
        "vitesse_cases": 4.5,      # cases par seconde
    }

    return pac, inv, pts, sc, ene

pacman, inventaire, pastilles, score, ennemi = reinitialiser()

etat = "jeu"      # "jeu" / "gagne" / "perdu"
message = ""

# ============================================================
# 11) BOUCLE PRINCIPALE DU JEU
# ============================================================

en_cours = True
while en_cours:
    dt = horloge.tick(60) / 1000.0  # temps écoulé depuis la frame précédente (en secondes)

    # -----------------------------
    # 11a) GESTION DES ÉVÉNEMENTS
    # -----------------------------
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            en_cours = False

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                en_cours = False
            if event.key == pygame.K_r and etat != "jeu":
                pacman, inventaire, pastilles, score, ennemi = reinitialiser()
                etat = "jeu"
                message = ""

    # La porte s'ouvre quand on a la clé ET qu'il ne reste plus de pastilles
    porte_ouverte = (inventaire["cle"] > 0) and (len(pastilles) == 0)

    # -----------------------------
    # 11b) LOGIQUE DE JEU (si on joue)
    # -----------------------------
    if etat == "jeu":
        touches = pygame.key.get_pressed()
        dx, dy = 0, 0

        if touches[pygame.K_LEFT]:
            dx -= 1
        if touches[pygame.K_RIGHT]:
            dx += 1
        if touches[pygame.K_UP]:
            dy -= 1
        if touches[pygame.K_DOWN]:
            dy += 1

        # Vitesse * dt => déplacement en pixels
        vx = dx * pacman["vitesse"] * dt
        vy = dy * pacman["vitesse"] * dt

        # Direction affichée du sprite
        if dx != 0 or dy != 0:
            pacman["direction_aff"] = (dx, dy)

        # Déplacement + collisions : on teste séparément X puis Y
        nx = pacman["x"] + vx
        if not collision_mur(rect_pacman(nx, pacman["y"])):
            pacman["x"] = nx

        ny = pacman["y"] + vy
        if not collision_mur(rect_pacman(pacman["x"], ny)):
            pacman["y"] = ny

        # On récupère la case où se trouve le joueur
        case_p = case_depuis_position(pacman["x"], pacman["y"])

        # Ramassage d'une pastille
        if case_p in pastilles:
            pastilles.remove(case_p)
            score += 1
            if son_chomp:
                son_chomp.play()

        # Ramassage de la clé
        if inventaire["cle"] == 0 and case_p == case_cle:
            inventaire["cle"] = 1
            if son_cle:
                son_cle.play()

        # Victoire si la porte est ouverte et qu'on marche dessus
        porte_ouverte = (inventaire["cle"] > 0) and (len(pastilles) == 0)
        if porte_ouverte and case_p == case_porte:
            etat = "gagne"
            message = "Victoire ! Appuie sur R pour rejouer"
            if son_porte:
                son_porte.play()
            if son_victoire:
                son_victoire.play()

        # Animation du Pac-Man (change d'image quand il bouge)
        bouge = (dx != 0 or dy != 0)
        if bouge:
            pacman["temps_anim"] += dt
            if pacman["temps_anim"] >= 1.0 / 12:  # 12 images par seconde
                pacman["temps_anim"] = 0.0
                pacman["frame"] = (pacman["frame"] + 1) % len(frames_base)
        else:
            pacman["frame"] = 0
            pacman["temps_anim"] = 0.0

        # Ennemi + collision avec le joueur
        mettre_a_jour_ennemi(ennemi, dt)
        ex, ey = position_ennemi_pixels(ennemi)

        dxp = pacman["x"] - ex
        dyp = pacman["y"] - ey
        if (dxp * dxp + dyp * dyp) < (0.45 * taille_tuile) ** 2:
            etat = "perdu"
            message = "Perdu ! Appuie sur R pour recommencer"
            if son_mort:
                son_mort.play()

    # -----------------------------
    # 11c) DESSIN
    # -----------------------------
    ecran.fill((15, 15, 20))
    dessiner_labyrinthe(porte_ouverte)
    dessiner_pastilles(pastilles)
    dessiner_cle(inventaire)

    # Ennemi (cercle rouge)
    ex, ey = position_ennemi_pixels(ennemi)
    pygame.draw.circle(
        ecran,
        (220, 60, 60),
        (int(ex), int(ey)),
        max(5, taille_tuile // 3)
    )

    # Pac-Man (sprite orienté)
    fr = frames_orientees(pacman["direction_aff"])
    sprite = fr[pacman["frame"]]
    ecran.blit(sprite, sprite.get_rect(center=(int(pacman["x"]), int(pacman["y"]))))

    # HUD (infos en haut)
    hud = f"Score : {score}   Clé : {inventaire['cle']}   Pastilles : {len(pastilles)}"
    ecran.blit(police.render(hud, True, (235, 235, 245)), (18, 14))

    if message:
        ecran.blit(police.render(message, True, (245, 245, 245)), (18, 40))

    pygame.display.flip()

pygame.quit()

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *