Przejdź do treści
School IT / Przestrzeń do nauki

Losowanie w aplikacji

Kostka, losowanie ucznia do odpowiedzi, tasowanie kart, przeciwnik w grze, który nie zachowuje się zawsze tak samo — wszędzie potrzebne są liczby losowe. Klasę Random znasz z kursu C#. Tu połączysz ją z oknem: wylosujesz element listy, kolor i pozycję, wymieszasz tablicę i wylosujesz kilka liczb bez powtórzeń. W przykładzie zegar z poprzedniej lekcji doda krótką animację rzutu kośćmi.

C# XAML Random Next tasowanie bez powtórzeń 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Utworzysz jeden generator liczb losowych dla całego okna i wylosujesz liczbę z podanego zakresu.
  • Wylosujesz element tablicy lub listy, kolor i pozycję kontrolki.
  • Wymieszasz tablicę algorytmem Fishera–Yatesa.
  • Wylosujesz kilka liczb bez powtórzeń.
  • Zrobisz losowanie powtarzalne do testów.
TEORIA

Generator liczb losowych

MainWindow.xaml.cs
public partial class MainWindow : Window
{
    private readonly Random los = new Random();     // jeden generator na całe okno

    private void Losuj_Click(object sender, RoutedEventArgs e)
    {
        int kostka = los.Next(1, 7);         // 1…6 — górna granica NIE jest losowana
        int procent = los.Next(101);         // 0…100
        double ulamek = los.NextDouble();    // 0,0 ≤ x < 1,0
        bool orzel = los.Next(2) == 0;       // rzut monetą

        Wynik.Text = $"Kostka: {kostka}, procent: {procent}, moneta: {(orzel ? "orzeł" : "reszka")}";
    }
}
WywołanieMożliwe wyniki
los.Next(10)0, 1, …, 9
los.Next(1, 7)1, 2, …, 6
los.Next(-5, 6)−5, …, 5
los.NextDouble()liczba rzeczywista od 0 (włącznie) do 1 (bez 1)
min + los.NextDouble() * (max - min)liczba rzeczywista z przedziału [min, max)

Generator jest polem klasy — tworzysz go raz i używasz wszędzie. W starszych wersjach .NET dwa generatory utworzone w tej samej chwili dawały identyczne ciągi liczb; w .NET 10 już tak nie jest, ale jeden wspólny generator to nadal najprostszy i najczytelniejszy wzór. Gotowy wspólny generator ma też sama klasa: Random.Shared.Next(1, 7).

TEORIA

Losowy element, kolor, pozycja

MainWindow.xaml.cs
// losowy element tablicy: numer od 0 do Length − 1
string[] imiona = { "Ola", "Kuba", "Maja", "Igor" };
string wylosowane = imiona[los.Next(imiona.Length)];

// losowa pozycja listy w oknie
if (Lista.Items.Count > 0)
{
    Lista.SelectedIndex = los.Next(Lista.Items.Count);
}

// losowy kolor (lekcja 06)
Color kolor = Color.FromRgb((byte)los.Next(256), (byte)los.Next(256), (byte)los.Next(256));
Kafel.Background = new SolidColorBrush(kolor);

// losowy margines — kontrolka w losowym miejscu obszaru
double x = los.NextDouble() * (Obszar.ActualWidth - Cel.Width);
double y = los.NextDouble() * (Obszar.ActualHeight - Cel.Height);
Cel.Margin = new Thickness(x, y, 0, 0);

Zawsze odejmuj rozmiar kontrolki od rozmiaru obszaru — inaczej kontrolka wylosowana przy prawej krawędzi wyjdzie poza okno.

TEORIA

Tasowanie

Żeby ułożyć elementy w losowej kolejności (talia kart, kolejność pytań w teście), używa się algorytmu Fishera–Yatesa: idziemy od końca tablicy i każdy element zamieniamy z losowym elementem spośród tych, które jeszcze nie zostały ustawione:

MainWindow.xaml.cs
private void Wymieszaj(string[] tablica)
{
    for (int i = tablica.Length - 1; i > 0; i--)
    {
        int j = los.Next(i + 1);          // 0…i
        string pom = tablica[i];          // zamiana i ↔ j
        tablica[i] = tablica[j];
        tablica[j] = pom;
    }
}

