snakeia.py

Created by maelg0000

Created on January 23, 2026

7.08 KB


# Snake IA pour NumWorks (MicroPython 1.17)
from kandinsky import fill_rect, draw_string
import ion, time, random

# ------------------ Réglages ------------------
CELL = 10                  # taille d'une case en pixels
GRID_W = 32                # 32*10 = 320 px (largeur écran)
GRID_H = 22                # 22*10 = 220 px (hauteur écran)
OX = 0                     # offset X
OY = 0                     # offset Y
DELAY = 0.05               # vitesse (secondes)
MAX_STEPS = 20000          # sécurité anti-boucle infinie

# Couleurs (RGB)
COL_BG    = (15, 15, 20)
COL_GRID  = (25, 25, 35)
COL_SNAKE = (60, 220, 120)
COL_HEAD  = (40, 180, 90)
COL_FOOD  = (240, 80, 80)
COL_TEXT  = (230, 230, 230)

# Directions: (dx,dy)
DIRS = [(1,0), (-1,0), (0,1), (0,-1)]

# ------------------ Outils dessin ------------------
def cell_rect(x, y):
  return OX + x*CELL, OY + y*CELL, CELL, CELL

def draw_cell(x, y, col):
  rx, ry, rw, rh = cell_rect(x, y)
  fill_rect(rx, ry, rw, rh, col)

def clear_screen():
  fill_rect(0, 0, 320, 240, COL_BG)

def draw_grid_lines():
  # optionnel, léger : quadrillage discret
  # (désactivable si tu veux plus de vitesse)
  for x in range(GRID_W):
    fill_rect(OX + x*CELL, OY, 1, GRID_H*CELL, COL_GRID)
  for y in range(GRID_H):
    fill_rect(OX, OY + y*CELL, GRID_W*CELL, 1, COL_GRID)

# ------------------ Logique ------------------
def inside(x, y):
  return 0 <= x < GRID_W and 0 <= y < GRID_H

def spawn_food(snake_set):
  # essaie aléatoire, puis fallback linéaire si besoin
  for _ in range(200):
    fx = random.randrange(GRID_W)
    fy = random.randrange(GRID_H)
    if (fx, fy) not in snake_set:
      return (fx, fy)
  for y in range(GRID_H):
    for x in range(GRID_W):
      if (x, y) not in snake_set:
        return (x, y)
  return None  # plus de place (victoire)

def bfs_next_move(head, food, snake, snake_set):
  """
  BFS sur la grille pour trouver le 1er pas vers la nourriture.
  On évite les cases occupées par le corps, SAUF la queue (car elle bouge).
  """
  hx, hy = head
  fx, fy = food

  # la queue va se libérer si on ne mange pas
  tail = snake[-1]

  # visited et parent: on stocke dans des listes 2D "compactes"
  # parent_dir[y][x] = direction index (0..3) utilisée pour arriver ici, depuis parent
  visited = [[0]*GRID_W for _ in range(GRID_H)]
  parent = [[-1]*GRID_W for _ in range(GRID_H)]

  qx = [hx]
  qy = [hy]
  qi = 0
  visited[hy][hx] = 1

  # autoriser la case de la queue (tail) car elle peut bouger
  # (bonne approximation pour éviter de se bloquer)
  while qi < len(qx):
    x = qx[qi]
    y = qy[qi]
    qi += 1

    if x == fx and y == fy:
      break

    for di in range(4):
      dx, dy = DIRS[di]
      nx = x + dx
      ny = y + dy
      if not inside(nx, ny):
        continue
      if visited[ny][nx]:
        continue

      # collision: éviter le corps, sauf tail
      if (nx, ny) in snake_set and (nx, ny) != tail:
        continue

      visited[ny][nx] = 1
      parent[ny][nx] = di
      qx.append(nx)
      qy.append(ny)

  if not visited[fy][fx]:
    return None  # pas de chemin

  # remonter depuis food jusqu'à head pour trouver le 1er pas
  cx, cy = fx, fy
  while True:
    di = parent[cy][cx]
    if di < 0:
      return None
    dx, dy = DIRS[di]
    px = cx - dx
    py = cy - dy
    if px == hx and py == hy:
      return di
    cx, cy = px, py

