← Lekcje
JavaScript · lekcja 30

kto wygrał - sprawdzamy planszę

Plan tematu (5 godzin):

  1. Godzina 1: zdarzenie, addEventListener, obiekt zdarzenia
  2. Godzina 2: nie tylko klik - inne zdarzenia
  3. Godzina 3: budujemy planszę 3×3
  4. Godzina 4: kliknięcie pola zapisuje ruch
  5. Godzina 5 (dziś): kto wygrał - sprawdzamy planszę
Rozgrzewka · 3 minuty

Rozgrzewka: kliknięcie zapisuje ruch

  1. Co sprawdza warunek board[index] !== "" w funkcji handleMove i co się dzieje, gdy jest prawdziwy?
  2. Skąd handleMove wie, które dokładnie pole tablicy board zmienić po kliknięciu?
  3. Jakim zapisem currentPlayer przełącza się między "X" i "O" po udanym ruchu?
Część 1 · Osiem układów zwycięskich

Osiem trójek pól, które dają zwycięstwo

Kółko i krzyżyk kończy się wygraną, gdy trzy takie same znaki (XXX albo OOO) staną w jednej linii: w rzędzie, w kolumnie albo na przekątnej. Na planszy 3×3 (indeksy 0-8 z lekcji 28) jest dokładnie osiem takich linii. Każdą linię zapisujecie jako trójkę indeksów tablicy board, a wszystkie osiem trójek razem - jako tablicę tablic.

const winningLines = [
  [0, 1, 2], [3, 4, 5], [6, 7, 8],
  [0, 3, 6], [1, 4, 7], [2, 5, 8],
  [0, 4, 8], [2, 4, 6]
];
0 1 2 3 4 5 6 7 8 3 rzędy [0,1,2] [3,4,5] [6,7,8] 3 kolumny [0,3,6] [1,4,7] [2,5,8] 2 przekątne [0,4,8] [2,4,6] winningLines = [[0,1,2],[3,4,5],[6,7,8], [0,3,6],[1,4,7],[2,5,8], [0,4,8],[2,4,6]]
Część 2 · Pętla sprawdza każdą trójkę

Pętla for: po kolei, aż do trafienia

for (let i = 0; i < winningLines.length; i++) {
  const a = winningLines[i][0];
  const b = winningLines[i][1];
  const c = winningLines[i][2];
  if (board[a] === board[b] && board[b] === board[c] && board[a] !== "") {
    return board[a];
  }
}

Pętla przechodzi po kolei przez wszystkie osiem trójek z winningLines (i < winningLines.length, tak jak w lekcji 13 - długość tablicy, nie liczba 8 wpisana na sztywno). Dla trójki [a, b, c] sprawdza, czy wszystkie trzy pola mają ten sam znak i czy ten znak nie jest pustym tekstem "" (bo trzy puste pola też są sobie równe). Gdy trafi, return od razu kończy funkcję - kolejne trójki nie są już sprawdzane.

kolejność sprawdzania: i = 0 → 7 0 1 2 3 4 5 7 6 0,1,2 3,4,5 6,7,8 0,3,6 1,4,7 2,5,8 2,4,6 0,4,8 trafienie board[0] === board[4] && board[4] === board[8] && board[0] !== "" return board[0] X X X
pętla for po tablicy: lekcja 43 (43_js_tablice_3_slajdy.html)
Część 3 · Remis - includes()

A gdy nikt nie wygrał?

Gdy pętla sprawdzi wszystkie osiem trójek i żadna nie pasuje, gra jeszcze się nie skończyła zwycięstwem. Mogła się jednak skończyć remisem: remis jest wtedy, gdy w tablicy board nie ma już ani jednego pustego pola. Metoda includes("") sprawdza, czy w tablicy JEST jeszcze pusty tekst - znak ! przed nią odwraca odpowiedź na „nie ma już pustych pól”.

if (!board.includes("")) {
  return "remis";
}
return null; // gra jeszcze trwa

Gdy gra jeszcze trwa (są puste pola i nikt nie wygrał), funkcja zwraca null - handleMove wie wtedy, że ma przełączyć gracza i grać dalej.

MDN, Web/JavaScript/Reference/Global_Objects/Array/includes: „The includes() method of Array instances determines whether an array includes a certain value among its entries, returning true or false as appropriate.” - „Metoda includes() instancji Array sprawdza, czy tablica zawiera podaną wartość wśród swoich elementów, zwracając true lub false.” (tłum. własne)
Część 4 · Nowa gra

resetGame(): plansza wraca do zera

