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:
Punkt(Point) – Abiklass, mis hoiab x ja y koordinaate ekraanil ja oskab ennast konsooli joonistada (nt sümbolina*või#).Uss(Snake) – Peamine tegelane. Tema keha koosneb mitmestPunktobjektist (List). Ta teab oma suunda ja oskab liikuda, kasvada ning kontrollida, kas ta hammustas ennast või seina.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.