Każde ułożenie jest tak samo prawdopodobne. Ta sama metoda jest wbudowana w .NET: los.Shuffle(tablica).

TEORIA

Bez powtórzeń

Losując np. 6 liczb z 49, nie możesz po prostu wywołać Next sześć razy — ta sama liczba może wypaść dwa razy. Dwa sposoby:

MainWindow.xaml.cs
// 1. Pamiętaj, co już wypadło, i losuj ponownie przy powtórce
bool[] byla = new bool[50];              // byla[1]…byla[49]
int[] wynik = new int[6];
for (int i = 0; i < 6; i++)
{
    int liczba;
    do
    {
        liczba = los.Next(1, 50);
    } while (byla[liczba]);              // powtórka — losuj jeszcze raz
    byla[liczba] = true;
    wynik[i] = liczba;
}

// 2. Wymieszaj wszystkie liczby i weź pierwsze sześć
int[] kula = new int[49];
for (int i = 0; i < 49; i++) kula[i] = i + 1;
los.Shuffle(kula);
// kula[0]…kula[5] to wynik losowania

Sposób pierwszy jest dobry, gdy losujesz mało z dużego zbioru. Gdy losujesz prawie wszystko (np. kolejność 30 pytań), tasowanie jest szybsze i prostsze.

TEORIA

Powtarzalne losowanie

Generator podany z liczbą w konstruktorze — ziarnem — zawsze daje ten sam ciąg wyników: new Random(2026). To przydatne przy testach i szukaniu błędów: gra zawsze startuje z tego samego układu. W gotowym programie ziarno usuwasz.

PRZYKŁAD

Kości do gry ze statystyką

Program rzuca od jednej do pięciu kości. Po kliknięciu Rzuć! oczka migają przez chwilę (12 losowań co 60 ms), a ostatnie losowanie jest wynikiem. Po prawej statystyka: jaki procent wszystkich rzuconych kości dał 1, 2, … 6 oczek. Po wielu rzutach paski wyrównują się w okolicy 17% — tak działa losowość.

Kości do gry–✕
Liczba kości:
3▾
Rzuć!
Suma: 12
Wypadło (kości razem: 60)
1
15%
2
18%
3
20%
4
13%
5
17%
6
17%

rzut trzema kośćmi: 5 + 6 + 1 = 12

MainWindow.xaml — pełny plik
<Window x:Class="Lekcja28.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Kości do gry" Height="470" Width="640"
        ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">

    <DockPanel Margin="15">
        <!-- sterowanie u góry -->
        <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,12">
            <TextBlock Text="Liczba kości:" VerticalAlignment="Center" Margin="0,0,8,0"/>
            <ComboBox x:Name="Ile" Width="60" SelectedIndex="1">
                <ComboBoxItem Content="1"/>
                <ComboBoxItem Content="2"/>
                <ComboBoxItem Content="3"/>
                <ComboBoxItem Content="4"/>
                <ComboBoxItem Content="5"/>
            </ComboBox>
            <Button x:Name="Rzuc" Content="Rzuć!" Width="100" Height="32" Margin="15,0,0,0"
                    FontWeight="Bold" Click="Rzuc_Click"/>
            <TextBlock x:Name="Suma" FontSize="20" FontWeight="Bold" VerticalAlignment="Center"
                       Margin="20,0,0,0"/>
        </StackPanel>

        <!-- statystyka po prawej: ile razy wypadło 1…6 -->
        <StackPanel DockPanel.Dock="Right" Width="200" Margin="15,0,0,0">
            <TextBlock x:Name="Naglowek" FontWeight="Bold" Margin="0,0,0,6"/>
            <StackPanel x:Name="Statystyka"/>
        </StackPanel>

        <!-- kości: tworzone w kodzie -->
        <WrapPanel x:Name="Kosci" Background="#FF2E7D32"/>
    </DockPanel>
</Window>
MainWindow.xaml.cs — pełny plik
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Threading;

namespace Lekcja28;

public partial class MainWindow : Window
{
    // JEDEN generator liczb losowych na cały program
    private readonly Random los = new Random();

