martes, 27 de septiembre de 2011

Juego serpiente

He estado repasando temas relacionados con los gráficos en la plataforma .NET y para probar algunas cosas se me ha ocurrido programar un juego al que jugaba cuando empecé a meterme en el mundo de la informática. El juego en cuestión es el de la serpiente (yo lo conocí como Nibbles). La idea es bastante sencilla: se trata de mover por la pantalla un elemento gráfico simulando una serpiente que debe pasar por encima de ciertos puntos donde se encuentra su alimento preferido. Cada vez que un alimento es engullido la serpiente aumenta su tamaño y aparece otro alimento en la pantalla. La serpiente no puede salirse de la pantalla ni chocar consigo misma.
Yo he utilizado C# para programarlo, aunque es muy parecido en Visual Basic o Visual C++. A continuación os muestro el código y también un par de imágenes de su ejecución.

using System;
using System.Timers;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing;

namespace nibblesCS01
{
    public partial class fNibbles : Form
    {
        List<Punto> path;
        private const int DESPLAZAMIENTO = 5;
        private const int ANCHO_SERPIENTE = 6;
        private const int ANCHO_INICIAL = 180;
        private const int INCREMENTO_ANCHO = 20;
        private const int ALIMENTOS_NECESARIOS = 12;
        private int INTERVALO_TEMPORIZADOR = 100;

        private List<Alimento> alimentos;
        int alimentosComidos;

        public fNibbles()
        {
            InitializeComponent();
        }

        protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
        {
            const int WM_KEYDOWN = 0x100;
            const int WM_SYSKEYDOWN = 0x104;
            Punto pto = new Punto();

            if ((msg.Msg == WM_KEYDOWN) || (msg.Msg == WM_SYSKEYDOWN))
            {
                switch (keyData)
                {
                    case Keys.Right:
                        if (path[path.Count - 1].d != 0)
                        {
                            pto.d = 0;
                            pto.x = path[path.Count - 1].x;
                            pto.y = path[path.Count - 1].y;
                            path[path.Count - 1].d = 0;
                            path.Add(pto);
                        }
                        break;
                    case Keys.Left:
                        if (path[path.Count - 1].d != 1)
                        {
                            pto.d = 1;
                            pto.x = path[path.Count - 1].x;
                            pto.y = path[path.Count - 1].y;
                            path[path.Count - 1].d = 1;
                            path.Add(pto);
                        }
                        break;
                    case Keys.Up:
                        if (path[path.Count - 1].d != 2)
                        {
                            pto.d = 2;
                            pto.x = path[path.Count - 1].x;
                            pto.y = path[path.Count - 1].y;
                            path[path.Count - 1].d = 2;
                            path.Add(pto);
                        }
                        break;
                    case Keys.Down:
                        if (path[path.Count - 1].d != 3)
                        {
                            pto.d = 3;
                            pto.x = path[path.Count - 1].x;
                            pto.y = path[path.Count - 1].y;
                            path[path.Count - 1].d = 3;
                            path.Add(pto);
                        }
                        break;

                    //Datos depuración
                    //case Keys.Enter:
                    //    for (int i = 0; i < path.Count; i++)
                    //    {
                    //        lbl.Text += "(" + path[i].x + "," + path[i].y + "," + path[i].d +") ";
                    //    }
                    //    lbl.Text += " -- ";
                    //    break;

                    //Opción pausar el juego
                    case Keys.P:
                        tTemp.Enabled = !tTemp.Enabled;
                        break;
                }
            }

            return base.ProcessCmdKey(ref msg, keyData);
        }

        private void fNibbles_Load(object sender, EventArgs e)
        {
            int xAlimento, yAlimento;

            alimentosComidos = 0;

            alimentos = new List<Alimento>();
            //Inicializar la serpiente
            path = new List<Punto>();
            Punto pto = new Punto(0, 200, 0);
            path.Add(pto);
            pto = new Punto(ANCHO_INICIAL, 200, 0);
            path.Add(pto);

            Random r = new Random(DateTime.Now.Millisecond);
            xAlimento = r.Next((this.Width - 5) / DESPLAZAMIENTO) * DESPLAZAMIENTO;
            yAlimento = r.Next((this.Height - 30) / DESPLAZAMIENTO) * DESPLAZAMIENTO;
            Alimento ptoAlimento = new Alimento(xAlimento, yAlimento, Color.Blue);
            alimentos.Add(ptoAlimento);

            lblAlimentosRestantes.Text = ALIMENTOS_NECESARIOS.ToString();

            //Datos depuración
            //lbl.Text = "[" + ptoAlimento.x + "," + ptoAlimento.y + "] ";

            //Inicializar el temporizador
            tTemp.Tick += new System.EventHandler(OnTimerEvent);
            tTemp.Interval = INTERVALO_TEMPORIZADOR;
            tTemp.Enabled = true;
        }

