Przejdź do treści
INF.04Styczeń 2026180 min

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ń.

Pobierz PDF

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.

Jak korzystać z tego rozbioru

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.

1

Metryka arkusza i klucz oceniania

Dane arkusza
  • 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.7z z pulpitu konta Egzamin, hasło podane w arkuszu
Oceniane rezultaty

Arkusz wymienia cztery rezultaty. To dosłownie lista, po której idzie egzaminator:

  1. implementacja, kompilacja, uruchomienie programów
  2. aplikacja konsolowa
  3. aplikacja mobilna
  4. testy aplikacji

Pierwszy rezultat to nie kod, tylko dowody: zrzuty ekranu, archiwa projektów i struktura folderów. Zdobywa się go stolarką, nie programowaniem.

Co musi znaleźć się w folderze

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 testow

Nazwy 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.

EtapCzasUwagi
Foldery, rozpakowanie zad1.7z, plik egzamin10 mindarmowe punkty z rezultatu 1, zrób od razu
Klasa Kosc i program główny w konsoli45 minfundament dla dwóch pozostałych rezultatów
Zrzuty konsoli i archiwum konsola.zip10 minrób od razu po uruchomieniu, nie na końcu
Testy jednostkowe: dwa przypadki + uruchomienie25 minnajtańszy punktowo etap w całym arkuszu
Mobilna: widok XML/XAML, kolory, marginesy30 mingrafiki wrzuć do zasobów zanim zaczniesz układ
Mobilna: logika, zdarzenia, emulator35 minemulator odpal wcześniej, startuje kilka minut
Zrzuty mobilne i archiwum mobilna.zip10 mintrzy stany aplikacji, całe ekrany
Nagranie płyty i sprawdzenie nagrania15 minrezerwa, 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

Czego wymaga arkusz
  • 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.7z zabezpieczonego hasłem.
  • Zrzuty ekranu dokumentujące uruchomienie każdej części: konsola1.png, mobilna1.png, test1.png i 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 egzamin w 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
Pierwsze dziesięć minut, po kolei
  1. Na pulpicie utwórz folder o nazwie równej twojemu numerowi PESEL, a w nim trzy puste podfoldery.
  2. Rozpakuj zad1.7z hasłem z arkusza. Sprawdź, czy widzisz siedem plików od kosc0.png do kosc6.png.
  3. Uruchom emulator mobilny — niech się wygrzewa w tle przez cały czas pracy nad konsolą.
  4. 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)
Ile zrzutów zrobić z konsoli

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

Czego wymaga arkusz

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.

Pola
  • ogólnodostępne, statyczne pole z liczbą utworzonych instancji klasy,
  • kolekcja napisowa z nazwami plików kosc0.pngkosc6.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.
Konstruktory
  • 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.
Metody (wszystkie publiczne i bezparametrowe)
  • 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".
Program główny
  • 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();
      }
  }
}
Dlaczego akurat tak
  • 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

Czego wymaga arkusz

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.

Stan początkowy
  • pięć obrazów kosc0.png bez 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.
Widok
  • 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.
Działanie
  • 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>
Jak czytać kolory z arkusza

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.

Wariant XAML dla .NET MAUI
<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"
  }
}
Trzy decyzje, które warto rozumieć
  • 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 onCreate od nowa. Bez onSaveInstanceState wszystkie kości wracają do zera w trakcie robienia zrzutów. Nie jest to wymagane wprost, ale kosztuje pięć linijek i ratuje przed nieprzyjemną niespodzianką.
Trzy zrzuty, które trzeba zrobić
  • 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.

Czego wymaga arkusz
  • 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.png z 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.

Na co patrzy egzaminator w teście jednostkowym

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:

KrokCo robiszW tym zadaniu
Przygotujtworzysz obiekt w znanym stanienowa kość o konkretnej wartości, np. 4
Wykonajwywołujesz testowaną metodę i nic więcejwywołanie metody rzutu
Sprawdźasercja — twierdzenie o wynikuwartość 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
Założenie projektu testowego (Visual Studio)
  1. Prawy przycisk na rozwiązaniu → DodajNowy projektMSTest Test Project. Nazwij go TestyKosci.
  2. W projekcie testowym: ZależnościDodaj 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.
  3. Klasa Kosc musi być public. Domyślnie klasa w C# jest wewnętrzna i test jej nie zobaczy.
  4. Uruchomienie: TestEksplorator testówUruchom 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");
      }
  }
}
Dlaczego pętla, a nie jedno wywołanie

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.

Który assert do czego
AsercjaKiedy 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 tekstowykomunikat 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
Konwencje nazw w trzech światach
JęzykKonwencjaPrzykład zgodny z arkuszem
C#PascalCaseRzutZwracaWartoscZZakresuOdJedenDoSzesc
Java, KotlincamelCaserzutNieZmieniaWartosciGdyKoscJestNiedostepna
Pythonsnake_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

Struktura i dokumentacja
  • Folder nazwany numerem zdającego, w nim podfoldery konsolowa, mobilna, testy — dokładnie te nazwy, małymi literami.
  • Plik egzamin zawiera 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.
Aplikacja konsolowa
  • Klasa nazywa się dokładnie Kosc, pole licznika instancji jest statyczne i publiczne.
  • 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.
Aplikacja mobilna
  • Pięć obrazów, przycisk RZUT, pole wyniku o rozmiarze czcionki 40 — wszystko wyśrodkowane.
  • Kolory wpisane ośmioznakowo: #ED27C121 tło, #ED275021 przycisk.
  • 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.
Testy
  • 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.
Płyta
  • 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.

Przećwicz to na czas

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.