Dataczwartek, 13 sierpnia 2026 Czas17:55:33
← Aplikacje webowe

Lekcja 21. Projekt 2: Kolorowy zgadywacz — gra w Angularze z zapisem wyników

Średni

Po co się tego uczymy?

Drugi projekt jest lżejszy niż kalkulator BMI — skupia się głównie na logice front-endowej (Angular) i dynamicznym stylowaniu elementów, z prostym zapisem najlepszego wyniku na backendzie. To dobre ćwiczenie przed dużym, trzecim projektem (system zamówień).

Teoria

Zasady gry: aplikacja pokazuje losowo wybrany kolor SŁOWNIE (np. "Kliknij: NIEBIESKI"), a użytkownik musi kliknąć odpowiedni przycisk spośród kilku kolorowych kwadratów. Poprawna odpowiedź zwiększa wynik, błędna — nie. Po każdej odpowiedzi losowane jest kolejne pytanie. Po zakończeniu rundy (np. 10 pytań) wynik jest wysyłany do backendu i zapisywany jako próba do rankingu najlepszych wyników.

Kluczowe mechanizmy Angulara ćwiczone w tym projekcie:

  • Dynamiczne stylowanie przez [style.background-color]="kolor.kodHex" — wiązanie właściwości bezpośrednio do stylu CSS elementu, sterowane danymi z TypeScript (rozszerzenie [property binding] z lekcji 9);
  • Generowanie losowych danychMath.random() (wbudowana funkcja JavaScript/TypeScript) do wyboru losowego koloru i przetasowania kolejności przycisków;
  • @for z track (lekcja 9) do wyrenderowania dynamicznej liczby przycisków kolorów z tablicy;
  • Stan gry w polach klasy (aktualne pytanie, wynik, licznik pytań, flaga "gra zakończona") — ten sam wzorzec "jedno źródło prawdy w komponencie", co w Blazor czy w PageModel z Razor Pages.

Zapis wyniku na backendzie. Po zakończeniu rundy Angular wysyła POST z liczbą punktów do prostego kontrolera WynikiController, a osobna strona/zakładka "Ranking" (GET /api/wyniki?limit=10) pokazuje 10 najlepszych wyników posortowanych malejąco — dokładnie ten sam wzorzec CRUD/API, którego uczyłeś się od lekcji 11.

Schemat

Start rundy: wynik = 0, pytanie = 1, losujNastepnePytanie()

losujNastepnePytanie():
  szukanyKolor = kolory[Math.floor(Math.random() * kolory.length)]
  przetasowaneKolory = [...kolory].sort(() => Math.random() - 0.5)

Widok:
  <p>Kliknij: {{ szukanyKolor.nazwa }}</p>
  @for (k of przetasowaneKolory; track k.nazwa) {
    <button [style.background-color]="k.kodHex"
            (click)="sprawdzOdpowiedz(k)"></button>
  }

sprawdzOdpowiedz(wybrany):
  jeśli wybrany === szukanyKolor → wynik++
  pytanie++
  jeśli pytanie > 10 → zakonczRunde()
  w przeciwnym razie → losujNastepnePytanie()

zakonczRunde():
  this.http.post("/api/wyniki", { punkty: wynik }).subscribe(...)
  pokazEkranKoncowy = true

Przykład z życia

Ten typ prostej gry quizowej z natychmiastową informacją zwrotną i rankingiem wyników to wzorzec spotykany w niezliczonych aplikacjach edukacyjnych i rozrywkowych — łącznie z systemem testów na samej stronie bitedu.pl, gdzie też losuje się pytania i zlicza poprawne odpowiedzi.

zgadywacz.component.html

<!-- zgadywacz.component.html -->
<div class="container text-center mt-5">

  @if (!pokazEkranKoncowy) {
    <h2>Kliknij kolor: <strong>{{ szukanyKolor.nazwa }}</strong></h2>
    <p>Pytanie {{ numerPytania }} / 10 · Wynik: {{ wynik }}</p>

    <div class="d-flex justify-content-center gap-3 my-4">
      @for (kolor of przetasowaneKolory; track kolor.nazwa) {
        <button class="rounded"
                style="width:70px; height:70px; border:2px solid #333;"
                [style.background-color]="kolor.kodHex"
                (click)="sprawdzOdpowiedz(kolor)">
        </button>
      }
    </div>
  } @else {
    <h2>Koniec gry!</h2>
    <p>Twój wynik: {{ wynik }} / 10</p>
    <button class="btn btn-primary" (click)="zagrajZnowu()">Zagraj ponownie</button>
  }

</div>

zgadywacz.component.ts

// zgadywacz.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

interface Kolor {
  nazwa: string;
  kodHex: string;
}

@Component({
  selector: 'app-zgadywacz',
  standalone: true,
  templateUrl: './zgadywacz.component.html'
})
export class ZgadywaczComponent implements OnInit {
  kolory: Kolor[] = [
    { nazwa: 'CZERWONY', kodHex: '#e74c3c' },
    { nazwa: 'NIEBIESKI', kodHex: '#3498db' },
    { nazwa: 'ZIELONY', kodHex: '#2ecc71' },
    { nazwa: 'ŻÓŁTY', kodHex: '#f1c40f' }
  ];