def count_reachable_free(start, snake_set, tail):
  """
  Mesure de "sécurité": combien de cases accessibles depuis start,
  en évitant le corps (sauf tail).
  """
  sx, sy = start
  if not inside(sx, sy):
    return 0
  if (sx, sy) in snake_set and (sx, sy) != tail:
    return 0

  visited = [[0]*GRID_W for _ in range(GRID_H)]
  qx = [sx]
  qy = [sy]
  qi = 0
  visited[sy][sx] = 1
  cnt = 1

  while qi < len(qx):
    x = qx[qi]
    y = qy[qi]
    qi += 1
    for di in range(4):
      dx, dy = DIRS[di]
      nx = x + dx
      ny = y + dy
      if not inside(nx, ny):
        continue
      if visited[ny][nx]:
        continue
      if (nx, ny) in snake_set and (nx, ny) != tail:
        continue
      visited[ny][nx] = 1
      qx.append(nx)
      qy.append(ny)
      cnt += 1
  return cnt

def choose_safe_move(head, food, snake, snake_set):
  """
  Fallback si BFS impossible: choisir un coup qui évite de mourir
  et garde le plus d'espace accessible (anti-suicide).
  """
  hx, hy = head
  tail = snake[-1]

  best_di = None
  best_score = -1

  for di in range(4):
    dx, dy = DIRS[di]
    nx = hx + dx
    ny = hy + dy
    if not inside(nx, ny):
      continue
    if (nx, ny) in snake_set and (nx, ny) != tail:
      continue

    # score = espace accessible + petit bonus si rapproche de la nourriture
    space = count_reachable_free((nx, ny), snake_set, tail)
    dist = abs(nx - food[0]) + abs(ny - food[1])
    score = space * 10 - dist
    if score > best_score:
      best_score = score
      best_di = di

  return best_di

# ------------------ Jeu ------------------
def game():
  random.seed()

  clear_screen()
  # draw_grid_lines()  # décommente si tu veux le quadrillage

  # Snake initial (au centre)
  sx = GRID_W // 2
  sy = GRID_H // 2
  snake = [(sx, sy), (sx-1, sy), (sx-2, sy)]
  snake_set = set(snake)

  food = spawn_food(snake_set)
  if food is None:
    draw_string("Victoire!", 100, 110, COL_TEXT, COL_BG)
    return

  # dessin initial
  for (x, y) in snake:
    draw_cell(x, y, COL_SNAKE)
  draw_cell(snake[0][0], snake[0][1], COL_HEAD)
  draw_cell(food[0], food[1], COL_FOOD)

  score = 0
  steps = 0

  while steps < MAX_STEPS:
    steps += 1

    # Quitter (touche BACK)
    if ion.keydown(ion.KEY_BACK):
      break

    head = snake[0]

    # 1) tenter BFS vers la nourriture
    di = bfs_next_move(head, food, snake, snake_set)

    # 2) sinon jouer un coup "sûr"
    if di is None:
      di = choose_safe_move(head, food, snake, snake_set)

    # 3) si aucun coup possible -> mort
    if di is None:
      break

    dx, dy = DIRS[di]
    nx = head[0] + dx
    ny = head[1] + dy

    # collision (mur / corps)
    tail = snake[-1]
    if (not inside(nx, ny)) or ((nx, ny) in snake_set and (nx, ny) != tail):
      break

    # avancer
    new_head = (nx, ny)
    ate = (nx == food[0] and ny == food[1])

    # redessiner ancienne tête en corps
    draw_cell(head[0], head[1], COL_SNAKE)

    # ajouter tête
    snake.insert(0, new_head)
    snake_set.add(new_head)

    # manger ?
    if ate:
      score += 1
      # nouvelle nourriture
      food = spawn_food(snake_set)
      if food is None:
        # plus de place
        draw_cell(new_head[0], new_head[1], COL_HEAD)
        draw_string("Victoire! Score: " + str(score), 40, 225, COL_TEXT, COL_BG)
        return
      draw_cell(food[0], food[1], COL_FOOD)
    else:
      # enlever queue
      tx, ty = snake.pop()
      snake_set.remove((tx, ty))
      draw_cell(tx, ty, COL_BG)

    # dessiner tête
    draw_cell(new_head[0], new_head[1], COL_HEAD)

    # petit HUD
    fill_rect(0, 225, 320, 15, COL_BG)
    draw_string("Score: " + str(score) + "  (BACK pour quitter)", 2, 225, COL_TEXT, COL_BG)

    time.sleep(DELAY)

  # fin
  fill_rect(0, 225, 320, 15, COL_BG)
  draw_string("Perdu. Score: " + str(score) + " (BACK)", 2, 225, COL_TEXT, COL_BG)

game()

During your visit to our site, NumWorks needs to install "cookies" or use other technologies to collect data about you in order to:

With the exception of Cookies essential to the operation of the site, NumWorks leaves you the choice: you can accept Cookies for audience measurement by clicking on the "Accept and continue" button, or refuse these Cookies by clicking on the "Continue without accepting" button or by continuing your browsing. You can update your choice at any time by clicking on the link "Manage my cookies" at the bottom of the page. For more information, please consult our cookies policy.