Portfoolio

Praktiline töö “Madu/Uss”(Snake)

Sissejuhatus

Selles praktilises töös lõin klassikalise Ussimängu (Snake) kasutades C# programmeerimiskeelt ja OOP (objektorienteeritud programmeerimise) põhimõtteid. Mäng töötab konsoolis ja kasutab mitut erinevat klassi, mis teevad omavahel koostööd.

1. samm: Projekti arhitektuur ja klassid

Enne koodi kirjutamist mõtlesin läbi, milliseid klasse on vaja. OOP põhimõtte järgi jagasin programmi eraldi osadeks:

KlassÜlesanne
PunktHoiab X ja Y koordinaate, oskab ennast konsooli joonistada
UssUssi keha (List<Punkt>), liikumine, kasvu ja enesehammustusloogika
ToitLoob toidu suvalisse kohta ekraanil
KaartJoonistab seinad (#), kontrollib kokkupõrkeid
MänguSeadedHoiab raskusastme seadeid (kiirus, ekraani suurus)
EdetabelSalvestab ja kuvab TOP 5 tulemused failist
HeliefektidMängib helisid (Console.Beep) asünkroonselt
ProgramPeamenüü, mängutsükkel, ekraanihaldus

2. samm: Punkt klass ja Suund enum

Esimese asjana lõin Punkt klassi ja Suund enumi. Punkt on abiklass, mis hoiab koordinaate ja oskab ennast konsooli õigesse kohta joonistada meetodiga Console.SetCursorPosition().

Suund enum määrab 4 võimalikku liikumissuunda: Üles, Alla, Vasakule, Paremale.

Kuidas see töötab:

  • Joonista() meetod paneb sümboli ekraanile õigesse kohta
  • Kustuta() meetod kirjutab tühiku peale, et simuleerida liikumist
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Snake
{
    // Määrab ära 4 võimalikku liikumissuunda
    public enum Suund
    {
        Üles,
        Alla,
        Vasakule,
        Paremale
    }

    public class Punkt
    {
        public int X { get; set; }
        public int Y { get; set; }
        public char Sümbol { get; set; } // '*' ussi jaoks, '@' toidu jaoks, '#' seina jaoks

        public Punkt(int x, int y, char sümbol)
        {
            X = x;
            Y = y;
            Sümbol = sümbol;
        }

        // Joonistab punkti konsooli õigesse kohta
        public void Joonista()
        {
            Console.SetCursorPosition(X, Y);
            Console.Write(Sümbol);
        }

        // Puhastab punkti asukoha (kustutab ekraanilt)
        public void Kustuta()
        {
            Console.SetCursorPosition(X, Y);
            Console.Write(' ');
        }
    }
}

3. samm: Uss klass

Uss klass on mängu kõige tähtsam osa. Ussi keha on List<Punkt> — esimene element on pea, viimane on saba.

Liikumise loogika:

  1. Võtame praeguse pea koordinaadid
  2. Arvutame uue pea asukoha vastavalt suunale (switch)
  3. Lisame uue pea listi algusesse (Insert(0, ...))
  4. Kustutame vana saba (Remove)

Kasvamine: Kui uss sööb toidu, ei kustuta me saba — uss lihtsalt muutub ühe punkti võrra pikemaks.

Enese hammustamise kontroll: Meetod OnHammustandItennast() kontrollib, kas pea koordinaadid kattuvad mõne keha punktiga (LINQ Skip(1).Any(...)).

using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Snake
{
    public class Uss
    {
        // Ussi keha on Punkt objektide nimekiri - esimene on pea, viimane on saba
        private List<Punkt> keha = new List<Punkt>();

        public Suund PraeguneSuund { get; set; }

        public Uss(int algX, int algY, int pikkus)
        {
            PraeguneSuund = Suund.Paremale; // Alguses liigub paremale

            // Loome ussi algse keha
            for (int i = 0; i < pikkus; i++)
            {
                Punkt p = new Punkt(algX - i, algY, '*');
                keha.Add(p);
                p.Joonista();
            }
        }

        // Liigutab ussi praeguse suuna järgi
        public void Liigu()
        {
            // 1. Leiame praeguse pea asukoha
            Punkt pea = keha.First();
            Punkt uusPea = new Punkt(pea.X, pea.Y, '*');

            // 2. Arvutame uue pea asukoha vastavalt suunale
            switch (PraeguneSuund)
            {
                case Suund.Paremale: uusPea.X++; break;
                case Suund.Vasakule: uusPea.X--; break;
                case Suund.Alla: uusPea.Y++; break;
                case Suund.Üles: uusPea.Y--; break;
            }

            // 3. Lisame uue pea listi algusesse ja joonistame
            keha.Insert(0, uusPea);
            uusPea.Joonista();

            // 4. Kustutame sabaotsa, et simuleerida liikumist
            Punkt saba = keha.Last();
            saba.Kustuta();
            keha.Remove(saba);
        }

        // Tagastab ussi pea (esimese elemendi)
        public Punkt HangiPea()
        {
            return keha.First();
        }

        // Ussi kasvamine - lisab saba lõppu uue punkti
        public void Kasva()
        {
            keha.Add(new Punkt(keha.Last().X, keha.Last().Y, '*'));
        }

        // Kontrollib, kas uss hammustas ennast (pea kattub kehaga)
        public bool OnHammustandItennast()
        {
            Punkt pea = keha.First();
            // Kontrollime alates teisest elemendist (indeks 1)
            return keha.Skip(1).Any(p => p.X == pea.X && p.Y == pea.Y);
        }

        // Ussi praegune pikkus (skooriarvestuseks)
        public int Pikkus => keha.Count;
    }
}

4. samm: Toit ja Kaart klassid

Toit klass loob toidu suvalisse kohta ekraanil kasutades Random.Next(). Toit ei tohi tekkida seinte sisse, seega piiran koordinaate.

using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Snake
{
    public class Toit
    {
        private Random rnd = new Random();
        private int ekraaniLaius;
        private int ekraaniKõrgus;
        private int offsetY; // Mitu rida ülevalt jätta infopaneelile

        // Toidu asukoht ekraanil
        public Punkt Asukoht { get; private set; }

        public Toit(int laius, int kõrgus, int offsetY = 0)
        {
            ekraaniLaius = laius;
            ekraaniKõrgus = kõrgus;
            this.offsetY = offsetY;
            LooUusToit(); // Loome kohe alguses esimese toidu
        }

        // Loob uue toidu suvalisse kohta ekraanil (seintest sees)
        public void LooUusToit()
        {
            int x = rnd.Next(2, ekraaniLaius - 2);
            int y = rnd.Next(2 + offsetY, ekraaniKõrgus - 2 + offsetY);
            Asukoht = new Punkt(x, y, '@'); // Toit näeb välja nagu '@'
            Asukoht.Joonista();
        }
    }
}

Kaart klass loob mänguvälja piirid (# sümbolitest). Kasutasin offsetY parameetrit, et jätta rida 0 infopaneelile (skoor + elud). Meetod OnKokkupõrge() kontrollib LINQ abil, kas ussi pea kattub mõne seinaga.

using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Snake
{
    public class Kaart
    {
        public List<Punkt> Takistused { get; private set; } = new List<Punkt>();

        // offsetY - mitu rida ülevalt jätta infopaneelile
        public Kaart(int laius, int kõrgus, int offsetY = 0)
        {
            // Ülemine ja alumine sein
            for (int x = 0; x < laius; x++)
            {
                Takistused.Add(new Punkt(x, offsetY, '#'));
                Takistused.Add(new Punkt(x, kõrgus - 1 + offsetY, '#'));
            }

            // Vasak ja parem sein
            for (int y = offsetY; y < kõrgus + offsetY; y++)
            {
                Takistused.Add(new Punkt(0, y, '#'));
                Takistused.Add(new Punkt(laius - 1, y, '#'));
            }
        }

        // Joonistab kõik seinad ekraanile
        public void Joonista()
        {
            foreach (var p in Takistused)
            {
                p.Joonista();
            }
        }

        // Kontrollib, kas antud punkt kattub mõne takistusega
        public bool OnKokkupõrge(Punkt pea)
        {
            return Takistused.Any(t => t.X == pea.X && t.Y == pea.Y);
        }
    }
}

5. samm: MänguSeaded klass — raskusastmed

Lõin eraldi MänguSeaded klassi, et vältida “maagiliste numbrite” kasutamist koodis. switch lause valib raskusastme järgi õiged väärtused:

  • Lihtne: 200ms viivitus, 40×20 ekraan
  • Keskmine: 100ms viivitus, 30×15 ekraan
  • Raske: 50ms viivitus, 20×10 ekraan

Mäng läheb ka mängu käigus kiiremaks — iga 50 punkti tagant väheneb Thread.Sleep() väärtus.

using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Snake
{
    public class MänguSeaded
    {
        public int Laius { get; set; }
        public int Kõrgus { get; set; }
        public int KiirusMS { get; set; } // Viivitus millisekundites - mida vähem, seda kiirem

        public MänguSeaded(int tase)
        {
            // Raskusastme valik
            switch (tase)
            {
                case 1: KiirusMS = 200; Laius = 40; Kõrgus = 20; break; // Lihtne
                case 2: KiirusMS = 100; Laius = 30; Kõrgus = 15; break; // Keskmine
                case 3: KiirusMS = 50; Laius = 20; Kõrgus = 10; break; // Raske
                default: KiirusMS = 150; Laius = 40; Kõrgus = 20; break;
            }
        }
    }
}

6. samm: Edetabel — faili salvestamine

Edetabel on static klass, mis tegeleb failiga skoorid.txt. Kasutasin File.AppendAllLines() salvestamiseks ja File.ReadAllLines() lugemiseks.

Tulemuste sortimine: kasutasin LINQ OrderByDescending() ja Take(5), et kuvada ainult TOP 5 tulemust.

using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Snake
{
    public static class Edetabel
    {
        private static string failiTee = "skoorid.txt";

        // Salvestab tulemuse faili
        public static void Salvesta(string nimi, int skoor)
        {
            File.AppendAllLines(failiTee, new[] { $"{nimi};{skoor}" });
        }

        // Kuvab TOP 5 tulemust ekraanile
        public static void KuvaEdetabel()
        {
            if (!File.Exists(failiTee)) return;

            var skoorid = File.ReadAllLines(failiTee)
                .Select(rida => rida.Split(';'))
                .Where(osad => osad.Length == 2)
                .Select(osad => new { Nimi = osad[0], Punktid = int.Parse(osad[1]) })
                .OrderByDescending(x => x.Punktid)
                .Take(5); // Ainult TOP 5

            Console.WriteLine("--- TOP 5 EDETABEL ---");
            foreach (var s in skoorid)
            {
                Console.WriteLine($"{s.Nimi}: {s.Punktid} punkti");
            }
        }
    }
}

7. samm: Heliefektid — asünkroonne heli

Heliefektid on samuti static klass. Kasutasin Task.Run(), et heli mängiks taustal — muidu jääks mäng heli ootama ja “hakkiks”.

  • MängiSöömist() — kõrge piiks (800Hz) kui uss sööb
  • MängiKaotust() — kolm langevat tooni kui mäng lõpeb
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Snake
{
    public static class Heliefektid
    {
        // Mängib heli, kui uss sööb toidu ära
        public static void MängiSöömist()
        {
            // Käivitame uue ülesandena, et mäng ei jääks heli ootama
            Task.Run(() => Console.Beep(800, 100));
        }

        // Mängib heli, kui mäng on läbi
        public static void MängiKaotust()
        {
            Task.Run(() =>
            {
                Console.Beep(400, 200);
                Console.Beep(300, 200);
                Console.Beep(200, 400);
            });
        }
    }
}

8. samm: Peamenüü ja mängutsükkel (Program.cs)

Peamenüü kasutab nooleklahve + Enter valimiseks. Kasutasin Console.ForegroundColor ja Console.BackgroundColor värviliseks kuvamiseks. ASCII-logo on joonistatud █ ja ╗ sümbolitega.

Mängutsükkel töötab nii:

  1. Loeme klahvivajutuse (Console.KeyAvailable)
  2. Liigutame ussi (uss.Liigu())
  3. Kontrollime kokkupõrget seinaga või iseendaga
  4. Kontrollime, kas uss sõi toidu ära
  5. Ootame (Thread.Sleep) vastavalt kiirusele

3 elu süsteem: Kui uss põrkab vastu seina, kaotab ta ühe elu ja uss taaskäivitatakse keskel. Kui kõik 3 elu on otsas, mäng lõpeb.

Infopaneel (rida 0): Skoor on vasakul (@ sümboliga), elud paremal (*** = 3 elu, *-- = 1 elu).

using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Snake
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.CursorVisible = false;
            Console.Title = "Ussimang";

            while (true)
            {
                int valik = KuvaPohinenu();

                if (valik == 1)
                {
                    int tase = KuvaRaskusmenuu();
                    MängiMängu(tase);
                }
                else if (valik == 2)
                {
                    Console.Clear();
                    Edetabel.KuvaEdetabel();
                    Console.WriteLine("\nVajuta Enter...");
                    Console.ReadLine();
                }
                else if (valik == 3)
                {
                    Console.Clear();
                    Console.WriteLine("Nagemist!");
                    Thread.Sleep(800);
                    return;
                }
            }
        }

        // =====================
        //      PEAMENÜÜ
        // =====================
        static int KuvaPohinenu()
        {
            // Taastame akna suuruse menüü jaoks
            try { Console.SetWindowSize(50, 25); } catch { }

            int valitud = 1;
            ConsoleKey klahv;

            do
            {
                Console.Clear();
                Console.WriteLine();
                Console.WriteLine("  ██╗   ██╗███████╗███████╗");
                Console.WriteLine("  ██║   ██║██╔════╝██╔════╝");
                Console.WriteLine("  ██║   ██║███████╗███████╗");
                Console.WriteLine("  ██║   ██║╚════██║╚════██║");
                Console.WriteLine("  ╚██████╔╝███████║███████║");
                Console.WriteLine("   ╚═════╝ ╚══════╝╚══════╝");
                Console.WriteLine("        U S S I M A N G");
                Console.WriteLine();

                Console.WriteLine("   +============================+");

                string[] punktid = { "Alusta mangu", "Edetabel", "Valju" };
                for (int i = 0; i < punktid.Length; i++)
                {
                    if (i + 1 == valitud)
                    {
                        Console.ForegroundColor = ConsoleColor.Green;
                        Console.WriteLine($"   |  >>> {punktid[i],-22}|");
                        Console.ResetColor();
                    }
                    else
                    {
                        Console.WriteLine($"   |      {punktid[i],-22}|");
                    }
                }

                Console.WriteLine("   +============================+");
                Console.WriteLine();

                klahv = Console.ReadKey(true).Key;
                if (klahv == ConsoleKey.UpArrow && valitud > 1) valitud--;
                else if (klahv == ConsoleKey.DownArrow && valitud < 3) valitud++;

            } while (klahv != ConsoleKey.Enter);

            return valitud;
        }

        // =====================
        //   RASKUSASTE MENÜÜ
        // =====================
        static int KuvaRaskusmenuu()
        {
            int valitud = 1;
            ConsoleKey klahv;

            do
            {
                Console.Clear();
                Console.WriteLine();
                Console.WriteLine("   +============================+");
                Console.WriteLine("   |      VALI RASKUSASTE       |");
                Console.WriteLine("   +============================+");

                string[] tasemed = { "Lihtne   (aeglane) ", "Keskmine (normaalne)", "Raske    (kiire)   " };
                for (int i = 0; i < tasemed.Length; i++)
                {
                    if (i + 1 == valitud)
                    {
                        Console.ForegroundColor = ConsoleColor.Yellow;
                        Console.WriteLine($"   |  >>> {tasemed[i],-22}|");
                        Console.ResetColor();
                    }
                    else
                    {
                        Console.WriteLine($"   |      {tasemed[i],-22}|");
                    }
                }

                Console.WriteLine("   +============================+");
                Console.WriteLine();

                klahv = Console.ReadKey(true).Key;
                if (klahv == ConsoleKey.UpArrow && valitud > 1) valitud--;
                else if (klahv == ConsoleKey.DownArrow && valitud < 3) valitud++;

            } while (klahv != ConsoleKey.Enter);

            return valitud;
        }

        // =====================
        //      MÄNGU LOOGIKA
        // =====================
        static void MängiMängu(int tase)
        {
            MänguSeaded seaded = new MänguSeaded(tase);

            // OLULINE: kõigepealt buffer, siis aken, siis ootame, siis Clear
            try
            {
                Console.SetBufferSize(seaded.Laius + 2, seaded.Kõrgus + 3);
                Console.SetWindowSize(seaded.Laius + 2, seaded.Kõrgus + 3);
            }
            catch { }

            Thread.Sleep(200);
            Console.Clear();
            Thread.Sleep(50);
            Console.CursorVisible = false;

            // offsetY = 1: rida 0 on infopaneel, mäng algab reast 1
            Kaart kaart = new Kaart(seaded.Laius, seaded.Kõrgus, offsetY: 1);
            Uss uss = new Uss(seaded.Laius / 2, seaded.Kõrgus / 2 + 1, 3);
            Toit toit = new Toit(seaded.Laius, seaded.Kõrgus, offsetY: 1);

            int skoor = 0;
            int elud = 3;

            kaart.Joonista();
            UuendaInfopaneel(skoor, elud, seaded.Laius);

            // MÄNGU TSÜKKEL
            while (true)
            {
                if (Console.KeyAvailable)
                {
                    ConsoleKeyInfo klahv = Console.ReadKey(true);

                    if (klahv.Key == ConsoleKey.UpArrow && uss.PraeguneSuund != Suund.Alla)
                        uss.PraeguneSuund = Suund.Üles;
                    else if (klahv.Key == ConsoleKey.DownArrow && uss.PraeguneSuund != Suund.Üles)
                        uss.PraeguneSuund = Suund.Alla;
                    else if (klahv.Key == ConsoleKey.LeftArrow && uss.PraeguneSuund != Suund.Paremale)
                        uss.PraeguneSuund = Suund.Vasakule;
                    else if (klahv.Key == ConsoleKey.RightArrow && uss.PraeguneSuund != Suund.Vasakule)
                        uss.PraeguneSuund = Suund.Paremale;
                    else if (klahv.Key == ConsoleKey.Escape)
                        break;
                }

                uss.Liigu();
                Punkt pea = uss.HangiPea();

                // Kokkupõrge seinaga või iseendaga
                if (kaart.OnKokkupõrge(pea) || uss.OnHammustandItennast())
                {
                    Heliefektid.MängiKaotust();
                    elud--;
                    UuendaInfopaneel(skoor, elud, seaded.Laius);

                    if (elud <= 0) break;

                    // Veel elusid - taaskäivitame ussi
                    Thread.Sleep(800);
                    Console.Clear();
                    kaart.Joonista();
                    uss = new Uss(seaded.Laius / 2, seaded.Kõrgus / 2 + 1, 3);
                    toit.LooUusToit();
                    UuendaInfopaneel(skoor, elud, seaded.Laius);
                    continue;
                }

                // Toidu söömine
                if (pea.X == toit.Asukoht.X && pea.Y == toit.Asukoht.Y)
                {
                    skoor += 10;
                    uss.Kasva();
                    toit.LooUusToit();
                    Heliefektid.MängiSöömist();
                    UuendaInfopaneel(skoor, elud, seaded.Laius);
                }

                int kiirus = Math.Max(50, seaded.KiirusMS - (skoor / 50) * 10);
                Thread.Sleep(kiirus);
            }

            // MÄNGU LÕPP - taastame akna suuruse ENNE kuvamist
            try
            {
                Console.SetBufferSize(50, 25);
                Console.SetWindowSize(50, 25);
            }
            catch { }

            Thread.Sleep(200);
            Console.Clear();
            Thread.Sleep(50);
            Console.CursorVisible = false;

            Console.WriteLine();
            Console.WriteLine("   +============================+");
            Console.WriteLine("   |        MANG LABI!          |");
            Console.WriteLine($"   |   Sinu skoor: {skoor,-13}|");
            Console.WriteLine("   +============================+");
            Console.WriteLine();
            Console.CursorVisible = true;
            Console.Write("   Sisesta oma nimi: ");
            string nimi = Console.ReadLine();
            Console.CursorVisible = false;

            if (!string.IsNullOrEmpty(nimi))
            {
                Edetabel.Salvesta(nimi, skoor);
            }
        }

        // Uuendab infopaneeli (rida 0) - skoor vasakul, elud paremal
        static void UuendaInfopaneel(int skoor, int elud, int laius)
        {
            Console.SetCursorPosition(0, 0);
            Console.Write(new string(' ', laius + 2));

            Console.SetCursorPosition(1, 0);
            Console.Write($"Skoor: {skoor}");

            // Elud: tärnid = elusid järel, miinused = kaotatud
            string eludTekst = $"Elud: {new string('*', elud)}{new string('-', 3 - elud)}";
            Console.SetCursorPosition(laius - eludTekst.Length, 0);
            Console.Write(eludTekst);
        }
    }
}

Mäng töötamas

[📸 LISA SIIA SCREENSHOTI: Raskusastme valik]

[📸 LISA SIIA SCREENSHOTI: Mäng käib — uss liigub, toit on ekraanil, skoor nähtav]

[📸 LISA SIIA SCREENSHOTI: Mäng läbi — MANG LABI ekraan koos skooriga]

[📸 LISA SIIA SCREENSHOTI: TOP 5 edetabel]

Lisafunktsioonid (täiendused)

Lisaks baasnõuetele lisasin:

✅ 3 elu süsteem — mängijal on 3 elu, uss taaskäivitatakse pärast kokkupõrget
✅ Enese hammustamine — mäng lõpeb, kui uss hammustab oma keha
✅ Dünaamiline kiirus — mäng läheb kiiremaks iga 50 punkti tagant
✅ Värviline menüü — iga nupp on eraldi värviga (roheline, tsüaan, kollane, punane)
✅ Heliefektid — erinevad helid söömisele ja kaotusele
✅ Edetabel failiga — tulemused salvestatakse skoorid.txt faili
✅ ASCII logo — menüüs on stiilne USS logo