  szukanyKolor!: Kolor;
  przetasowaneKolory: Kolor[] = [];
  wynik = 0;
  numerPytania = 1;
  pokazEkranKoncowy = false;

  constructor(private http: HttpClient) {}

  ngOnInit(): void {
    this.losujNastepnePytanie();
  }

  losujNastepnePytanie(): void {
    this.szukanyKolor = this.kolory[Math.floor(Math.random() * this.kolory.length)];
    this.przetasowaneKolory = [...this.kolory].sort(() => Math.random() - 0.5);
  }

  sprawdzOdpowiedz(wybrany: Kolor): void {
    if (wybrany.nazwa === this.szukanyKolor.nazwa) {
      this.wynik++;
    }

    if (this.numerPytania >= 10) {
      this.zakonczRunde();
      return;
    }

    this.numerPytania++;
    this.losujNastepnePytanie();
  }

  zakonczRunde(): void {
    this.pokazEkranKoncowy = true;
    this.http.post('https://localhost:7042/api/wyniki', { punkty: this.wynik })
      .subscribe({ error: (e) => console.error('Nie udało się zapisać wyniku', e) });
  }

  zagrajZnowu(): void {
    this.wynik = 0;
    this.numerPytania = 1;
    this.pokazEkranKoncowy = false;
    this.losujNastepnePytanie();
  }
}

Komentarz i wyjaśnienie kodu

[...this.kolory].sort(() => Math.random() - 0.5) to popularny (choć nie idealnie matematycznie równomierny) sposób "przetasowania" tablicy w JavaScript/TypeScript — trzy kropki (..., operator rozwinięcia/spread) tworzą KOPIĘ tablicy kolory, żeby przetasowanie NIE zmieniało oryginalnej listy (bo za każdym razem chcemy tasować od nowa, z tego samego zestawu kolorów).

Math.floor(Math.random() * this.kolory.length) to standardowy wzorzec losowania INDEKSU tablicy: Math.random() zwraca liczbę z przedziału [0, 1), mnożenie przez długość tablicy i zaokrąglenie w dół (Math.floor) daje poprawny, losowy indeks od 0 do (długość-1).

W zakonczRunde(), subskrypcja .subscribe({ error: ... }) obsługuje TYLKO błąd, bez next — to celowe: nie potrzebujemy żadnej reakcji na UDANY zapis (ekran końcowy już się pokazał), ale WARTO wiedzieć, gdyby zapis się nie powiódł (np. backend niedostępny).

Ćwiczenie samodzielne

Zbuduj komponent gry jak w przykładzie (na razie bez zapisu do backendu — sam frontend). Zagraj kilka rund, sprawdzając poprawność liczenia wyniku i losowania kolejnych pytań.

Zadania do pracy własnej

  1. Dodaj krótką animację/efekt wizualny (np. zmianę koloru tła CAŁEJ strony na sekundę) po udzieleniu POPRAWNEJ odpowiedzi.

  2. Dodaj kontroler WynikiController (backend) z akcją [HttpPost] zapisującą wynik przez EF Core, i akcją [HttpGet] zwracającą 10 najlepszych wyników posortowanych malejąco — zbuduj osobny komponent "Ranking" wyświetlający tę listę.

  3. Rozbuduj grę o rosnący poziom trudności: co 3 poprawne odpowiedzi z rzędu dodawaj JEDEN nowy kolor do puli (start: 4 kolory, potem 5, 6...) — zaimplementuj też limit czasu na odpowiedź (np. 3 sekundy na pytanie, z widocznym paskiem odliczającym), po przekroczeniu którego pytanie liczy się jako błędne.

Typowe błędy

Tasowanie/modyfikowanie ORYGINALNEJ tablicy kolory zamiast jej kopii — bez operatora rozwinięcia (...) każde kolejne losowanie zniekształcałoby oryginalny zestaw danych.

Zapominanie o wywołaniu losujNastepnePytanie() w ngOnInit() — bez tego gra wystartuje z niezdefiniowanym pytaniem (błąd w konsoli przy próbie wyświetlenia szukanyKolor.nazwa, bo szukanyKolor nie miałoby jeszcze przypisanej wartości).

Porównywanie obiektów koloru przez referencję (=== na całych obiektach) zamiast po nazwie — po przetasowaniu tablicy klikany obiekt to inna instancja niż szukanyKolor, nawet jeśli reprezentuje "ten sam" kolor; porównanie musi odbywać się po WŁAŚCIWOŚCI (np. nazwa), nie po tożsamości obiektu.

Nawiązanie do egzaminu zawodowego

To drugi z ośmiu projektów realizujących INF.04.7 — skupiony głównie na logice i interaktywności front-endowej Angulara (dynamiczne stylowanie, generowanie losowych danych, zarządzanie stanem gry), z lekkim, ale kompletnym zapisem wyniku na backendzie. Kolejny, trzeci projekt (lekcja 22) to sklep internetowy — pierwszy z projektów wprost odwzorowujący nazwaną w podstawie programowej funkcjonalność "e-sklepu".