function resetGame() {
  board = Array(9).fill("");
  currentPlayer = "X";
  for (let i = 0; i < cells.length; i++) {
    cells[i].textContent = "";
  }
  status.textContent = "";
}

Przycisk id="newGame" dostaje addEventListener("click", resetGame). Kliknięcie zamienia board z powrotem na dziewięć pustych pól (ten sam zapis Array(9).fill("") co na starcie gry, lekcja 28), ustawia currentPlayer z powrotem na "X", czyści tekst na każdym przycisku (textContent, lekcja 22) i czyści komunikat w #status.

X X X O O Wygrywa X! klik #newGame resetGame() (puste)
Array(9).fill(""): lekcja 68; textContent: lekcja 52 (MDN, Web/API/Node/textContent, zacytowane w 54_js_dom_5_slajdy.html)
Egzamin EE.09

Tak pytają na egzaminie o pętlę po tablicy

Ten temat to projekt własny - bank CKE nie ma pytania o sprawdzanie planszy do gry. Ale funkcja checkWinner() to dokładnie ten sam wzorzec: pętla for z warunkiem sprawdzanym przy każdym przejściu przez tablicę. Tego wzorca dotyczy pytanie z banku:

var result = 0;
for (i = 0; i < 100; i++)
  if (numbers[i] % 2 == 0) result += numbers[i];

Po wykonaniu przedstawionego kodu JavaScript, działającego na wcześniej zainicjalizowanej tablicy numbers, w zmiennej result jest przechowywana suma

  1. dodatnich elementów tablicy.
  2. wszystkich elementów tablicy.
  3. parzystych elementów tablicy.
  4. nieparzystych elementów tablicy.
CKE, EE.09, czerwiec 2022, zadanie 7
Ćwiczenie przy komputerze · 22 minuty

Ćwiczenie: kto wygrał, przycisk „Nowa gra”

  1. Otwórz tic-tac-toe.html i tic-tac-toe.js z poprzednich dwóch godzin (plansza, tablica board, currentPlayer, funkcja handleMove(event)).
  2. W tic-tac-toe.html dodaj pod planszą <p id="status"></p> i przycisk <button type="button" id="newGame">Nowa gra</button>.
  3. W tic-tac-toe.js zapisz const winningLines z ośmioma trójkami indeksów (3 rzędy, 3 kolumny, 2 przekątne).
  4. Napisz checkWinner(): pętlą for sprawdź każdą trójkę - gdy trzy pola mają ten sam, niepusty znak, zwróć go; gdy żadna nie pasuje i board.includes("") jest fałszywe, zwróć "remis", w przeciwnym razie null.
  5. W handleMove(event) wywołaj checkWinner() zaraz po wpisaniu znaku: gdy wynik to "remis" albo znak gracza, wpisz komunikat do #status i zakończ funkcję (return) - dopiero gdy nikt nie wygrał, przełącz gracza jak w lekcji 29.
  6. Napisz resetGame(): wyczyść board, ustaw currentPlayer na "X", wyczyść tekst każdego przycisku i #status; podepnij ją pod addEventListener("click", ...) przycisku #newGame.
  7. Sprawdź w przeglądarce: rozegraj grę do wygranej i do remisu, potem kliknij „Nowa gra” - plansza i komunikat mają się wyczyścić.
  8. Dla chętnych: zapisz zmiany w swoim repozytorium - commit „Dodaję sprawdzanie zwycięzcy i reset gry”; gra jest teraz gotowym projektem.
Cały plik: tic-tac-toe.html
Odpowiedzi · Ćwiczenie (1/4)

Ćwiczenie: rozwiązanie - tic-tac-toe.html

<!-- tic-tac-toe.html, caly plik -->
<!DOCTYPE html><html lang="pl"><head>
<meta charset="utf-8"><title>Kółko i krzyżyk</title>
<style>#board{display:grid;grid-template-columns:repeat(3,100px);gap:4px}
#board button{height:100px;font-size:2rem}</style>
</head><body>
<div id="board">
<button type="button" class="cell"></button> <button type="button" class="cell"></button> <button type="button" class="cell"></button>
<button type="button" class="cell"></button> <button type="button" class="cell"></button> <button type="button" class="cell"></button>
<button type="button" class="cell"></button> <button type="button" class="cell"></button> <button type="button" class="cell"></button>
</div>
<p id="status"></p>
<button type="button" id="newGame">Nowa gra</button>
<script src="tic-tac-toe.js"></script>
</body></html>
Kod zgodny ze składnią slajdów 3-6, sprawdzony node --check; siatka i przyciski: lekcja 28; #status i #newGame: nowe w tej lekcji
Cały plik: tic-tac-toe.js — przygotowanie
Odpowiedzi · Ćwiczenie (2/4)