    // które z 9 oczek (siatka 3 × 3) są widoczne dla wyniku 1–6
    private readonly string[] wzory =
    {
        "....X....",   // 1
        "X.......X",   // 2
        "X...X...X",   // 3
        "X.X...X.X",   // 4
        "X.X.X.X.X",   // 5
        "X.XX.XX.X"    // 6
    };

    private readonly int[] ileRazy = new int[7];   // ileRazy[1]…ileRazy[6]; element 0 nieużywany
    private int rzutow = 0;

    private readonly DispatcherTimer animacja = new DispatcherTimer();
    private int klatka = 0;

    public MainWindow()
    {
        InitializeComponent();

        // paski statystyki dla oczek 1–6
        for (int i = 1; i <= 6; i++)
        {
            StackPanel wiersz = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) };
            wiersz.Children.Add(new TextBlock { Text = $"{i}", Width = 16 });
            wiersz.Children.Add(new ProgressBar { Width = 130, Height = 14, Maximum = 1 });
            wiersz.Children.Add(new TextBlock { Width = 40, TextAlignment = TextAlignment.Right });
            Statystyka.Children.Add(wiersz);
        }

        animacja.Interval = TimeSpan.FromMilliseconds(60);
        animacja.Tick += Animacja_Tick;

        PokazStatystyke();
    }

    private void Rzuc_Click(object sender, RoutedEventArgs e)
    {
        // tyle kości, ile wybrano — każda to UniformGrid 3 × 3 z oczkami
        int ile = Ile.SelectedIndex + 1;
        Kosci.Children.Clear();
        for (int i = 0; i < ile; i++)
        {
            Kosci.Children.Add(NowaKosc());
        }

        klatka = 0;
        Rzuc.IsEnabled = false;
        Ile.IsEnabled = false;
        animacja.Start();              // krótka „animacja turlania”
    }

    private void Animacja_Tick(object? sender, EventArgs e)
    {
        klatka++;

        int suma = 0;
        foreach (Border kosc in Kosci.Children)
        {
            int wynik = los.Next(1, 7);   // 1, 2, 3, 4, 5 albo 6 — górna granica NIE wchodzi
            PokazOczka(kosc, wynik);
            suma += wynik;

            if (klatka == 12)             // ostatnia klatka = prawdziwy wynik
            {
                ileRazy[wynik]++;
                rzutow++;
            }
        }

        if (klatka == 12)
        {
            animacja.Stop();
            Suma.Text = $"Suma: {suma}";
            Rzuc.IsEnabled = true;
            Ile.IsEnabled = true;
            PokazStatystyke();
        }
        else
        {
            Suma.Text = "…";
        }
    }

    private Border NowaKosc()
    {
        UniformGrid oczka = new UniformGrid { Rows = 3, Columns = 3 };
        for (int i = 0; i < 9; i++)
        {
            oczka.Children.Add(new Border
            {
                Width = 14,
                Height = 14,
                CornerRadius = new CornerRadius(7),     // kółko
                Background = Brushes.Black
            });
        }

        return new Border
        {
            Width = 90,
            Height = 90,
            Margin = new Thickness(10),
            Padding = new Thickness(10),
            Background = Brushes.White,
            CornerRadius = new CornerRadius(12),
            Child = oczka
        };
    }

    private void PokazOczka(Border kosc, int wynik)
    {
        UniformGrid oczka = (UniformGrid)kosc.Child;
        string wzor = wzory[wynik - 1];

        for (int i = 0; i < 9; i++)
        {
            oczka.Children[i].Visibility = wzor[i] == 'X' ? Visibility.Visible : Visibility.Hidden;
        }
    }

    private void PokazStatystyke()
    {
        Naglowek.Text = $"Wypadło (kości razem: {rzutow})";

        for (int i = 1; i <= 6; i++)
        {
            StackPanel wiersz = (StackPanel)Statystyka.Children[i - 1];
            ProgressBar pasek = (ProgressBar)wiersz.Children[1];
            TextBlock opis = (TextBlock)wiersz.Children[2];

            double udzial = rzutow == 0 ? 0 : (double)ileRazy[i] / rzutow;
            pasek.Value = udzial;
            opis.Text = $"{udzial * 100:F0}%";
        }
    }
}