        //Calcula la longitud de la serpiente en función de los puntos que componen su silueta actual
        private int longitudSerpiente()
        {
            int longitud;

            longitud = 0;
            for (int i = 1; i < path.Count; i++)
            {
                if (path[i - 1].x == path[i].x)
                {
                    longitud += Math.Abs(path[i].y - path[i - 1].y);
                }
                else
                {
                    longitud += Math.Abs(path[i].x - path[i-1].x);
                }
            }
            return longitud;
        }

        //Muestra un punto en la pantalla por cada objeto de tipo alimento guardado en la lista
        //de alimentos
        public void MostrarAlimentos()
        {
            Graphics g = this.CreateGraphics();

            foreach (Alimento al in alimentos)
            {
                g.FillEllipse(Brushes.Blue, al.x - 4, al.y - 4, 8, 8);
            }
        }

        //Manejador del evento correspondiente al vencimiento del temporizador
        //Desplaza la serpiente una posición y la redibuja
        public void OnTimerEvent(object source, EventArgs e)
        {
            //Desplazar la serpiente una posición
            if (DesplazarSerpiente())
            {
                //Limpiar la pantalla y redibujar la serpiente
                MostrarSerpiente();
                MostrarAlimentos();
            }
        }

        //Dependiendo de la dirección del último punto añadido a la ruta, modificar el
        //valor de dicho punto. El primer punto de la ruta debe chequearse para ver si
        //coincide con el valor del segundo, y si es así, eliminar el primero de la ruta
        private bool DesplazarSerpiente()
        {
            Punto pto0 = new Punto();
            Punto pto1 = new Punto();
            Punto ultimoPto = new Punto();
            bool noDesplazarCola;
            int xAlimento, yAlimento;

            pto0 = path[0];
            pto1 = path[1];
            ultimoPto = path[path.Count - 1];
            noDesplazarCola = false;

            //Desplazamos la cabeza de la serpiente en la dirección que corresponda
            switch (ultimoPto.d)
            {
                case 0:  //dcha
                    ultimoPto.x += DESPLAZAMIENTO;
                    break;
                case 1:  //izda
                    ultimoPto.x -= DESPLAZAMIENTO;
                    break;
                case 2:  //arriba
                    ultimoPto.y -= DESPLAZAMIENTO;
                    break;
                case 3:  //abajo
                    ultimoPto.y += DESPLAZAMIENTO;
                    break;
            }
            //Comprobamos si la serpiente se sale de los márgenes o se muerde a sí misma
            if (path[path.Count - 1].x > this.Width - 5 || path[path.Count - 1].y > this.Height - 55 || path[path.Count - 1].x < 0 || path[path.Count - 1].y < 0 ||
                SerpienteSeMuerde())
            {
                tTemp.Enabled = false;
                //this.Refresh();
                MostrarMensaje("GAME OVER!!!", 240, 150, Brushes.DarkRed);
                //MessageBox.Show("GAME OVER");
                return false;
            }

            //Comprobamos si la serpiente se come un alimento
            if (alimentos.Count > 0)
            {
                if (path[path.Count - 1].x == alimentos[0].x && path[path.Count - 1].y == alimentos[0].y)
                {
                    //Come un alimento
                    alimentosComidos++;
                    //Actualizamos el contador de alimentos restantes
                    lblAlimentosRestantes.Text = (ALIMENTOS_NECESARIOS - alimentosComidos).ToString();

                    if (alimentosComidos == ALIMENTOS_NECESARIOS)
                    {
                        tTemp.Enabled = false;
                        MostrarMensaje("CONGRATULATIONS! YOU PASSED THIS LEVEL.", 100, 150, Brushes.DarkGreen);
                        return false;
                    }
                    else
                    {
                        alimentos.RemoveAt(0);
                        noDesplazarCola = true;
                        //Colocamos otro alimento en la pantalla
                        Random r = new Random(DateTime.Now.Millisecond);
                        xAlimento = r.Next((this.Width - 5) / DESPLAZAMIENTO) * DESPLAZAMIENTO;
                        yAlimento = r.Next((this.Height - 55) / DESPLAZAMIENTO) * DESPLAZAMIENTO;
                        Alimento ptoAlimento = new Alimento(xAlimento, yAlimento, Color.Blue);
                        alimentos.Add(ptoAlimento);

                        //Datos depuración
                        //lbl.Text += "*" + path[0].x + "," + path[0].y + "* ";
                        //lbl.Text = "[" + ptoAlimento.x + "," + ptoAlimento.y + "] ";

                    }
                }
            }

            if (noDesplazarCola == false)
            {
                //Desplazamos la cola de la serpiente en la dirección que indica el último punto
                switch (pto0.d)
                {
                    case 0:  //derecha
                        if ((pto0.x + DESPLAZAMIENTO == pto1.x) && (pto0.y == pto1.y))
                        {
                            path.RemoveAt(0);
                        }
                        else
                        {
                            path[0].x += DESPLAZAMIENTO;
                        }
                        break;

                    case 1:  //izda
                        if ((pto0.x - DESPLAZAMIENTO == pto1.x) && (pto0.y == pto1.y))
                        {
                            path.RemoveAt(0);
                        }
                        else
                        {
                            path[0].x -= DESPLAZAMIENTO;
                        }
                        break;

                    case 2:  //arriba
                        if ((pto0.y - DESPLAZAMIENTO == pto1.y) && (pto0.x == pto1.x))
                        {
                            path.RemoveAt(0);
                        }
                        else
                        {
                            path[0].y -= DESPLAZAMIENTO;
                        }
                        break;

                    case 3:  //abajo
                        if ((pto0.y + DESPLAZAMIENTO == pto1.y) && (pto0.x == pto1.x))
                        {
                            path.RemoveAt(0);
                        }
                        else
                        {
                            path[0].y += DESPLAZAMIENTO;
                        }
                        break;
                }
            }
            return true;
        }

