INF.04 styczeń 2026 — gra w kości: aplikacja konsolowa, mobilna i testy jednostkowe
Rozbiór arkusza INF.04 ze stycznia 2026: klasa Kosc w aplikacji konsolowej, mobilna gra w pięć kości oraz testy jednostkowe metody rzutu. Plan na 180 minut i rozwiązania do odsłonięcia.
Oryginalny arkusz CKE (PDF)
Pobierz plik źródłowy i rozwiąż go samodzielnie przed sprawdzeniem rozwiązań.
Arkusz, w którym czwartym rezultatem są testy
Styczeń 2026 to sesja INF.04, w której obok aplikacji konsolowej i mobilnej oceniane są osobno testy jednostkowe. To jedna czwarta wyniku za kod, który u wielu zdających nigdy nie powstaje, bo „zabrakło czasu". A napisanie dwóch metod testowych do gotowej klasy zajmuje kwadrans — pod warunkiem, że wiesz, jak wygląda projekt testowy w twoim środowisku i nie szukasz tego dopiero na egzaminie.
Każdy etap zaczyna się od wymagań arkusza — te są widoczne od razu. Rozwiązanie jest schowane pod przyciskiem. Zanim je odsłonisz, spróbuj napisać kod samodzielnie: samo przeczytanie cudzego rozwiązania nie przygotowuje do egzaminu, na którym trzeba wyprodukować je z głowy w 180 minut, w środowisku bez internetu.
📋 Spis treści
1
Metryka arkusza i klucz oceniania
- Kwalifikacja: INF.04 — projektowanie, programowanie i testowanie aplikacji
- Sesja: styczeń 2026, zadanie 01, wersja arkusza SG
- Czas: 180 minut, plus 10 minut na zapoznanie się z zadaniem (niewliczane)
- Próg zdania: 75% punktów
- Temat: gra w kości — klasa Kosc, mobilna gra pięcioma kośćmi, testy metody rzutu
- Materiały: archiwum
zad1.7zz pulpitu konta Egzamin, hasło podane w arkuszu
Arkusz wymienia cztery rezultaty. To dosłownie lista, po której idzie egzaminator:
- implementacja, kompilacja, uruchomienie programów
- aplikacja konsolowa
- aplikacja mobilna
- testy aplikacji
Pierwszy rezultat to nie kod, tylko dowody: zrzuty ekranu, archiwa projektów i struktura folderów. Zdobywa się go stolarką, nie programowaniem.
Folder nazwany numerem zdającego, a w nim trzy podfoldery i plik dokumentacji:
PESEL/
egzamin.txt system operacyjny, srodowiska, emulator, jezyki
konsolowa/
konsola.zip archiwum calego projektu konsolowego
Kosc.cs skopiowany plik z kodem zrodlowym
Program.cs skopiowany plik z programem glownym
konsola.exe plik wykonywalny, jesli istnieje
konsola1.png zrzut: uruchomienie programu
konsola2.png zrzut: kolejna interakcja uzytkownika
mobilna/
mobilna.zip archiwum calego folderu projektu
activity_main.xml plik widoku (albo MainPage.xaml)
MainActivity.kt plik logiki (albo MainPage.xaml.cs)
mobilna1.png zrzut: stan poczatkowy
mobilna2.png zrzut: po wcisnieciu RZUT
mobilna3.png zrzut: kosci zablokowane, przezroczystosc 50%
testy/
testy.zip archiwum projektu testowego
KoscTesty.cs plik z metodami testujacymi
test1.png zrzut z wynikami uruchomienia testowNazwy zrzutów i podfolderów są w arkuszu podane wprost. Zrzut nazwany
„zrzut ekranu 2026-01-14.png" zamiast
konsola1.png
to strata punktu za coś, co kosztuje trzy sekundy.
Pułapka organizacyjna tej sesji:
obrazy kości siedzą w archiwum zad1.7z
zabezpieczonym hasłem. Rozpakuj je w pierwszych minutach, jeszcze zanim
zaczniesz cokolwiek pisać — bez plików
kosc0.png
… kosc6.png
aplikacja mobilna nie ma czego wyświetlać, a to cały jeden rezultat.
2
Plan czasowy na 180 minut
Cztery rezultaty ważą podobnie, a najdroższy czasowo — aplikacja mobilna — jest wart tyle samo, co testy pisane przez piętnaście minut. Stąd jedyna sensowna kolejność: najpierw klasa Kosc, bo z niej korzystają dwa pozostałe rezultaty, potem testy (są tanie), a mobilna na końcu z dużym budżetem.
| Etap | Czas | Uwagi |
|---|---|---|
| Foldery, rozpakowanie zad1.7z, plik egzamin | 10 min | darmowe punkty z rezultatu 1, zrób od razu |
| Klasa Kosc i program główny w konsoli | 45 min | fundament dla dwóch pozostałych rezultatów |
| Zrzuty konsoli i archiwum konsola.zip | 10 min | rób od razu po uruchomieniu, nie na końcu |
| Testy jednostkowe: dwa przypadki + uruchomienie | 25 min | najtańszy punktowo etap w całym arkuszu |
| Mobilna: widok XML/XAML, kolory, marginesy | 30 min | grafiki wrzuć do zasobów zanim zaczniesz układ |
| Mobilna: logika, zdarzenia, emulator | 35 min | emulator odpal wcześniej, startuje kilka minut |
| Zrzuty mobilne i archiwum mobilna.zip | 10 min | trzy stany aplikacji, całe ekrany |
| Nagranie płyty i sprawdzenie nagrania | 15 min | rezerwa, nigdy jej nie tnij |
Zasada, która ratuje egzamin: emulator systemu mobilnego uruchamiaj w tle już w dziesiątej minucie, razem z tworzeniem folderów. Zimny start emulatora Androida na sprzęcie egzaminacyjnym to nierzadko 5–10 minut, a to czas, którego pod koniec nie będziesz miał. Jeśli o 150. minucie mobilna nadal się nie kompiluje — zrób zrzut tego, co masz, spakuj projekt i przejdź do nagrywania płyty. Rezultat „aplikacja mobilna" oceniany jest po widoku i kodzie, a nie wyłącznie po działającej emulacji, więc częściowo zrobiony ekran to nadal punkty.
3
Rezultat 1: implementacja, kompilacja, uruchomienie
- Folder o nazwie równej numerowi zdającego, a w nim podfoldery konsolowa, mobilna, testy.
- Praca na koncie Egzamin bez hasła, obrazy z archiwum
zad1.7zzabezpieczonego hasłem. - Zrzuty ekranu dokumentujące uruchomienie każdej części:
konsola1.png,mobilna1.png,test1.pngi kolejne. Każdy zrzut całego ekranu, z paskiem zadań i widocznym środowiskiem programistycznym. - Liczba zrzutów konsoli ma odpowiadać wszystkim możliwym interakcjom użytkownika z programem.
- W każdym podfolderze: archiwum całego projektu, skopiowane pliki źródłowe, plik wykonywalny (jeśli powstaje) i zrzuty.
- Plik tekstowy
egzaminw folderze z numerem zdającego: nazwa systemu, nazwy środowisk, nazwa emulatora, nazwy języków.
To najtańsze punkty na całym egzaminie. Rozpisz sobie tę listę na kartce w pierwszych minutach.
Pokaż rozwiązanieProcedura startowa i wzór pliku egzamin.txt
- Na pulpicie utwórz folder o nazwie równej twojemu numerowi PESEL, a w nim trzy puste podfoldery.
- Rozpakuj
zad1.7zhasłem z arkusza. Sprawdź, czy widzisz siedem plików odkosc0.pngdokosc6.png. - Uruchom emulator mobilny — niech się wygrzewa w tle przez cały czas pracy nad konsolą.
- Napisz plik
egzamin.txt. Zajmuje minutę, a jest osobno punktowanym elementem.
Dokumentacja narzedzi wykorzystanych na egzaminie
System operacyjny: Windows 11 Pro 64-bit
Srodowisko - konsolowa: Microsoft Visual Studio 2022
Srodowisko - testy: Microsoft Visual Studio 2022, biblioteka MSTest
Srodowisko - mobilna: Android Studio Koala
Emulator aplikacji mobilnej: Android Emulator, Pixel 6 API 34
Jezyki programowania: C# (aplikacja konsolowa i testy)
Kotlin, XML (aplikacja mobilna)Arkusz mówi: „liczba zrzutów powinna odpowiadać wszystkim możliwym interakcjom użytkownika z programem". W tym zadaniu interakcja jest jedna — wpisanie liczby z klawiatury — ale ma dwa istotnie różne przebiegi:
konsola1.png— użytkownik wpisuje poprawną wartość z zakresu 1–6, program pokazuje liczbę oczek, słowo i nazwę pliku.konsola2.png— użytkownik wpisuje wartość spoza zakresu (np. 9), obiekt dostaje 0 i nazwękosc0.png.- Opcjonalnie
konsola3.png— wpisanie tekstu zamiast liczby, jeśli twój program to obsługuje.
Gdzie się traci punkty:
kadrowany zrzut samego okna konsoli. Arkusz trzy razy powtarza „cały
obszar ekranu, z widocznym paskiem zadań oraz środowiskiem
programistycznym" — zrzut samej konsoli nie dowodzi, że program
skompilowałeś ty i tutaj. Drugi klasyk to spakowanie folderu
bin
zamiast całego projektu: archiwum ma zawierać cały folder projektu,
łącznie z plikiem rozwiązania i plikami źródłowymi.
4
Rezultat 2: aplikacja konsolowa i klasa Kosc
Klasa o nazwie Kosc w obiektowym języku dostępnym na stanowisku (C++, C#, Java lub Python), modelująca jedną sześcienną kość o wartościach 1÷6.
- ogólnodostępne, statyczne pole z liczbą utworzonych instancji klasy,
- kolekcja napisowa z nazwami plików
kosc0.png…kosc6.png, - liczba wyrzuconych oczek — typ całkowity,
- identyfikator pliku graficznego — typ całkowity, jest to indeks w tablicy nazw,
- informacja, czy kość jest dostępna — typ logiczny.
- Jednoargumentowy: argument to wyrzucona wartość; jeśli nie jest to 1–6, ustawia 0. Przypisuje wartość liczbie oczek i identyfikatorowi pliku, ustawia kość jako dostępną, inkrementuje licznik instancji.
- Bezargumentowy: losuje liczbę pseudolosową 1–6 i robi to samo.
- W Pythonie — jeden konstruktor z domyślną wartością argumentu
None.
- rzut — nic nie zwraca; działa tylko gdy kość jest dostępna: losuje 1–6 i zapisuje do liczby oczek oraz identyfikatora pliku,
- blokada — nic nie zwraca, ustawia kość jako niedostępną,
- wartość słownie — zwraca tekst, np. dla 3 zwraca „trzy".
- dwa obiekty klasy Kosc, każdy utworzony innym konstruktorem,
- argument konstruktora jednoargumentowego pobrany z klawiatury,
- po utworzeniu każdego obiektu wypisz: liczbę instancji klasy, liczbę oczek liczbowo i słownie, nazwę pliku odpowiadającego wynikowi,
- komunikaty zrozumiałe dla użytkownika, znaczące nazwy zmiennych, czyste formatowanie.
Napisz tę klasę sam, z zegarem ustawionym na 45 minut — potem porównaj.
Pokaż rozwiązanieKlasa Kosc i program główny w C#
Plik Kosc.cs —
logika kości. Ta sama klasa posłuży potem testom jednostkowym.
using System;
namespace GraWKosci
{
public class Kosc
{
// Pole statyczne, wspolne dla wszystkich instancji klasy
public static int LiczbaInstancji = 0;
// Jeden generator dla calej klasy - dwa obiekty tworzone
// w tej samej milisekundzie nie wylosuja tej samej wartosci
private static readonly Random Generator = new Random();
// Indeks tablicy odpowiada liczbie oczek: NazwyPlikow[3] to kosc3.png
public string[] NazwyPlikow = {
"kosc0.png", "kosc1.png", "kosc2.png", "kosc3.png",
"kosc4.png", "kosc5.png", "kosc6.png"
};
public int LiczbaOczek;
public int IdentyfikatorPliku;
public bool CzyDostepna;
// Konstruktor jednoargumentowy
public Kosc(int wartosc)
{
if (wartosc < 1 || wartosc > 6)
{
wartosc = 0;
}
LiczbaOczek = wartosc;
IdentyfikatorPliku = wartosc;
CzyDostepna = true;
LiczbaInstancji++;
}
// Konstruktor bezargumentowy - losuje wartosc kosci
public Kosc()
{
int wylosowana = Generator.Next(1, 7);
LiczbaOczek = wylosowana;
IdentyfikatorPliku = wylosowana;
CzyDostepna = true;
LiczbaInstancji++;
}
// Rzut wykonuje sie tylko wtedy, gdy kosc jest dostepna
public void Rzut()
{
if (!CzyDostepna)
{
return;
}
int wylosowana = Generator.Next(1, 7);
LiczbaOczek = wylosowana;
IdentyfikatorPliku = wylosowana;
}
public void Zablokuj()
{
CzyDostepna = false;
}
public string OczkaSlownie()
{
switch (LiczbaOczek)
{
case 1: return "jeden";
case 2: return "dwa";
case 3: return "trzy";
case 4: return "cztery";
case 5: return "piec";
case 6: return "szesc";
default: return "zero";
}
}
public string NazwaPliku()
{
return NazwyPlikow[IdentyfikatorPliku];
}
}
}Plik Program.cs —
sprawdzenie działania klasy. Komunikaty pełnym zdaniem, bo arkusz żąda,
by informacja była zrozumiała dla użytkownika.
using System;
namespace GraWKosci
{
class Program
{
static void WyswietlInformacje(Kosc kosc, string opis)
{
Console.WriteLine();
Console.WriteLine("--- " + opis + " ---");
Console.WriteLine("Liczba utworzonych instancji klasy Kosc: "
+ Kosc.LiczbaInstancji);
Console.WriteLine("Liczba wyrzuconych oczek: "
+ kosc.LiczbaOczek + " (slownie: " + kosc.OczkaSlownie() + ")");
Console.WriteLine("Nazwa pliku z grafika kosci: "
+ kosc.NazwaPliku());
}
static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine("GRA W KOSCI - sprawdzenie dzialania klasy Kosc");
Console.WriteLine();
Console.Write("Podaj liczbe oczek pierwszej kosci (od 1 do 6): ");
string wprowadzonyTekst = Console.ReadLine();
int podanaWartosc;
if (!int.TryParse(wprowadzonyTekst, out podanaWartosc))
{
Console.WriteLine("To nie jest liczba - kosc otrzyma wartosc 0.");
podanaWartosc = 0;
}
Kosc pierwszaKosc = new Kosc(podanaWartosc);
WyswietlInformacje(pierwszaKosc,
"Kosc utworzona konstruktorem jednoargumentowym");
Kosc drugaKosc = new Kosc();
WyswietlInformacje(drugaKosc,
"Kosc utworzona konstruktorem bezargumentowym (wartosc losowa)");
Console.WriteLine();
Console.WriteLine("Nacisnij dowolny klawisz, aby zakonczyc...");
Console.ReadKey();
}
}
}- Statyczny generator losowy. Gdyby każdy obiekt tworzył własny
new Random(), dwie kości utworzone w tej samej milisekundzie dostałyby ten sam ziarno i tę samą wartość. Na zrzucie ekranu wyglądałoby to jak błąd losowania. - Liczba oczek i identyfikator pliku to dwa osobne pola, mimo że zawsze mają tę samą wartość. Arkusz wymienia je osobno, więc muszą istnieć osobno — połączenie ich w jedno pole to strata punktu.
Generator.Next(1, 7)— górna granica w C# jest wyłączna, więc to jest zakres 1–6. Klasyczny błąd o jeden:Next(1, 6)nigdy nie wyrzuci szóstki.- Wczesny powrót w metodzie rzutu. Arkusz pisze „realizuje rzut kością tylko, gdy kość jest dostępna" — to jest dokładnie ten warunek, który potem sprawdza drugi test jednostkowy.
- Tablica nazw jako pole publiczne, indeksowana wartością oczek. Dzięki temu nazwa pliku nie wymaga żadnego mapowania, wystarczy indeks.
Gdzie się traci punkty: pole zliczające instancje zrobione jako zwykłe (niestatyczne) — wtedy każdy obiekt ma własny licznik i program zawsze pokazuje 1. Drugi klasyk: metoda zwracająca słowo zwraca liczbę zamiast tekstu, albo obsługuje tylko wartości 1–6 i wywala się dla zera, które przecież sami wprowadzamy konstruktorem z argumentem spoza zakresu.
Pokaż rozwiązanieWariant w Pythonie — jeden konstruktor z argumentem None
Python nie ma przeciążania konstruktorów, dlatego arkusz wprost dopuszcza jeden konstruktor z domyślnym argumentem. Cała reszta wymagań zostaje bez zmian.
import random
class Kosc:
liczba_instancji = 0
def __init__(self, wartosc=None):
self.nazwy_plikow = [
"kosc0.png", "kosc1.png", "kosc2.png", "kosc3.png",
"kosc4.png", "kosc5.png", "kosc6.png"
]
if wartosc is None:
wartosc = random.randint(1, 6)
elif wartosc not in (1, 2, 3, 4, 5, 6):
wartosc = 0
self.liczba_oczek = wartosc
self.identyfikator_pliku = wartosc
self.czy_dostepna = True
Kosc.liczba_instancji += 1
def rzut(self):
if not self.czy_dostepna:
return
wylosowana = random.randint(1, 6)
self.liczba_oczek = wylosowana
self.identyfikator_pliku = wylosowana
def zablokuj(self):
self.czy_dostepna = False
def oczka_slownie(self):
slowa = ["zero", "jeden", "dwa", "trzy", "cztery", "piec", "szesc"]
return slowa[self.liczba_oczek]
def nazwa_pliku(self):
return self.nazwy_plikow[self.identyfikator_pliku]
if __name__ == "__main__":
tekst = input("Podaj liczbe oczek pierwszej kosci (od 1 do 6): ")
wartosc = int(tekst) if tekst.strip().isdigit() else 0
pierwsza = Kosc(wartosc)
print("Liczba instancji klasy Kosc:", Kosc.liczba_instancji)
print("Oczka:", pierwsza.liczba_oczek, "slownie:", pierwsza.oczka_slownie())
print("Plik graficzny:", pierwsza.nazwa_pliku())
druga = Kosc()
print("Liczba instancji klasy Kosc:", Kosc.liczba_instancji)
print("Oczka:", druga.liczba_oczek, "slownie:", druga.oczka_slownie())
print("Plik graficzny:", druga.nazwa_pliku())Uwaga na losowanie w Pythonie:
random.randint(1, 6)
ma obie granice włączne — inaczej niż w C#. Za to
random.randrange(1, 6)
już nie wylosuje szóstki. Sprawdź to w dokumentacji swojego języka, zanim
napiszesz test na zakres 1–6.
5
Rezultat 3: aplikacja mobilna
Gra w kości: pięć sześciennych kości, rzut przyciskiem, blokowanie kości kliknięciem w jej obraz. Interfejs zapisany w języku znaczników wspieranym przez środowisko (XML w Android Studio albo XAML w .NET MAUI / Xamarin). Wolno wykorzystać klasę Kosc z aplikacji konsolowej.
- pięć obrazów
kosc0.pngbez przezroczystości — wszystkie kości dostępne, - przycisk z napisem RZUT,
- pole tekstowe z wynikiem rzutu, początkowa wartość 0, zawiera sumę oczek pięciu kości.
- tło okna / rozkładu
#ED27C121, tło przycisku#ED275021, - rozmiar czcionki pola tekstowego 40,
- marginesy zewnętrzne obrazów i przycisku 10,
- wszystkie elementy wyśrodkowane, dowolny układ pozwalający rozmieścić je jak na obrazie z arkusza.
- Przycisk RZUT: dla dostępnych kości losowana jest liczba oczek, w kontrolkach obrazu pojawia się grafika odpowiadająca wynikowi, a w polu tekstowym suma oczek ze wszystkich kości.
- Kliknięcie w obraz kości: dostępna staje się niedostępna i jej obraz dostaje przezroczystość 50%; niedostępna wraca do dostępnej i pełnej widoczności.
- Próba kompilacji i emulacji, zrzuty wszystkich stanów aplikacji:
mobilna1.png,mobilna2.png, …
Najpierw sam widok — uruchom go w emulatorze, zanim dopiszesz choć jedną linijkę logiki.
Pokaż rozwiązanieWidok activity_main.xml — struktura ekranu
Zanim napiszesz układ, skopiuj siedem plików PNG do folderu
res/drawable.
Android wymaga nazw zasobów małymi literami bez rozszerzenia w kodzie —
kosc0.png
przechodzi bez zmiany nazwy.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="#ED27C121">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center">
<ImageView
android:id="@+id/obrazKosci1"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="10dp"
android:src="@drawable/kosc0"
android:contentDescription="kosc pierwsza" />
<ImageView
android:id="@+id/obrazKosci2"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="10dp"
android:src="@drawable/kosc0"
android:contentDescription="kosc druga" />
<ImageView
android:id="@+id/obrazKosci3"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="10dp"
android:src="@drawable/kosc0"
android:contentDescription="kosc trzecia" />
<ImageView
android:id="@+id/obrazKosci4"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="10dp"
android:src="@drawable/kosc0"
android:contentDescription="kosc czwarta" />
<ImageView
android:id="@+id/obrazKosci5"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="10dp"
android:src="@drawable/kosc0"
android:contentDescription="kosc piata" />
</LinearLayout>
<Button
android:id="@+id/przyciskRzut"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
android:backgroundTint="#ED275021"
android:text="RZUT" />
<TextView
android:id="@+id/poleWyniku"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
android:textSize="40sp"
android:text="0" />
</LinearLayout>Wartości #ED27C121
i #ED275021
mają osiem znaków, a nie sześć. To zapis ARGB: dwie
pierwsze cyfry to kanał alfa. Zarówno Android, jak i XAML rozumieją tę
formę wprost — wpisz ją znak w znak, nie „popraw" jej do sześciu znaków.
Po obcięciu alfy dostaniesz zupełnie inny kolor i tracisz punkt za wygląd.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="GraWKosci.MainPage"
BackgroundColor="#ED27C121">
<VerticalStackLayout HorizontalOptions="Center"
VerticalOptions="Center">
<HorizontalStackLayout HorizontalOptions="Center">
<Image x:Name="obrazKosci1" Source="kosc0.png"
WidthRequest="60" Margin="10">
<Image.GestureRecognizers>
<TapGestureRecognizer Tapped="PrzelaczDostepnosc"
CommandParameter="0" />
</Image.GestureRecognizers>
</Image>
<!-- pozostale cztery obrazy analogicznie -->
</HorizontalStackLayout>
<Button x:Name="przyciskRzut" Text="RZUT" Margin="10"
BackgroundColor="#ED275021" Clicked="WykonajRzut" />
<Label x:Name="poleWyniku" Text="0" FontSize="40"
HorizontalOptions="Center" Margin="10" />
</VerticalStackLayout>
</ContentPage>Gdzie się traci punkty:
marginesy ustawione jako padding
zamiast margin —
arkusz mówi wyraźnie „marginesy zewnętrzne 10". Drugi klasyk to
ustawienie tła przycisku przez android:background
w nowym Material Design, gdzie bywa nadpisywane motywem —
android:backgroundTint
działa niezawodnie. I trzeci: brak wyśrodkowania. „Wszystkie elementy
widoku są wyśrodkowane" to osobne wymaganie, nie ozdobnik.
Pokaż rozwiązanieMainActivity.kt — obsługa zdarzeń i cykl życia
Najpierw port klasy Kosc do Kotlina — plik
Kosc.kt.
Logika jest identyczna jak w wersji konsolowej, dochodzi tylko metoda
odblokowująca kość, bo mobilna gra przełącza dostępność w obie strony.
package com.example.grawkosci
import kotlin.random.Random
class Kosc {
companion object {
var liczbaInstancji = 0
}
val nazwyPlikow = arrayOf(
"kosc0.png", "kosc1.png", "kosc2.png", "kosc3.png",
"kosc4.png", "kosc5.png", "kosc6.png"
)
var liczbaOczek = 0
var identyfikatorPliku = 0
var czyDostepna = true
constructor(wartosc: Int) {
val poprawnaWartosc = if (wartosc in 1..6) wartosc else 0
liczbaOczek = poprawnaWartosc
identyfikatorPliku = poprawnaWartosc
czyDostepna = true
liczbaInstancji++
}
constructor() {
val wylosowana = Random.nextInt(1, 7)
liczbaOczek = wylosowana
identyfikatorPliku = wylosowana
czyDostepna = true
liczbaInstancji++
}
fun rzut() {
if (!czyDostepna) return
val wylosowana = Random.nextInt(1, 7)
liczbaOczek = wylosowana
identyfikatorPliku = wylosowana
}
fun zablokuj() {
czyDostepna = false
}
fun udostepnij() {
czyDostepna = true
}
fun oczkaSlownie(): String = when (liczbaOczek) {
1 -> "jeden"
2 -> "dwa"
3 -> "trzy"
4 -> "cztery"
5 -> "piec"
6 -> "szesc"
else -> "zero"
}
}Plik MainActivity.kt —
podpięcie kontrolek, dwa rodzaje zdarzeń i jedna metoda odświeżająca cały widok.
package com.example.grawkosci
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private val liczbaKosci = 5
// Piec kosci w stanie poczatkowym: wartosc 0, obraz kosc0.png
private val kosci = List(liczbaKosci) { Kosc(0) }
private lateinit var obrazyKosci: List<ImageView>
private lateinit var poleWyniku: TextView
private lateinit var przyciskRzut: Button
override fun onCreate(zapisanyStan: Bundle?) {
super.onCreate(zapisanyStan)
setContentView(R.layout.activity_main)
obrazyKosci = listOf(
findViewById(R.id.obrazKosci1),
findViewById(R.id.obrazKosci2),
findViewById(R.id.obrazKosci3),
findViewById(R.id.obrazKosci4),
findViewById(R.id.obrazKosci5)
)
poleWyniku = findViewById(R.id.poleWyniku)
przyciskRzut = findViewById(R.id.przyciskRzut)
// Zdarzenie 1: przycisk RZUT
przyciskRzut.setOnClickListener { wykonajRzut() }
// Zdarzenie 2: klikniecie w obraz kosci
obrazyKosci.forEachIndexed { indeks, obraz ->
obraz.setOnClickListener { przelaczDostepnosc(indeks) }
}
// Odtworzenie stanu po obrocie ekranu
if (zapisanyStan != null) {
val oczka = zapisanyStan.getIntArray(KLUCZ_OCZKA)
val dostepnosc = zapisanyStan.getBooleanArray(KLUCZ_DOSTEPNOSC)
if (oczka != null && dostepnosc != null) {
for (indeks in kosci.indices) {
kosci[indeks].liczbaOczek = oczka[indeks]
kosci[indeks].identyfikatorPliku = oczka[indeks]
kosci[indeks].czyDostepna = dostepnosc[indeks]
}
}
}
odswiezWidok()
}
private fun wykonajRzut() {
// Metoda rzut() sama pomija kosci niedostepne
kosci.forEach { kosc -> kosc.rzut() }
odswiezWidok()
}
private fun przelaczDostepnosc(indeks: Int) {
val kosc = kosci[indeks]
if (kosc.czyDostepna) {
kosc.zablokuj()
} else {
kosc.udostepnij()
}
odswiezWidok()
}
private fun odswiezWidok() {
var sumaOczek = 0
for (indeks in kosci.indices) {
val kosc = kosci[indeks]
val nazwaZasobu = "kosc" + kosc.identyfikatorPliku
val idZasobu = resources.getIdentifier(
nazwaZasobu, "drawable", packageName
)
obrazyKosci[indeks].setImageResource(idZasobu)
obrazyKosci[indeks].alpha = if (kosc.czyDostepna) 1.0f else 0.5f
sumaOczek += kosc.liczbaOczek
}
poleWyniku.text = sumaOczek.toString()
}
override fun onSaveInstanceState(stan: Bundle) {
super.onSaveInstanceState(stan)
stan.putIntArray(KLUCZ_OCZKA,
kosci.map { kosc -> kosc.liczbaOczek }.toIntArray())
stan.putBooleanArray(KLUCZ_DOSTEPNOSC,
kosci.map { kosc -> kosc.czyDostepna }.toBooleanArray())
}
companion object {
private const val KLUCZ_OCZKA = "oczka"
private const val KLUCZ_DOSTEPNOSC = "dostepnosc"
}
}- Jedna metoda odświeżająca widok. Zamiast rozsypywać podmianę obrazków po obu obsługach zdarzeń, wszystko idzie przez
odswiezWidok(). Zmiana wyglądu ma wtedy jedno miejsce, a suma oczek liczy się zawsze z aktualnego stanu wszystkich kości — także zablokowanych, co arkusz wprost nakazuje. - Nie filtrujemy kości przed rzutem. Warunek dostępności siedzi już w metodzie
rzut(). To ta sama logika, którą sprawdzi test jednostkowy — jeden warunek, dwa rezultaty egzaminacyjne. - Cykl życia. Obrót emulatora niszczy aktywność i uruchamia
onCreateod nowa. BezonSaveInstanceStatewszystkie kości wracają do zera w trakcie robienia zrzutów. Nie jest to wymagane wprost, ale kosztuje pięć linijek i ratuje przed nieprzyjemną niespodzianką.
mobilna1.png— stan początkowy: pięć razy kosc0.png, wynik 0, wszystkie kości pełne.mobilna2.png— po wciśnięciu RZUT: pięć różnych grafik i suma oczek w polu tekstowym.mobilna3.png— po kliknięciu w kilka kości: te są wyblakłe (50% przezroczystości), a kolejny rzut zmienia tylko pozostałe.
Gdzie się traci punkty:
suma oczek liczona tylko z kości dostępnych. Arkusz mówi „suma oczek
z wszystkich kości" i pokazuje to na przykładzie ze stanu 3 —
zablokowane kości nadal wliczają się do wyniku. Drugi błąd: ukrywanie
zablokowanej kości zamiast ustawienia przezroczystości 50%. Trzeci:
ImageView
bez ustawionego nasłuchiwania kliknięć — w Androidzie obraz domyślnie nie
jest klikalny, więc jeśli nie podpinasz listenera w kodzie, musisz dodać
android:clickable="true"
w widoku.
6
Rezultat 4: testy jednostkowe
Ta część odróżnia styczeń 2026 od większości arkuszy INF.04. Testy są osobnym, pełnoprawnym rezultatem, a wymagania są tak wąskie, że da się je zrobić w kwadrans — jeśli wiesz, jak w twoim środowisku założyć projekt testowy. Dlatego poniżej jest nie tylko kod, ale i procedura klikania.
- Testy jednostkowe aplikacji konsolowej, konkretnie metody realizującej rzut kością. Nie testujesz konstruktorów, nie testujesz aplikacji mobilnej.
- Biblioteka testowa dostępna na stanowisku dla języka, w którym napisałeś klasę Kosc.
- Dwa przypadki testowe: (a) czy wyrzucona wartość mieści się w zakresie od 1 do 6, (b) czy wartość na kości pozostaje bez zmian, gdy kość nie jest dostępna.
- Każdy przypadek w osobnej metodzie.
- Nazwy metod mają wyrażać cel testu i być zgodne z konwencją nazewnictwa danego języka.
- Uruchomienie wszystkich metod i zrzuty
test1.png,test2.pngz widocznymi wynikami działania metod testujących. - W folderze
testy: archiwum projektu, pliki z metodami testującymi i zrzuty.
Spróbuj sam: dwie metody, po jednej asercji na przypadek. Zegar na 20 minut.
Test jednostkowy zawsze ma tę samą budowę — trzy kroki, znane jako arrange – act – assert (przygotuj – wykonaj – sprawdź). Jeśli w twojej metodzie brakuje któregoś z nich, to nie jest test:
| Krok | Co robisz | W tym zadaniu |
|---|---|---|
| Przygotuj | tworzysz obiekt w znanym stanie | nowa kość o konkretnej wartości, np. 4 |
| Wykonaj | wywołujesz testowaną metodę i nic więcej | wywołanie metody rzutu |
| Sprawdź | asercja — twierdzenie o wyniku | wartość w zakresie 1–6 albo równa poprzedniej |
Dochodzi czwarty element, o którym arkusz pisze wprost: nazwa
metody wyrażająca cel testu. W C# konwencja to PascalCase, w Javie
i Kotlinie camelCase, w Pythonie snake_case z przedrostkiem
test_.
Metoda nazwana Test1
spełnia wymóg techniczny i łamie wymóg z arkusza.
Pokaż rozwiązanieProjekt testowy MSTest w C# — dwa przypadki z asercjami
- Prawy przycisk na rozwiązaniu → Dodaj → Nowy projekt → MSTest Test Project. Nazwij go
TestyKosci. - W projekcie testowym: Zależności → Dodaj odwołanie do projektu i zaznacz projekt konsolowy. Bez tego kroku klasa Kosc jest dla testów niewidoczna — to najczęstsza przyczyna „nie da się tego zrobić" na egzaminie.
- Klasa Kosc musi być
public. Domyślnie klasa w C# jest wewnętrzna i test jej nie zobaczy. - Uruchomienie: Test → Eksplorator testów → Uruchom wszystkie testy.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using GraWKosci;
namespace TestyKosci
{
[TestClass]
public class KoscTesty
{
// Rzut jest losowy, wiec jedno wywolanie nie dowodzi niczego.
// Powtarzamy go wiele razy - test wychwyci blad typu Next(1, 6)
// albo Next(0, 6), ktory czasem zwroci wartosc spoza zakresu.
private const int LiczbaPowtorzen = 1000;
[TestMethod]
public void RzutZwracaWartoscZZakresuOdJedenDoSzesc()
{
// przygotuj
Kosc kosc = new Kosc(1);
for (int probka = 0; probka < LiczbaPowtorzen; probka++)
{
// wykonaj
kosc.Rzut();
// sprawdz
Assert.IsTrue(kosc.LiczbaOczek >= 1 && kosc.LiczbaOczek <= 6,
"Kosc wyrzucila wartosc spoza zakresu 1-6: "
+ kosc.LiczbaOczek);
}
}
[TestMethod]
public void RzutNieZmieniaWartosciGdyKoscJestNiedostepna()
{
// przygotuj
Kosc kosc = new Kosc(4);
kosc.Zablokuj();
int wartoscPrzedRzutem = kosc.LiczbaOczek;
int identyfikatorPrzedRzutem = kosc.IdentyfikatorPliku;
// wykonaj
for (int probka = 0; probka < LiczbaPowtorzen; probka++)
{
kosc.Rzut();
}
// sprawdz
Assert.AreEqual(wartoscPrzedRzutem, kosc.LiczbaOczek,
"Zablokowana kosc zmienila liczbe oczek");
Assert.AreEqual(identyfikatorPrzedRzutem, kosc.IdentyfikatorPliku,
"Zablokowana kosc zmienila identyfikator pliku");
}
}
}Metoda rzutu jest niedeterministyczna. Pojedyncze wywołanie z wynikiem
w zakresie niczego nie dowodzi — błędny kod
Next(0, 7)
przechodzi taki test w sześciu przypadkach na siedem. Tysiąc powtórzeń
sprawia, że prawdopodobieństwo przeoczenia błędu jest znikome, a test
nadal trwa milisekundy. To samo dotyczy drugiego przypadku: chcemy mieć
pewność, że zablokowana kość nie zmieniła wartości ani razu.
| Asercja | Kiedy jej używasz |
|---|---|
Assert.IsTrue(warunek) | sprawdzasz warunek, np. przynależność do zakresu |
Assert.AreEqual(oczekiwana, rzeczywista) | porównujesz z konkretną wartością — kolejność argumentów ma znaczenie w komunikacie błędu |
Assert.IsFalse(warunek) | np. sprawdzenie, że kość po zablokowaniu nie jest dostępna |
| drugi argument tekstowy | komunikat wyświetlany po niepowodzeniu — na zrzucie ekranu pokazuje, co dokładnie sprawdzałeś |
Gdzie się traci punkty:
oba przypadki wciśnięte w jedną metodę testową. Arkusz mówi wprost: „każdy
przypadek testowy powinien być sprawdzany oddzielną metodą". Druga
pułapka: test bez ani jednej asercji — metoda, która tylko wypisuje wynik
na konsolę, zawsze przechodzi i niczego nie testuje. Trzecia: zrzut ekranu
z zamkniętym Eksploratorem testów. Na
test1.png
muszą być widoczne nazwy metod i ich wyniki — dwie zielone
ikony obok nazw, które same z siebie tłumaczą, co testowały.
Pokaż rozwiązanieTe same testy w JUnit 5 (Java / Kotlin) i pytest
Jeśli klasę Kosc napisałeś w Javie albo w Kotlinie, biblioteką dostępną na
stanowisku będzie JUnit. Plik trafia do katalogu
src/test/java.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class KoscTest {
private static final int LICZBA_POWTORZEN = 1000;
@Test
void rzutZwracaWartoscZZakresuOdJedenDoSzesc() {
Kosc kosc = new Kosc(1);
for (int probka = 0; probka < LICZBA_POWTORZEN; probka++) {
kosc.rzut();
assertTrue(kosc.liczbaOczek >= 1 && kosc.liczbaOczek <= 6,
"Wartosc spoza zakresu 1-6: " + kosc.liczbaOczek);
}
}
@Test
void rzutNieZmieniaWartosciGdyKoscJestNiedostepna() {
Kosc kosc = new Kosc(4);
kosc.zablokuj();
for (int probka = 0; probka < LICZBA_POWTORZEN; probka++) {
kosc.rzut();
}
assertEquals(4, kosc.liczbaOczek,
"Zablokowana kosc zmienila liczbe oczek");
assertEquals(4, kosc.identyfikatorPliku,
"Zablokowana kosc zmienila identyfikator pliku");
}
}Wariant dla Pythona — biblioteka pytest
albo wbudowany unittest.
Poniżej pytest, plik test_kosc.py,
uruchamiany poleceniem pytest -v
(przełącznik -v wypisuje nazwy metod, co jest potrzebne na zrzucie ekranu).
from kosc import Kosc
LICZBA_POWTORZEN = 1000
def test_rzut_zwraca_wartosc_z_zakresu_od_jeden_do_szesc():
kosc = Kosc(1)
for _ in range(LICZBA_POWTORZEN):
kosc.rzut()
assert 1 <= kosc.liczba_oczek <= 6, (
"Wartosc spoza zakresu 1-6: " + str(kosc.liczba_oczek)
)
def test_rzut_nie_zmienia_wartosci_gdy_kosc_jest_niedostepna():
kosc = Kosc(4)
kosc.zablokuj()
for _ in range(LICZBA_POWTORZEN):
kosc.rzut()
assert kosc.liczba_oczek == 4
assert kosc.identyfikator_pliku == 4| Język | Konwencja | Przykład zgodny z arkuszem |
|---|---|---|
| C# | PascalCase | RzutZwracaWartoscZZakresuOdJedenDoSzesc |
| Java, Kotlin | camelCase | rzutNieZmieniaWartosciGdyKoscJestNiedostepna |
| Python | snake_case z przedrostkiem test_ | test_rzut_zwraca_wartosc_z_zakresu_od_jeden_do_szesc |
Jeśli zabrakło ci czasu:
napisz jedną metodę testową z jedną asercją i uruchom ją. Jeden działający,
poprawnie nazwany test z widocznym wynikiem na zrzucie to zawsze więcej niż
pusty folder testy.
Ten rezultat jest oceniany po tym, co realnie znajdzie się na płycie.
7
Checklista przed oddaniem
- Folder nazwany numerem zdającego, w nim podfoldery konsolowa, mobilna, testy — dokładnie te nazwy, małymi literami.
- Plik
egzaminzawiera system, środowiska, emulator i języki — cztery pozycje, żadnej nie brakuje. - W każdym podfolderze jest archiwum całego projektu, a nie tylko plików źródłowych.
- Zrzuty nazwane wzorem
konsola1.png,mobilna1.png,test1.png, każdy obejmuje cały ekran z paskiem zadań i środowiskiem.
- Klasa nazywa się dokładnie Kosc, pole licznika instancji jest statyczne i publiczne.
- Są dwa konstruktory (albo jeden z domyślnym argumentem w Pythonie), a program tworzy obiekty każdym z nich.
- Wartość spoza zakresu 1–6 daje 0 i nazwę pliku
kosc0.png— przetestowane i udokumentowane zrzutem. - Program wypisuje wszystkie trzy informacje po utworzeniu każdego obiektu, pełnymi zdaniami.
- Metoda słowna zwraca tekst dla wszystkich wartości od 0 do 6.
- Pięć obrazów, przycisk RZUT, pole wyniku o rozmiarze czcionki 40 — wszystko wyśrodkowane.
- Kolory wpisane ośmioznakowo:
#ED27C121tło,#ED275021przycisk. - Marginesy zewnętrzne 10 przy obrazach i przycisku.
- Stan początkowy: pięć razy kosc0.png bez przezroczystości, wynik 0.
- Rzut zmienia tylko kości dostępne, ale suma liczona jest ze wszystkich pięciu.
- Kliknięcie kości przełącza dostępność w obie strony, przezroczystość 50% i z powrotem.
- Trzy zrzuty pokazujące trzy różne stany aplikacji w emulatorze.
- Dwie osobne metody testowe, po jednej na przypadek testowy.
- Nazwy metod mówią, co testują, i są zgodne z konwencją języka.
- Każda metoda ma przynajmniej jedną asercję.
- Testy zostały uruchomione i przeszły, a zrzut pokazuje nazwy metod wraz z wynikami.
- Nagrana, sprawdzona po nagraniu (otwórz kilka plików z płyty, nie z dysku).
- Opisana numerem zdającego i zapakowana w pudełku razem z arkuszem.
- Oświadczenie na ostatniej stronie arkusza wypełnione i podpisane.
Ostatnie pięć minut: otwórz folder z numerem zdającego i przejdź go wzrokiem po strukturze z sekcji 1. Egzaminator nie ocenia tego, co masz w projekcie w Visual Studio — ocenia to, co znalazł na płycie. Plik, którego tam nie ma, nie istnieje.
Przeczytanie rozwiązania to nie to samo, co napisanie go w 180 minut na egzaminacyjnym sprzęcie, bez internetu i z emulatorem, który startuje pięć minut. Otwórz oryginalny arkusz, ustaw stoper i zrób zadanie od zera — dopiero wynik tej próby pokaże, gdzie naprawdę stoisz.