Ussi Mäng Test

1. Arhitektuur: Milliseid klasse me vajame?

OOP-s jagame programmi iseseisvateks osadeks (objektideks), mis teevad omavahel koostööd. Ussimängu jaoks on meil vaja kolme peamist komponenti:

  1. Punkt (Point) – Abiklass, mis hoiab x ja y koordinaate ekraanil ja oskab ennast konsooli joonistada (nt sümbolina * või #).
  2. Uss (Snake) – Peamine tegelane. Tema keha koosneb mitmest Punkt objektist (List). Ta teab oma suunda ja oskab liikuda, kasvada ning kontrollida, kas ta hammustas ennast või seina.
  3. Toit (Food) – Objekt, mis ilmub ekraanile suvalisse kohta ja mida uss peab ära sööma.

2. Klasside loomise sammud

Samm 1: Suund Enum ja Punkt klass

Kõigepealt on vaja viisi, kuidas määrata liikumissuunda ja hoida koordinaate. Konsoolis liikumine tähendab lihtsalt X (veerg) ja Y (rida) väärtuste muutmist.

// 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; } // Näiteks '*' ussi jaoks, '@' toidu jaoks

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

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

    // Puhastab eelmise asukoha (kui uss liigub edasi)
    public void Kustuta()
    {
        Console.SetCursorPosition(X, Y);
        Console.Write(' '); // Kirjutame tühiku peale
    }
}

Samm 2: Toit klass

Toidu eesmärk on luua uus Punkt suvaliste koordinaatidega ekraani piirides.

public class Toit
{
    private Random rnd = new Random();
    private int ekraaniLaius;
    private int ekraaniKõrgus;
    public Punkt Asukoht { get; private set; }

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

    public void LooUusToit()
    {
        int x = rnd.Next(2, ekraaniLaius - 2);
        int y = rnd.Next(2, ekraaniKõrgus - 2);
        Asukoht = new Punkt(x, y, '@'); // Toit näeb välja nagu '@'
        Asukoht.Joonista();
    }
}

Samm 3: Uss klass (Kõige olulisem!)

Uss on List<Punkt>, kus esimene element on pea ja viimane on sabaots. Liikumise loogika on lihtne: lisame liikumissuunas uue “pea” ja kustutame “saba” (viimase elemendi). Kui uss sööb toidu, siis me lihtsalt ei kustuta saba ja uss muutub ühe punkti võrra pikemaks!

using System.Collections.Generic;
using System.Linq;

public class Uss
{
    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();
        }
    }

    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 (viimase elemendi), et simuleerida liikumist
        Punkt saba = keha.Last();
        saba.Kustuta();
        keha.Remove(saba);
    }

    public Punkt HangiPea()
    {
        return keha.First();
    }

    public void Kasva()
    {
        // Kasvamine on lihtne - lisame saba lõppu lihtsalt uue nähtamatu punkti, 
        // mis järgmise liikumise ajal asendab päris saba.
        // Lihtsuse mõttes võime lihtsalt järgmisel liikumisel saba mitte kustutada!
        // Siin lisame lihtsalt ajutise koopia viimasest elemendist.
        keha.Add(new Punkt(keha.Last().X, keha.Last().Y, '*'));
    }
}

3. Mängu Peatsükkel (Game Loop) – Program.cs

Siin paneme kõik klassid kokku. Loome lõputu tsükli (while(true)), mis loeb kasutaja klahvivajutusi, liigutab ussi ja kontrollib reegleid.

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        // Seadistame konsooli akna
        Console.WindowHeight = 20;
        Console.WindowWidth = 50;
        Console.CursorVisible = false;

        // Loome objektid
        Uss uss = new Uss(10, 10, 3);
        Toit toit = new Toit(Console.WindowWidth, Console.WindowHeight);

        // MÄNGU TSÜKKEL
        while (true)
        {
            // 1. Sisendi lugemine (kas kasutaja vajutas nooleklahvi?)
            if (Console.KeyAvailable)
            {
                ConsoleKeyInfo klahv = Console.ReadKey(true);
                // Muudame suunda, aga väldime tagurdamist
                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;
            }

            // 2. Objektide uuendamine
            uss.Liigu();
            Punkt pea = uss.HangiPea();

            // 3. Kas uss sõi toidu ära?
            if (pea.X == toit.Asukoht.X && pea.Y == toit.Asukoht.Y)
            {
                uss.Kasva();
                toit.LooUusToit();
            }

            // 4. Kas uss põrkas vastu seina? (Lihtne mängu lõpu kontroll)
            if (pea.X <= 0 || pea.X >= Console.WindowWidth - 1 || 
                pea.Y <= 0 || pea.Y >= Console.WindowHeight - 1)
            {
                Console.SetCursorPosition(15, 10);
                Console.WriteLine("MÄNG LÄBI!");
                break; // Murrame tsüklist välja
            }

            // Mängu kiirus (mida väiksem number, seda kiirem uss)
            Thread.Sleep(150); 
        }

        Console.ReadLine();
    }
}

Meie kood tulemus:

“*” Sümbolid on meie uss, “@” sümbol on meie õun miss uss peab sööma. Kui meie uss õuna sööb, muutub see pikemaks. Kui meie uss puudutab oma seina, siis mäng lõpeb. Kuid mänguväljakut piiravad nähtamatud seinad, mille lisame järgmises osas.