        //Comprobamos si la cabeza de la serpiente se cruza con el resto del cuerpo en algún punto
        public bool SerpienteSeMuerde()
        {
            for (int i = 0; i < path.Count - 2; i++)
            {
                if (((path[i].x == path[path.Count - 1].x) && (path[path.Count - 1].y >= path[i].y) && (path[path.Count - 1].y <= path[i+1].y))||
                    ((path[i].x == path[path.Count - 1].x) && (path[path.Count - 1].y <= path[i].y) && (path[path.Count - 1].y >= path[i + 1].y)))
                {
                    return true;
                }
                if (((path[i].y == path[path.Count - 1].y) && (path[path.Count - 1].x >= path[i].x) && (path[path.Count - 1].x <= path[i+1].x)) ||
                    ((path[i].y == path[path.Count - 1].y) && (path[path.Count - 1].x <= path[i].x) && (path[path.Count - 1].x >= path[i + 1].x)))
                {
                    return true;
                }
                if ((path[path.Count - 1].x == path[i].x) && (path[path.Count - 1].y == path[i].y))
                    return true;
            }
            return false;
        }

        //Dibujar líneas punto a punto entre cada dos puntos contiguos almacenados en
        //la ruta
        public void MostrarSerpiente()
        {
            Graphics g = this.CreateGraphics();
            Pen p = new Pen(Brushes.BurlyWood, ANCHO_SERPIENTE);

            this.Refresh();
            if (path.Count > 1)
            {
                for (int i = 1; i < path.Count; i++)
                {
                    g.DrawLine(p, path[i - 1].x, path[i - 1].y, path[i].x, path[i].y);
                }
            }

            //Datos para depuración
            //lbl1.Text = "Xn=" + path[path.Count - 1].x;
            //lbl2.Text = "Yn=" + path[path.Count - 1].y;
            //lbl3.Text = "Xn_1=" + path[path.Count - 2].x;
            //lbl4.Text = "Yn_1=" + path[path.Count - 2].y;
            //lblLong.Text = longitudSerpiente().ToString();

        }

        private void fNibbles_KeyDown(object sender, KeyEventArgs e)
        {
            //SE PODRÍA UTILIZAR ESTE EVENTO EN LUGAR DE SOBREESCRIBIR EL MÉTODO ProcessCmdKey DEL
            //FORMULARIO. AQUÍ NO SE UTILIZA ESTA OPCIÓN.

        //    //Comprobamos si se trata de una flecha
        //    if ((e.KeyCode == Keys.Up) || (e.KeyCode == Keys.Down) ||
        //        (e.KeyCode == Keys.Left) || (e.KeyCode == Keys.Right))
        //    {
        //        if (e.KeyCode == Keys.Right)
        //            path[path.Count - 1].d = 0;
        //        if (e.KeyCode == Keys.Left)
        //            path[path.Count - 1].d = 1;
        //        if (e.KeyCode == Keys.Up)
        //            path[path.Count - 1].d = 2;
        //        if (e.KeyCode == Keys.Down)
        //            path[path.Count - 1].d = 3;

        //    }

        }

        private void MostrarMensaje(String mensaje, int x, int y, Brush b)
        {
            Graphics g = this.CreateGraphics();
            Font f = new Font("Arial", 14, FontStyle.Bold);

            this.Refresh();
            g.DrawString(mensaje, f, b , x, y);
        }
    }

 
    //Clase que ayuda a definir la longitud y forma de la serpiente para
    //su impresión en la pantalla

    class Punto
    {
        public int x;
        public int y;
        public int d;  //0(dcha), 1(arriba), 2(abajo), 3(izda)

        public Punto() { }
        public Punto(int _x, int _y, int _d)
        {
            x = _x;
            y = _y;
            d = _d;
        }
    }


    //Esta clase define los alimentos que la serpiente debe ir "comiendo"
    //para avanzar en el juego. En esta versión la propiedad c(color) no
    //se utiliza.

    class Alimento
    {
        public int x;
        public int y;
        public Color c;

        public Alimento() { }
        public Alimento(int _x, int _y, Color _c)
        {
            x = _x;
            y = _y;
            c = _c;
        }
    }

}

No hay comentarios:

Publicar un comentario