Co dzieje się po kolei

  • Generator los jest polem klasy, tworzonym raz. Wszystkie losowania korzystają z niego.
  • Kość to biała ramka z UniformGrid 3 × 3, w którym leży 9 okrągłych oczek. Wynik 1–6 decyduje, które oczka są widoczne — wzory są zapisane w tablicy napisów, gdzie X oznacza widoczne oczko.
  • Ukryte oczka mają Visibility.Hidden, a nie Collapsed — zajmują dalej miejsce, więc pozostałe nie przesuwają się w siatce.
  • Rzuc_Click tworzy tyle kości, ile wybrano, blokuje przycisk i uruchamia zegar animacji.
  • Każde tyknięcie losuje nowe oczka dla każdej kości (los.Next(1, 7)). Po 12. tyknięciu zegar się zatrzymuje, a wyniki tego losowania trafiają do statystyki.
  • Statystyka liczy, ile razy wypadła każda liczba oczek, w tablicy ileRazy[1…6] — element 0 pomijamy, żeby numer elementu był równy liczbie oczek.
  • Paski mają zakres 0–1, a wartością jest udział danej liczby oczek. Rzutowanie (double) zapobiega dzieleniu całkowitemu.
  • UniformGrid tworzony w kodzie wymaga using System.Windows.Controls.Primitives; — w tej przestrzeni nazw jest ta klasa.

Sprawdź trzy rzeczy

  1. Rzuć pięcioma kośćmi 20 razy i obserwuj paski — przy 100 kościach różnice są jeszcze wyraźne, przy kilkuset paski się wyrównują.
  2. Zmień los.Next(1, 7) na los.Next(1, 6). Szóstka przestanie wypadać — górna granica nie wchodzi do losowania.
  3. Zmień new Random() na new Random(7) i uruchom program dwa razy. Kolejne rzuty będą identyczne.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
new Random(), new Random(ziarno)obiektGenerator; z ziarnem — powtarzalny.
Next(max), Next(min, max)intLiczba od min do max − 1.
NextDouble()doubleLiczba z przedziału [0, 1).
Shuffle(tablica)metodaMiesza tablicę w miejscu.
Random.SharedRandomGotowy wspólny generator.
Visibility.HiddenwartośćNiewidoczna, ale zajmuje miejsce (inaczej niż Collapsed).
do { … } while (warunek);pętlaWykonuje się co najmniej raz — wygodna do losowania „aż się uda”.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
los.Next(1, 6) jako kostkaNigdy nie wypadnie 6 — górna granica nie wchodzi.
tablica[los.Next(tablica.Length + 1)]Czasem IndexOutOfRangeException — ostatni numer to Length − 1.
los.Next(Lista.Items.Count) przy pustej liścieNext(0) zwraca 0 — a elementu o numerze 0 nie ma.
sześć razy Next(1, 50) jako totolotekLiczby mogą się powtórzyć.
losowa pozycja bez odjęcia rozmiaru kontrolkiKontrolka częściowo wychodzi poza obszar.
(byte)los.Next(255) jako składowa koloruNigdy nie wypadnie 255. Pełny zakres to Next(256).
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Używaj jednego generatora liczb losowych na całe okno. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Zgadnij liczbę★☆☆

Program losuje liczbę z zakresu wybranego przyciskami opcji (1–50, 1–100, 1–1000). Gracz wpisuje propozycję i zatwierdza Enter. Odpowiedź: „za mało”, „za dużo” albo „Brawo! Trafiłeś w 7 próbach”. Pod polem lista dotychczasowych prób z podpowiedziami, a obok najlepszy wynik dla każdego zakresu. Po trafieniu przycisk Nowa gra.

pokaż wskazówkę
  • Górna granica zakresu w Tag przycisku opcji; losowanie los.Next(1, gora + 1).
ZAD 2Kto do odpowiedzi?★☆☆

Lista uczniów (wczytana z tablicy w konstruktorze) i przycisk Losuj. Wylosowany uczeń pojawia się dużą czcionką i zostaje zaznaczony na liście. Pole wyboru Bez powtórzeń: wylosowani przenoszą się na drugą listę „Byli pytani” i nie biorą udziału w kolejnych losowaniach, aż pierwsza lista się opróżni (wtedy komunikat i przycisk Od nowa). Przez 1 sekundę przed wynikiem na ekranie migają losowe imiona.