Ćwiczenie: rozwiązanie - przygotowanie w tic-tac-toe.js

// tic-tac-toe.js, część 1: z lekcji 28-29 + nowe przygotowanie
let board = Array(9).fill("");
let currentPlayer = "X";
const cells = document.getElementsByClassName("cell");
for (let i = 0; i < cells.length; i++) {
  cells[i].setAttribute("data-index", i);
  cells[i].addEventListener("click", handleMove);
}
const winningLines = [
  [0,1,2], [3,4,5], [6,7,8],
  [0,3,6], [1,4,7], [2,5,8],
  [0,4,8], [2,4,6]
];
const status = document.getElementById("status");
const newGameButton = document.getElementById("newGame");
newGameButton.addEventListener("click", resetGame);
Kod zgodny ze składnią slajdów 3-6, sprawdzony node --check; pętle podpinające: lekcje 28-29 (tu dla zwięzłości połączone w jedną pętlę); dalszy ciąg w dodatku 3/4 i 4/4
Cały plik: tic-tac-toe.js — handleMove
Odpowiedzi · Ćwiczenie (3/4)

handleMove: rozwiązanie

// tic-tac-toe.js, część 2: handleMove (zmieniona - dopisane sprawdzenie zwycięzcy)
function handleMove(event) {
  const index = event.target.dataset.index;
  if (board[index] !== "") {
    return;
  }
  board[index] = currentPlayer;
  event.target.textContent = currentPlayer;
  const winner = checkWinner();
  if (winner === "remis") {
    status.textContent = "Remis!";
    return;
  }
  if (winner !== null) {
    status.textContent = "Wygrywa " + winner + "!";
    return;
  }
  currentPlayer = currentPlayer === "X" ? "O" : "X";
}
Kod zgodny ze składnią slajdów 3-6, sprawdzony node --check; pierwsze pięć linii ciała funkcji: lekcja 29; wywołanie checkWinner() i komunikat w #status: nowe w tej lekcji
Cały plik: tic-tac-toe.js — checkWinner i resetGame
Odpowiedzi · Ćwiczenie (4/4)

checkWinner i resetGame: rozwiązanie

// tic-tac-toe.js, część 3: checkWinner i resetGame (nowe)
function checkWinner() {
  for (let i = 0; i < winningLines.length; i++) {
    const a = winningLines[i][0];
    const b = winningLines[i][1];
    const c = winningLines[i][2];
    if (board[a] === board[b] && board[b] === board[c] && board[a] !== "") {
      return board[a];
    }
  }
  if (!board.includes("")) {
    return "remis";
  }
  return null;
}
function resetGame() {
  board = Array(9).fill("");
  currentPlayer = "X";
  for (let i = 0; i < cells.length; i++) {
    cells[i].textContent = "";
  }
  status.textContent = "";
}
Kod sprawdzony node --check; logika uruchomiona w Node.js (bez DOM) na 3 planszach: ["X","X","X","O","O","","","",""]"X" (wygrana w rzędzie 0-1-2), ["O","X","X","X","O","","","","O"]"O" (wygrana na przekątnej 0-4-8), ["X","O","X","X","O","O","O","X","X"]"remis" (pełna plansza, brak trójki)
Na koniec

Sprawdź się

  1. Ile trójek indeksów sprawdza pętla w funkcji checkWinner()?
  2. Jaki warunek w tablicy board oznacza remis?
  3. Co dokładnie zmienia w tablicy board i na ekranie kliknięcie przycisku „Nowa gra”?

To tyle na dziś - gra w kółko i krzyżyk jest gotowym projektem.

Poza szkołą

A jak to wygląda w produkcji?

Na lekcjiW produkcjiStatus
8 układów zwycięskich wpisanych ręcznie do winningLinesPrzy większych planszach (np. Connect Four, Gomoku) liczba układów rośnie bardzo szybko - silniki gier liczą je algorytmicznie zamiast wpisywać ręcznie.zastępowane
checkWinner() skanuje od nowa całą tablicę winningLines po każdym ruchuSprawdza się tylko linie przechodzące przez pole ostatniego ruchu - mniej porównań, szybsze przy większych planszach.zastępowane
Przeciwnik to drugi uczeń klikający tę samą planszęPrzeciwnika komputerowego liczy algorytm przeglądający możliwe ruchy i wybierający najlepszy (np. minimax) - kółko i krzyżyk to klasyczny przykład uczący tego algorytmu.inna forma