Madu/Uss

Selle projekti eesmärk oli luua funktsionaalne ussimäng, kasutades objektorienteeritud programmeerimise (OOP) põhiprintsiipe. Selle asemel, et kirjutada kogu kood ühte pikka faili, jagati programm iseseisvateks klassideks, millest igaühel on oma selge vastutusala (kapseldatus)

Esialgul lõin projekti Visual Stuidios ja nimetasin seda Madu

Lisasin kood mis oli juba antud ehk baasmäng

Siin on ekraanipilt mis saab pärast käivitamist

Nüüd arendame seda mängu edasi

esimeseks ma tegin raskusastmed ja Ekraani seaded

Eesmärk ja loogika

Selle klassi eesmärk on kaotada koodist “maagilised numbrid” (magic numbers). Klass kapseldab endasse mänguvälja mõõtmed ja kiiruse. Kasutades switch-konstruktsiooni, määratakse mängu parameetrid dünaamiliselt vastavalt mängija valitud raskusastmele. Mida raskem tase, seda väiksem on viivitus millisekundites (KiirusMS) ehk seda kiiremini uss liigub.

Siin on kood:

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

       public ManguSeaded(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;
           }
       }
   }

Muutusin ka programm cs-i et programmi käivitamisel tuleks ette ilus munüü ja taseme valik

Ja siin on tulemus

Selle asemel, et kontrollida ussi põrkumist konsooliakna äärega, lõin klassi Kaart. See loob List<Punkt> kollektsiooni, kuhu genereeritakse tsüklite abil # sümbolitest koosnevad välisseinad.

  • OOP eelis: Kuna seinad on dünaamiline nimekiri punktidest, võimaldab see arhitektuur tulevikus luua erinevaid tasemeid, lisades Takistused listi ka kaardi keskel asuvaid seinte elemente või labürinte.
using System;
using System.Collections.Generic;

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

    public Kaart(int laius, int kõrgus)
    {
        // Horisontaalsed välisseinad (ülemine ja alumine)
        for (int x = 0; x < laius; x++)
        {
            Takistused.Add(new Punkt(x, 0, '#'));
            Takistused.Add(new Punkt(x, kõrgus - 1, '#'));
        }
        // Vertikaalsed välisseinad (vasak ja parem)
        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();
    }
}

Joonistasin ka seinaid

Siin on tulemus

Faili salvestamine ja Edetabel (Edetabel.cs)

Eesmärk ja loogika

Klass pakub andmete püsivust (data persistence). Tegemist on static klassiga, mis tähendab, et sellest ei looda isendit, vaid meetodeid kutsutakse otse välja.

  • Salvesta() meetod kirjutab andmed faili skoorid.txt kasutades formaati Nimi;Skoor.
  • KuvaEdetabel() meetod kasutab päringuid, et andmed failist lugeda, tükeldada (Split), sorteerida kahanemise järjekorras (OrderByDescending) ja võtta funktsiooniga Take(5) ainult TOP 5 parimat tulemust.
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(';'))
            .Where(osad => osad.Length == 2 && int.TryParse(osad[1], out _))
            .Select(osad => new { Nimi = osad[0], Punktid = int.Parse(osad[1]) })
            .OrderByDescending(x => x.Punktid)
            .Take(5);

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

See küsib nime ja pärast salvestab tulemust

Mängu käivitamisel leidsin viga, et kõige raskemal tasemel uss on seinate väljase ehk parandasin seda

Siin on tulemus

heliefektid (Heliefektid.cs)

Eesmärk ja loogika

Heliefektide lisamiseks kasutatakse konsooli baasheli Console.Beep(sagedus, kestus). Kuna helisignaal on sünkroonne ja paneb programmi ootele, siis otse peatsüklis mängides hakkaks uss heli ajal hakkima. Probleemi lahendamiseks kasutasin asünkroonset meetodit Task.Run(), mis käivitab heli eraldi lõimes (thread), tagades mängu täieliku sujuvuse.

using System;
using System.Threading.Tasks;

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

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

Mängureeglite täiendused: Enese hammustamine ja punktisüsteem

Mängitavuse tõstmiseks lisati funktsionaalsused otse põhiklassidesse:

Enese hammustamise kontroll (Uss.cs sees)

Uss kaotab mängu, kui tema pea koordinaadid kattuvad mõne tema kehaosa koordinaadiga. LINQ laiendusmeetod Skip(1) jätab kontrollist välja pea enda (listi esimene element) ja Any() vaatab, kas mõni ülejäänud punkt ühtib peaga.

public bool KasHammustasEnnast()
{
    Punkt pea = HangiPea();
    return keha.Skip(1).Any(p => p.X == pea.X && p.Y == pea.Y);
}

Dünaamiline punktisüsteem (Program.cs sees)

Loodi muutuja int skoor = 0;. Iga kord, kui peatsükkel tuvastab, et pea asukoht kattub toidu asukohaga, suurendatakse skoori 10 punkti võrra ja väljastatakse see reaalajas mänguvälja allääres.

// Loogika peatsükli sees toidu söömisel:
if (pea.X == toit.Asukoht.X && pea.Y == toit.Asukoht.Y)
{
    skoor += 10;
    uss.Kasva();
    toit.LooUusToit();
    Heliefektid.MängiSöömist();
}

// Skoori kuvamine reaalajas kaardi all:
Console.SetCursorPosition(0, seaded.Kõrgus + 1);
Console.Write($"Skoor: {skoor}");

Siin on rohkem pilte mängu väljanägemiseks:

Raskuse valik

Mäng ise

Erinevad raskustasemed

Mängu lõpp ja Salvestamis küsimus nimeks

Ja edetabel

github link: https://github.com/MihkelUnt2006/Madu/tree/master/Madu