Kui mäng lõpeb, siis meie uus peatub ja kuvatakse teade “mäng läbi”.


4. Bonus ideedega laiendada

1. Raskusastmed ja Ekraani seaded

Selleks on kõige parem luua klass MänguSeaded, mis hoiab endas kõiki muutujaid. See väldib “maagiliste numbrite” kasutamist koodis.

public class MänguSeaded
{
    public int Laius { get; set; }
    public int Kõrgus { get; set; }
    public int KiirusMS { get; set; } // Viivitus millisekundites

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

2. Seinad ja Takistused (Walls)

Selle asemel, et kontrollida ainult akna piire, loome klassi Kaart, mis hoiab nimekirja punktidest, kus asuvad takistused.

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

    public Kaart(int laius, int kõrgus)
    {
        // Loome ümbermõõdu (välisseinad)
        for (int x = 0; x < laius; x++)
        {
            Takistused.Add(new Punkt(x, 0, '#'));
            Takistused.Add(new Punkt(x, kõrgus - 1, '#'));
        }
        for (int y = 0; y < kõrgus; y++)
        {
            Takistused.Add(new Punkt(0, y, '#'));
            Takistused.Add(new Punkt(laius - 1, y, '#'));
        }
    }

    public void Joonista()
    {
        foreach (var p in Takistused) p.Joonista();
    }
}

3. Faili salvestamine ja Edetabel

Edetabeli jaoks loome staatilise klassi, mis tegeleb faili lugemise ja kirjutamisega. See on õpilastele hea näide List sorteerimisest.

using System.IO;
using System.Linq;

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

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

    public static void KuvaEdetabel()
    {
        if (!File.Exists(failiTee)) return;

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

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

4. Muusika ja Heliefektid

Konsoolirakenduses on kõige lihtsam kasutada Console.Beep() funktsiooni. See on suurepärane võimalus õpetada asünkroonset käivitamist, et heli ei paneks mängu “hakkima”.

using System.Threading.Tasks;

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

    public static void MängiKaotust()
    {
        Task.Run(() => {
            Console.Beep(400, 200);
            Console.Beep(300, 200);
            Console.Beep(200, 400);
        });
    }
}

Kuidas need kõik Main-tsüklis kokku jooksevad?

Siin on näide, kuidas peatsükkel uute klassidega välja näeb:

// 1. Mängu algus ja seadistamine
Console.WriteLine("Vali raskus (1-3): ");
int tase = int.Parse(Console.ReadLine());
MänguSeaded seaded = new MänguSeaded(tase);

Console.SetWindowSize(seaded.Laius + 2, seaded.Kõrgus + 2);
Kaart kaart = new Kaart(seaded.Laius, seaded.Kõrgus);
Uss uss = new Uss(10, 10, 3);
Toit toit = new Toit(seaded.Laius, seaded.Kõrgus);
int skoor = 0;

kaart.Joonista();

// 2. Mängu peatsükkel
while (true)
{
    // ... sisendi lugemine ...
    
    uss.Liigu();
    Punkt pea = uss.HangiPea();

    // Kokkupõrge seinaga (takistusega)
    if (kaart.Takistused.Any(t => t.X == pea.X && t.Y == pea.Y))
    {
        Heliefektid.MängiKaotust();
        break; 
    }

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

    Thread.Sleep(seaded.KiirusMS);
}

// 3. Pärast mängu lõppu
Console.Clear();
Console.Write("Mäng läbi! Sisesta oma nimi: ");
string nimi = Console.ReadLine();
Edetabel.Salvesta(nimi, skoor);
Edetabel.KuvaEdetabel();

5. Kõik Main-tsükkel

class Program
{
    static void Main()
    {
        // 1. Mängu algus ja seadistamine
        Console.WriteLine("Vali raskus (1-3): ");
        int tase = int.Parse(Console.ReadLine());
        MänguSeaded seaded = new MänguSeaded(tase);

        Console.SetWindowSize(seaded.Laius + 2, seaded.Kõrgus + 2);
        Console.CursorVisible = false;

        Kaart kaart = new Kaart(seaded.Laius, seaded.Kõrgus);
        Uss uss = new Uss(10, 10, 3);
        Toit toit = new Toit(seaded.Laius, seaded.Kõrgus);
        int skoor = 0;

        kaart.Joonista();

        // 2. Mängu peatsükkel
        while (true)
        {
            // Sisendi lugemine
            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;
            }

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

            // Kokkupõrge seinaga (takistusega)
            if (kaart.Takistused.Any(t => t.X == pea.X && t.Y == pea.Y))
            {
                Heliefektid.MängiKaotust();
                break;
            }

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

            Thread.Sleep(seaded.KiirusMS);
        }

        // 3. Pärast mängu lõppu
        Console.Clear();
        Console.Write("Mäng läbi! Sisesta oma nimi: ");
        string nimi = Console.ReadLine();
        Edetabel.Salvesta(nimi, skoor);
        Edetabel.KuvaEdetabel();
    }
}

Meie uuendatud kood tulemus:

Kõigepealt palutakse kasutajal valida raskusaste. Mida kõrgem on raskusaste, seda kiiremini madu liigub.

Mäng jääb samaks, aga nüüd on mänguväljal selged piirid.

Kui mäng lõpeb, palutakse teil sisestada oma nimi, et see saaks tulemustabeli faili salvestada.

Enne koodi lõppu kuvatakse tulemustabel 5 parima mängija ja nende punktisummaga.