pokaż wskazówkę
  • Miganie: zegar co 80 ms przez określoną liczbę tyknięć, jak animacja kości w przykładzie.
ZAD 3Generator haseł★★☆

Suwak długości (8–32), pola wyboru: małe litery, wielkie litery, cyfry, znaki specjalne, „bez podobnych znaków” (bez l, 1, I, O, 0). Przycisk Generuj tworzy hasło zawierające co najmniej jeden znak z każdej zaznaczonej grupy, a pozostałe znaki losuje ze wszystkich zaznaczonych grup. Pod hasłem ocena siły jak w lekcji 18. Przycisk Kopiuj kopiuje hasło do schowka. Przy żadnej zaznaczonej grupie przycisk generowania jest nieaktywny.

pokaż wskazówkę
  • Najpierw dodaj po jednym znaku z każdej zaznaczonej grupy, potem dolosuj resztę, a na końcu wymieszaj znaki — inaczej wymagane znaki zawsze będą na początku.
  • Schowek: Clipboard.SetText(tekst).
ZAD 4Totolotek★★☆

Kupon: siatka 49 przycisków-liczb (7 × 7); kliknięcie zaznacza/odznacza liczbę, najwyżej 6. Przycisk Chybił trafił zaznacza 6 losowych liczb. Losowanie wylosowuje 6 liczb bez powtórzeń, pokazuje je posortowane w kulach (okrągłe ramki) i podaje liczbę trafień, wyróżniając trafione liczby na kuponie. Przycisk Symuluj 10 000 losowań pokazuje, ile razy kupon trafiłby 3, 4, 5 i 6 liczb.

pokaż wskazówkę
  • Symulacja to pętla bez rysowania — rysuj dopiero wynik. Losowanie bez powtórzeń wydziel do metody zwracającej int[].
ZAD 5Memory★★☆

Plansza 4 × 4 zakrytych kart (8 par symboli, np. litery albo znaki ♠ ♥ ♦ ♣ ★ ☀ ☂ ♫), wymieszana przy każdej nowej grze. Kliknięcie odkrywa kartę. Gdy odkryte są dwie: jeśli tworzą parę, zostają odkryte; jeśli nie — po 0,8 sekundy się zakrywają, a w tym czasie kliknięcia są ignorowane. Licznik ruchów, czas gry i komunikat po odkryciu wszystkich par.

pokaż wskazówkę
  • Tablica 16 symboli (każdy dwa razy), wymieszana przez Shuffle. Karta zna swój numer w Tag.
  • Opóźnione zakrycie: osobny zegar z Interval 800 ms, który w Tick zakrywa karty i sam się zatrzymuje.
ZAD 6Saper★★★

Plansza 9 × 9 z 10 losowo rozmieszczonymi minami. Lewy przycisk myszy odkrywa pole: mina kończy grę (pokazując wszystkie miny), a inne pole pokazuje liczbę min w 8 sąsiednich polach (kolor zależny od liczby). Pole bez min w sąsiedztwie odkrywa automatycznie wszystkie sąsiednie pola. Prawy przycisk stawia i zdejmuje flagę. Licznik „Miny: 10 − flagi”, czas gry i wygrana, gdy odkryto wszystkie pola bez min. Pierwsze kliknięcie nigdy nie trafia w minę.

pokaż wskazówkę
  • Miny losuj dopiero po pierwszym kliknięciu, z pominięciem klikniętego pola.
  • Automatyczne odkrywanie najprościej napisać jako metodę, która dla pustego pola wywołuje samą siebie dla sąsiadów (rekurencja) — pamiętaj, żeby nie odkrywać pola drugi raz.
PODSUMOWANIE

Co trzeba zapamiętać

  • Jeden generator Random jako pole klasy; Next(min, max) nie losuje max.
  • Losowy element: tablica[los.Next(tablica.Length)]; losowa pozycja: odejmij rozmiar kontrolki od rozmiaru obszaru.
  • Tasowanie: Fisher–Yates albo los.Shuffle(tablica).
  • Bez powtórzeń: zapamiętuj wylosowane albo wymieszaj i weź początek.
  • Ziarno w konstruktorze daje powtarzalne wyniki — do testów.

Dokumentacja: Microsoft Learn — klasa Random.

Postęp zapisuje się w tej przeglądarce.