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.
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.
Generator liczb losowych
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łanie | Moż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).
Losowy element, kolor, pozycja
// 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.
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:
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).
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:
// 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.
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.
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ść.
rzut trzema kośćmi: 5 + 6 + 1 = 12
<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>
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
losjest polem klasy, tworzonym raz. Wszystkie losowania korzystają z niego. - Kość to biała ramka z
UniformGrid3 × 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, gdzieXoznacza widoczne oczko. - Ukryte oczka mają
Visibility.Hidden, a nieCollapsed— zajmują dalej miejsce, więc pozostałe nie przesuwają się w siatce. Rzuc_Clicktworzy 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. UniformGridtworzony w kodzie wymagausing System.Windows.Controls.Primitives;— w tej przestrzeni nazw jest ta klasa.
Sprawdź trzy rzeczy
- 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ą.
- Zmień
los.Next(1, 7)nalos.Next(1, 6). Szóstka przestanie wypadać — górna granica nie wchodzi do losowania. - Zmień
new Random()nanew Random(7)i uruchom program dwa razy. Kolejne rzuty będą identyczne.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
new Random(), new Random(ziarno) | obiekt | Generator; z ziarnem — powtarzalny. |
Next(max), Next(min, max) | int | Liczba od min do max − 1. |
NextDouble() | double | Liczba z przedziału [0, 1). |
Shuffle(tablica) | metoda | Miesza tablicę w miejscu. |
Random.Shared | Random | Gotowy wspólny generator. |
Visibility.Hidden | wartość | Niewidoczna, ale zajmuje miejsce (inaczej niż Collapsed). |
do { … } while (warunek); | pętla | Wykonuje się co najmniej raz — wygodna do losowania „aż się uda”. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
los.Next(1, 6) jako kostka | Nigdy 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ście | Next(0) zwraca 0 — a elementu o numerze 0 nie ma. |
sześć razy Next(1, 50) jako totolotek | Liczby mogą się powtórzyć. |
| losowa pozycja bez odjęcia rozmiaru kontrolki | Kontrolka częściowo wychodzi poza obszar. |
(byte)los.Next(255) jako składowa koloru | Nigdy nie wypadnie 255. Pełny zakres to Next(256). |
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.
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
Tagprzycisku opcji; losowanielos.Next(1, gora + 1).
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.
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).
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[].
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 wTag. - Opóźnione zakrycie: osobny zegar z
Interval800 ms, który wTickzakrywa karty i sam się zatrzymuje.
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.
Co trzeba zapamiętać
- Jeden generator
Randomjako pole klasy;Next(min, max)nie losujemax. - 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.