El juego de la viborita o también conocido como snake
es un clásico atemporal, ya que ha logrado fascinar a personas de todas las edades. Como buen clásico ha sido replicado en muchas versiones nuevas, así que en este blog te enseñaré a construir tu propia versión en Python, utilizando el módulo Turtle para crear los gráficos y manejar los eventos.
Configuración del Entorno de Trabajo
Antes de empezar a programar el juego, es necesario organizar el entorno de trabajo y comprender la estructura de archivos que contendrá el juego. Asegúrate de tener Python instalado en tu computadora, para comprobar que lo tienes instalado puedes usar el comando:
python --version
>> Python 3.10.3
Si no lo tienes instalado, puedes consultar la documentación oficial de Python para buscar la opción de instalación que mejor se adapte a tu sistema operativo.
Estructura de Archivos
Vamos a dividir el código en cuatro archivos principales para mantener el proyecto ordenado y modular. Esta estructura facilita la comprensión del código y también permite que sea más fácil de mantener a lo largo del tiempo.
La estructura el proyecto se debe de ver así:
snake-game/
├── main.py
├── snake.py
├── food.py
└── scoreboard.py
main.py
: Este es el archivo principal que ejecuta el juego. Aquí se inicializa la pantalla del juego, se crean las instancias de las clasesSnake
,Food
yScoreboard
, y también se maneja el bucle principal del juego.snake.py
: Contiene la claseSnake
, la cual se encarga de administrar todo lo relacionado con la serpiente. Por ejemplo: su creación, movimiento y crecimiento.food.py
: Define la claseFood
, esta clase tiene la tarea de generar la comida en la pantalla en posiciones aleatorias.scoreboard.py
: Este archivo contiene la claseScoreboard
, que se utiliza para mostrar la puntuación actual y la puntuación más alta del jugador.
Tip: puedes descargar la estructura base del proyecto de este repositorio.
Módulo principal: main.py
El corazón del juego es este archivo. Aquí es donde todo comienza, ya que se controlan las interacciones y el flujo del juego, así que empecemos paso por paso.
Configuración de la Pantalla
El primer paso es configurar la ventana del juego utilizando el módulo Turtle. Aquí se van a definir las características básicas como el tamaño, color de fondo y el título que tendrá la ventana.
from turtle import Screen
screen = Screen()
screen.setup(width=600, height=600)
screen.bgcolor("black")
screen.title("The Snake Game")
screen.tracer(0)
Con este código creamos la pantalla que contendrá el juego, indicamos que sus dimensiones serán de 600x600, que el color de fondo será negro y que el titulo de la ventana será “The Snake Game”. Adicionalmente a esto, el screen.tracer(0)
desactiva la animación automática de Turtle
, lo que nos facilitará su control para el flujo del juego.
Si ejecutas la aplicación en este momento, verás que la pantalla se cierra sola de forma automática, para prevenir esto añade al final del archivo la siguiente línea.
screen.exitonclick()
Inicialización de Clases
Ahora hay que inicializar las clases Scoreboard
, Snake
y Food
. Como ya te mencioné previamente, cada una de estas clases maneja un aspecto diferente del juego, y en conjunto hacen toda la magia.
No olvides importarlas al inicio del archivo main.py
, justo después de la importación de Turtle
.
from snake import Snake
from food import Food
from scoreboard import Scoreboard
Y ahora sí, se pueden inicializar.
scoreboard = Scoreboard()
snake = Snake()
food = Food()
No te preocupes si tu IDE te marca algún error en este momento, ya que es normal puesto que las clases aún no las hemos creado, pero quiero que dejemos el archivo main.py
listo, con todo lo necesario antes de avanzar a los siguientes.
Configuración de los controles
Turtle
tiene un método llamado listen()
, el cual permite detectar las pulsaciones de cada tecla que haga el usuario cuando la ventana del juego este en foco.
Sí se combina este método con el de onkey()
, se podrán vincular cada una de estas pulsaciones con métodos que controlaran qué dirección toma la serpiente.
screen.listen()
screen.onkey(snake.up, "Up")
screen.onkey(snake.down, "Down")
screen.onkey(snake.left, "Left")
screen.onkey(snake.right, "Right")
Los métodos propios de la clase Snake
como up
, down
, left
y right
, los veremos en la sección dedicada al archivo snake.py
.
Ciclo Principal
Una vez que ya tenemos el control de la serpiente, hay que encontrar la forma de que el juego se mantenga en ejecución y pueda interactuar multiples veces con este.
La forma de lograr eso es haciendo uso del ciclo while
, ya que dentro de este ciclo se podrá actualizar la pantalla, controlar la velocidad del juego y también manejar las colisiones y otras interacciones del juego.
game_is_on = True
while game_is_on:
screen.update()
time.sleep(0.1)
snake.move()
Para poder utilizar time.sleep(0.1)
, agrega la importación de time
al inicio del archivo, justo después de las importaciones de las clases.
import time
Detección de Colisiones
El juego necesita identificar cuando se produce una colisión, ya sea con la comida o las paredes, incluso cuando la serpiente choque contra su propio cuerpo.
Las condiciones serían de la siguiente forma:
Comida: Si la serpiente alcanza la comida, la comida desaparece de la posición actual y se reposiciona en un nuevo punto de la pantalla. La serpiente también debe de crecer y la puntuación debe aumentar.
Pared: Si la serpiente choca con la pared, el juego termina y se muestra la puntuación final.
Cuerpo: Si la serpiente choca con su propio cuerpo, el juego también termina y se muestra la puntuación final.
Para lograr esto Turtle
incluye métodos que permiten conocer la ubicación del curso de la tortuga en todo momento, tanto su posición en x
con xcor()
, como su posición en y
con ycord()
. También incluye el método distance()
que permite saber a que distancia esta de otro cursor.
Importante: todo el código relacionado con las condiciones previamente definidas debe de ser incluido dentro del ciclo while que se creo en la sección anterior.
Colisión con la Comida
Para detectar que la serpiente efectivamente alcanzó la comida, se utiliza el método distance()
, aquí se va evaluar si la distancia entre la comida y la cabeza es menor de 20, entonces quiere decir que efectivamente la serpiente llegó a la comida.
Seguramente te preguntas ¿por qué 20? ¿Es un número arbitrario o tiene razón de ser? La verdad es que así como es 20, también puede ser 40 o 100, la razón es porque en el archivo snake.py
, se define la velocidad de movimiento que tendrá la serpiente y yo decidí definirla en 20 puntos, cuando lleguemos a esa sección tu puedes elegir dejar la velocidad en 20, en menos o más, simplemente recuerda modificar ese valor aquí también.
if snake.head.distance(food) < 20:
food.refresh()
snake.extends()
scoreboard.increase_score()
El método de food.refresh()
, creará comida en otra sección de la ventana, snake.extends()
aumentará en 1 el tamaño de la serpiente y scoreboard.increase_score()
incrementará el puntaje que lleve el jugador hasta el momento. Todos estos métodos los crearemos más adelante en las secciones respectivas.
Colisión con una Pared
Al crear la pantalla se definió que fuera 600x600, y debes de verla como un plano cartesiano en donde el centro de la pantalla tiene las coordenadas (0, 0). Sabiendo esto, podemos decir que la pantalla en el eje de las x
va desde el punto -300 al 300 y lo mismo con el eje de las y.
Por lo tanto, aquí es donde brillaran los métods xcor()
y ycor()
ya que si la posición de la serpiente excede estos puntos, significa que chocó con un pared.
if snake.head.xcor() > 280 or snake.head.xcor() < -280 or snake.head.ycor() > 280 or snake.head.ycor() < -280:
scoreboard.reset()
scoreboard.reset_score()
snake.reset()
Si te acabo de decir que la pantalla va de -300 a 300, entonces ¿por qué en la condición if
, estoy utilizando -280 y 280? La razón es la misma que en la condición de la comida, como la serpiente avanza 20 puntos cada vez que se mueve si yo defino 300 en lugar de 280, en la animación va a parecer que la serpiente se salió de la ventana de juego antes de chocar, para evitar esto, utilizo las coordenadas de -280 a 280.
El método de scoreboard.reset()
y scoreboard.reset_score()
guardará la puntuación actual y reiniciará la puntuación respectivamente. El snake.reset()
se encargará de reiniciar el juego. Todos estos métodos los crearemos más adelante en las secciones respectivas.
Colisión con el Cuerpo
Si se quiere detectar la colisión de la serpiente con su propio cuerpo, lo primero que hay que hacer es crear un ciclo que itere sobre todos los elementos del cuerpo, excluyendo la cabeza. Esto se hace para identificar la distancia que tiene la cabeza de la serpiente de cualquier elemento de su cuerpo con el método distance()
que ya vimos previamente.
for segment in snake.segments[1:]:
if snake.head.distance(segment) < 10:
scoreboard.reset()
scoreboard.reset_score()
snake.reset()
Puntuación más Alta
La última parte que falta por configurar en el main
de este juego, es añadir (sí, todavía dentro del ciclo while
) una funcionalidad que revise si al puntuación obtenida en este juego es la más alta hasta el momento y si lo es, que se guarde.
De momento el código es tan sencillo como esto:
if scoreboard.check_scores():
scoreboard.reset()
Recuerda que tanto el método check_scores()
, como el reset()
, los crearemos y revisaremos a detalle más adelante en la sección de Scoreboard
.
Si quieres revisar el código completo que debe de tener el archivo main.py
, puedes consultarlo aquí.
La Serpiente: snake.py
En el archivo snake.py
, es donde se define la clase Snake
. Esta clase como ya vimos en la sección anterior es fundamental para el juego, ya que se encarga de gestionar todo lo relacionado con la serpiente, como lo es su apariencia, movimiento y crecimiento.
Clase Snake
Esta clase es la representación de la serpiente en el juego. Lo primero que hay que hacer es definir las constantes que se utilizaran a lo largo de esta clase e importar el modulo Turtle
.
from turtle import Turtle
STARTING_POSITION = [(0, 0), (-20, 0), (-40, 0)]
MOVE_DISTANCE = 20
UP = 90
DOWN = 270
LEFT = 180
RIGHT = 0
Aquí STARTING_POSITION
define las posiciones iniciales de los segmentos de la serpiente, MOVE_DISTANCE
es la distancia que avanza la serpiente en cada paso y UP
, DOWN
, LEFT
y RIGHT
son las constantes para las direcciones de movimiento.
Lo siguiente que hay que hacer es inicializar la clase Snake
para trabajar con ella. El constructor inicializa la serpiente creando los segmentos iniciales de su cuerpo.
class Snake:
def __init__(self):
self.segments = []
self.create_snake()
self.head = self.segments[0]
Creación de la Serpiente
Aquí se crea el método create_snake()
, el cual como su nombre lo indica creará la serpiente base del juego.
def create_snake(self):
for position in STARTING_POSITION:
self.add_segment(position)
Lo que hace create_snake()
es iterar sobre las STARTING_POSITION
y crea cada segmento de la serpiente llamando a add_segment()
el cual crea un nuevo segmento de la serpiente en la posición dada y lo añade a la lista de segmentos.
def add_segment(self, position):
snake = Turtle("square")
snake.color("white")
snake.penup()
snake.goto(position)
self.segments.append(snake)
Movimiento de la Serpiente
Para mover a la serpiente se crea el método move() el cual contendrá todas las instrucciones para mover cada segmento de la serpiente a la posición del segmento siguiente, esto lo hace comenzando desde el final y moviendo la cabeza en la dirección actual.
def move(self):
len_segments = len(self.segments) - 1
for index in range(len_segments, 0, -1):
new_x = self.segments[index - 1].xcor()
new_y = self.segments[index - 1].ycor()
self.segments[index].goto(new_x, new_y)
self.head.forward(MOVE_DISTANCE)
Básicamente lo que hace el código es mover la cabeza a la nueva posición y todos los segmentos avanzan a la posición que tenía el segmento por delante, de esta forma se da la impresión del movimiento ed la serpiente.
Cambio de Dirección
Se utilizan métodos específicos para gestionar cada una de las direcciones posibles que puede tomar la cabeza, siempre y cuando la nueva dirección no sea opuestamente a la actual. Los métodos utilizados para esta gestión son:
up()
def up(self): if self.head.heading() != DOWN: self.head.setheading(UP)
down()
def down(self): if self.head.heading() != UP: self.head.setheading(DOWN)
left()
def left(self): if self.head.heading() != RIGHT: self.head.setheading(LEFT)
right()
def right(self): if self.head.heading() != LEFT: self.head.setheading(RIGHT)
Tamaño de la Serpiente
Cada que la serpiente alcanza un elemento food
crece, para eso se utiliza el método extends()
, el cual añade un nuevo segmento al final de la serpiente en la posición del último segmento.
def extends(self):
self.add_segment(self.segments[-1].position())
Reinicio de la Serpiente
Cuando la serpiente choca contra algún elemento, se manda a llamar al método reset()
. Lo que hace este método es mover todos los segmentos fuera de la pantalla, limpia la lista de segmentos y crea una nueva serpiente.
def reset(self):
for segment in self.segments:
segment.goto(1000, 1000)
self.segments.clear()
self.create_snake()
self.head = self.segments[0]
Si quieres revisar el código completo que debe de tener el archivo snake.py
, puedes consultarlo aquí.
La Comida: food.py
En esta sección se trabajará con la comida con la que se alimenta la serpiente. Este archivo contendrá la clase Food
, que hereda de Turtle
. La clase Food
juega un papel importante dentro de juego, ya que proporciona el objetivo principal del juego que es: comer para crecer.
Lo primero de todo es importar Turtle
, también importar el módulo de Random
, el cual ayudará a generar la comida en puntos aleatorios de la pantalla e inicializar la clase Food
.
from turtle import Turtle
from random import randint
def __init__(self):
super().__init__()
self.shape("circle")
self.penup()
self.color("blue")
self.speed("fastest")
self.refresh()
El código nos dice que Food
hereda de Turtle
, lo que significa que obtiene todas sus propiedades y métodos. Con esto en mente, dentro del constructor se puede configurar la forma, color y velocidad de la comida, por ejemplo penup()
evita que la comida deje un rastro en la pantalla.
El método refresh()
no viene de Turtle
, este se crea a continuación.
def refresh(self):
random_x = randint(-270, 260)
random_y = randint(-270, 260)
self.goto(random_x, random_y)
Este método es fundamental en el juego, ya que cada vez que la serpiente come la comida, se debe de generar una nueva en una posición aleatoria. Esto se logra generando coordenadas x
y y
aleatorias dentro de los limites de la ventana de juego y luego moviendo la comida a esa posición.
Como puedes ver, la clase Food es realmente pequeña y hasta sencilla, pero es una parte vital dentro del funcionamiento del juego. Si quieres revisar el código completo que debe de tener el archivo food.py
, puedes consultarlo aquí.
El Marcador: scoreboard.py
El archivo scoreboard.py
es la última pieza de este juego. Aquí me enfocaré en la clase Scoreboard, la cual se encarga de llevar la cuenta de la puntuación del jugar y mostrar la puntuación más alta. En esta clase manejará la visualización de texto y almacenamiento de datos en Python. Empecemos con el código.
Clase Scoreboard
Esta clase también extiende de la clase Turtle
y se utiliza para mostrar la puntuación actual y la más alta en la pantalla del juego. Como en todos los casos anteriores, lo primero es hacer las importaciones correspondientes, definir constantes a utilizar e inicializar la clase.
from turtle import Turtle
ALIGNMENT = "center"
FONT = ("Courier", 24, "normal")
class Scoreboard(Turtle):
def __init__(self):
super().__init__()
self.score = 0
self.high_score = 0
self.color("white")
self.hideturtle()
self.penup()
self.goto(0, 270)
self.update_high_score()
self.update_scoreboard()
El constructor inicializa la puntuación actual y la más alta, configura el color que tendrá el texto y coloca el marcador en su posición, la cual será la parte más alta de la pantalla. También llama a los métodos update_high_score()
para cargar la puntuación más alta guardada y update_scoreboard()
para mostrar la puntuación en pantalla, de momento estos métodos no hacen nada, así que hay que crearlos.
Gestión de la Puntuación más Alta
El método update_high_score()
lee la ultima puntuación guardad en un archivo llamado data.txt
, en este archivo se guarda la puntuación más alta alcanzada por los jugadores, y almacenarla en un archivo permite que persista entre diferentes sesiones de juego.
def update_high_score(self):
with open("data.txt", "r") as board:
self.high_score = int(board.read())
Actualización del Marcador
El método update_scoreboard()
borra la puntuación actual y muestra la nueva puntuación obtenida por el jugador actual. El método write
de Turtle
permite mostrar texto en pantalla por lo que es ideal para esta funcionalidad.
def update_scoreboard(self):
self.clear()
self.write(f"Score: {self.score} | High Score: {self.high_score}", move=False, align=ALIGNMENT, font=FONT)
Incrementar la Puntuación
Cada vez que la serpiente come una comida, se manda a llamar a increase_score()
, este método incrementa la puntuación y actualiza el marcador.
def increase_score(self):
self.score += 1
self.update_scoreboard()
Reiniciar y Revisar el Marcador
Al trabajar con el marcador, se necesita tener métodos que lo reinicien cuando esto sea necesario, para reiniciar el marcador de la puntuación más alta se tiene el método reset()
y para reiniciar la propiedad que almacena el puntaje actual se usa el método reset_score()
.
def reset(self):
if self.check_scores():
with open("data.txt", "w") as board:
board.write(str(self.score))
self.update_high_score()
self.update_scoreboard()
def reset_score(self):
self.score = 0
Estos métodos son llamados cuando la serpiente choca consigo misma y si la puntuación actual es más alta que la puntuación más alta anterior, se actualiza en el archivo data.txt
. Para hacer la validación de si la puntuación actual es más alta que la guardada en el archivo se manda a llamar un último método llamado check_scores(self)
.
def check_scores(self):
return self.score > self.high_score
Y con esto completamos la última parte del juego. Si quieres revisar el código completo que debe de tener el archivo food.py
, puedes consultarlo aquí.
Durante la creación de este mítico juego, has aprendido a crear una aplicación con enfoque modular y orientado a objetos. También has construido cada componente, la estructura inicial y la gestión de datos necesaria para funcionar correctamente.
Todavía hay muchas que se pueden mejorar, volver más precio el movimiento de la serpiente, o crear una clasificación de puntuación con jugadores, puntaje y fecha de cuando se logró esa puntuación, como las que había en las recreativas de antaño, todo lo que quieras añadir eres libre de hacerlo.
Te dejo aquí el repositorio final con todos los archivos utilizados para este proyecto. Ahora tienes el juego de la viborita completamente funcional, creado por ti paso a paso. La puntuación más alta que he logrado hasta el momento es de 21 puntos, ¿cuál es la tuya? Déjamelo saber en los comentarios 💜.