Gra Saper w 100 liniach czystego JavaScript




Przywróćmy do życia grę "Saper" z roku 1989 ! Tak naprawdę jest ona znacznie starsza, ale światową sławę zdobyła kiedy dodano ją do Windows 3.1. Prawdopodobnie spowodowała ona zmarnowanie milionów godzin produktywności, kiedy ludzie grali w nią w pracy. Jeżeli masz mniej niż 30 lat, pozwól, że nauczę cię zasad gry. Jeżeli masz więcej niż 30 lat, pozwól, że przypomnę ci zasady, bo w naszym wieku pamięć już nie ta:

Plansza składa się z pól ułożonych w rzędy i kolumny, a na niektórych polach znajdują się miny. Na początku wszystkie pola są ukryte. Celem gry jest odsłonięcie wszystkich pól oprócz zaminowanych. Kiedy klikasz na pole, pokazuje się jego zawartość. Jeżeli to mina, przegrywasz. W przeciwnym wypadku, pole pokaże sumę min w ośmiu sąsiednich polach. Puste pole oznacza zero. Na podstawie odkrytych liczb obliczasz, które z sąsiednich pól są zaminowane, oznaczasz je flagą i klikasz na bezpiecznych polach, by je odkryć. Powtarzasz ten proces, aż wszystkie pola zostaną odkryte. Kliknięcie prawym klawiszem myszy przełącza między flagą, znakiem zapytania i ukrytym polem. Flagę stawiasz na tym polu, o którym jesteś pewien, że jest zaminowane. To zmniejsza liczbę min pozostałych do znalezienia. Znak zapytania możesz postawić, jeżeli podejrzewasz, że pole może być zaminowane, ale nie jesteś pewien. To pozwala ci symulować różne scenariusze - stawiasz znak zapytania i sprawdzasz, czy sąsiednie pola mają prawidłowe liczby.

Zagraj w tę grę klikając na pola na górze tej strony!

Algorytm





Kodowanie tej gry w JavaScript to fajna zabawa, ponieważ jest to stosunkowo krótki i prosty algorytm, ale jednocześnie dość interesujący. Mechanizm odkrywania wszystkich pól o wartości zero jest dobrym przykładem funkcji rekurencyjnych - ta funkcja wielokrotnie wywołuje samą siebie.

Stworzymy trzy tablice:
1. "tablica", która będzie zawierać: 'mina' w przypadku miny lub liczbę 0 do 8, która jest sumą min w ośmiu sąsiednich polach.
2. "pole", która będzie zawierać dynamicznie stworzone obiekty "obraz" (IMG), na których będzie klikał gracz. Obrazy to:

0.png do 8.png - liczba min w sąsiednich polach (puste oznacza zero)

ukryte.png - ukryte pole

flaga.png - flaga

pytanie.png - znak zapytania

Te dwa obrazy są pokazywane tylko po zakończeniu gry:

mina.png - mina

pomylka.png - pole, gdzie nieprawidłowo postawiono flagę



3. "obraz", która będzie zawierać jeden z następujących ciągów: "ukryte", "flaga" i "pytanie", tak, żebyśmy nie musieli sprawdzać nazwy obrazu i odcinać rozszerzenia ".png" za każdym razem gdy sprawdzamy, co pokazuje dane pole.

Ponieważ JavaScript nie obsługuje bezpośrednio tablic dwuwymiarowych, stworzymy tablicę tablic (linie 13-19). Na przykład główna tablica "plansza" będzie składać się z pięciu elementów odpowiadającym rzędom planszy. Każdy z tych elementów także będzie tablicą, której elementy będą reprezentowały faktyczne pola.

Najpierw, w funkcji "inicjuj", stworzymy pola i umieścimy je na ekranie. Potem losowo rozmieścimy miny.
Kiedy gracz kliknie na ukrytym polu, odkryjemy je. Jeżeli wartość tego pola to zero (co oznacza, że żadne z sąsiednich pól nie ma miny), odsłonimy także te sąsiednie pola. Jeżeli któreś z tych sąsiednich pól również będzie miało wartość zero, odsłonimy też pola sąsiadujące z tym polem... I tak dalej i tak dalej... Funkcja "odkryj" będzie wywoływać samą siebie rekurencyjnie, aż wszystkie sąsiadujące zerowe pola zostaną odkryte. To chyba najciekawsza część całego programu.

Gra kończy się, gdy gracz kliknie na zaminowanym polu (porażka) lub wszystkie niezaminowane pola zostaną odkryte (zwycięstwo). W obu przypadkach pokażemy wszystkie miny.

Miłego rozminowywania!

Kod



<html>
<body oncontextmenu='return false;' >
<br><br><br>
<label id='stan'></label>
 
<script>
const rzedy = 4;
const kolumny = 20;
let minypozostaloodkryte;
let stan = document.getElementById('stan');
stan.addEventListener('click', inicjuj)
 
let plansza = new Array(rzedy);
let obraz = new Array(rzedy);
let pole = new Array(rzedy);
for (let i = 0i < plansza.lengthi++) {
  plansza[i] = new Array(kolumny);
  obraz[i] = new Array(kolumny);
  pole[i] = new Array(kolumny)
}
 
inicjuj();
 
function sprawdz(rzadkolumna) {
  if (kolumna >= 0 && rzad >= 0 && kolumna < kolumny && rzad < rzedy)
    return plansza[rzad][kolumna];
}
 
function inicjuj() {
  miny = 5;
  pozostalo = miny;
  odkryte = 0;
  stan.innerHTML = ('Kliknij na polach, by je odkryć');
  for (let rzad = 0rzad < rzedyrzad++)
    for (let kolumna = 0kolumna < kolumnykolumna++) {
      let index = rzad * kolumny + kolumna;
      pole[rzad][kolumna] = document.createElement('img');
      pole[rzad][kolumna].src = 'ukryte.png';
      pole[rzad][kolumna].style = 'position:absolute;height:30px; width: 30px';
      pole[rzad][kolumna].style.top = 150 + rzad * 30;
      pole[rzad][kolumna].style.left = 50 + kolumna * 30;
      pole[rzad][kolumna].addEventListener('mousedown', klik);
      pole[rzad][kolumna].id = index;
      document.body.appendChild(pole[rzad][kolumna]);
      obraz[rzad][kolumna] = 'ukryte';
      plansza[rzad][kolumna] = '';
    }
 
  let ustawione = 0;
  while (ustawione < miny){
    let kolumna = Math.floor(Math.random() * kolumny);
    let rzad = Math.floor(Math.random() * rzedy);
 
    if (plansza[rzad][kolumna] != 'mine') {
      plansza[rzad][kolumna] = 'mine';
      ustawione++;
    }
  } 
 
  for (let kolumna = 0kolumna < kolumnykolumna++)
    for (let rzad = 0rzad < rzedyrzad++) {
      if (sprawdz(rzadkolumna) != 'mine') {
        plansza[rzad][kolumna] =
          ((sprawdz(rzad + 1kolumna) == 'mine') | 0) +
          ((sprawdz(rzad + 1kolumna - 1) == 'mine') | 0) +
          ((sprawdz(rzad + 1kolumna + 1) == 'mine') | 0) +
          ((sprawdz(rzad - 1kolumna) == 'mine') | 0) +
          ((sprawdz(rzad - 1kolumna - 1) == 'mine') | 0) +
          ((sprawdz(rzad - 1kolumna + 1) == 'mine') | 0) +
          ((sprawdz(rzadkolumna - 1) == 'mine') | 0) +
          ((sprawdz(rzadkolumna + 1) == 'mine') | 0);
      }
    }
}
 
function klik(event) {
  let zrodlo = event.target;
  let id = zrodlo.id;
  let rzad = Math.floor(id / kolumny);
  let kolumna = id % kolumny;
 
  if (event.which == 3) {
    switch (obraz[rzad][kolumna]) {
      case 'ukryte':
        pole[rzad][kolumna].src = 'flaga.png';
        pozostalo--;
        obraz[rzad][kolumna] = 'flaga';
        break;
      case 'flaga':
        pole[rzad][kolumna].src = 'pytanie.png';
        pozostalo++;
        obraz[rzad][kolumna] = 'pytanie';
        break;
      case 'pytanie':
        pole[rzad][kolumna].src = 'ukryte.png';
        obraz[rzad][kolumna] = 'ukryte';
        break;
    }
    event.preventDefault();
  }
  stan.innerHTML = 'Pozostało min: ' + pozostalo;
 
  if (event.which == 1 && obraz[rzad][kolumna] != 'flaga') {
    if (plansza[rzad][kolumna] == 'mine') {
      for (let rzad = 0rzad < rzedyrzad++)
        for (let kolumna = 0kolumna < kolumnykolumna++) {
          if (plansza[rzad][kolumna] == 'mine') {
            pole[rzad][kolumna].src = 'mine.png';
          }
          if (plansza[rzad][kolumna] != 'mine' && obraz[rzad][kolumna] == 'flaga') {
            pole[rzad][kolumna].src = 'pomylka.png';
          }
        }
      stan.innerHTML = 'KONIEC GRY<br><br>Kliknij tutaj, by zacząć nową grę.';
    } else
    if (obraz[rzad][kolumna] == 'ukryte') odkryj(rzadkolumna);
  }
 
  if (odkryte == rzedy * kolumny - miny)
    stan.innerHTML = 'Wygrałeś!<br><br>Kliknij tutaj, by zacząć nową grę.';
}
 
function odkryj(rzadkolumna) {
  pole[rzad][kolumna].src = plansza[rzad][kolumna] + '.png';
  if (plansza[rzad][kolumna] != 'mine' && obraz[rzad][kolumna] == 'ukryte')
    odkryte++;
  obraz[rzad][kolumna] = plansza[rzad][kolumna];
 
  if (plansza[rzad][kolumna] == 0) {
    if (kolumna > 0 && obraz[rzad][kolumna - 1] == 'ukryte') odkryj(rzadkolumna - 1);
    if (kolumna < (kolumny - 1) && obraz[rzad][+kolumna + 1] == 'ukryte') odkryj(rzad, +kolumna + 1);
    if (rzad < (rzedy - 1) && obraz[+rzad + 1][kolumna] == 'ukryte') odkryj(+rzad + 1kolumna);
    if (rzad > 0 && obraz[rzad - 1][kolumna] == 'ukryte') odkryj(rzad - 1kolumna);
    if (kolumna > 0 && rzad > 0 && obraz[rzad - 1][kolumna - 1] == 'ukryte') odkryj(rzad - 1kolumna - 1);
    if (kolumna > 0 && rzad < (rzedy - 1) && obraz[+rzad + 1][kolumna - 1] == 'ukryte') odkryj(+rzad + 1kolumna - 1);
    if (kolumna < (kolumny - 1) && rzad < (rzedy - 1) && obraz[+rzad + 1][+kolumna + 1] == 'ukryte') odkryj(+rzad + 1, +kolumna + 1);
    if (kolumna < (kolumny - 1) && rzad > 0 && obraz[rzad - 1][+kolumna + 1] == 'ukryte') odkryj(rzad - 1, +kolumna + 1);
  }
}
 
</script>
</body></html>



Inne samouczki:

JavaScript:

Fraktale - 25 linii


Złudzenie optyczne - 18 linii


Sinus scroll w stylu 8-bitowych demo - 30 linii


Płomienie - 20 linii


Python (Blender 3d):

Rozbiajanie muru kulą - 14 linii


Efekt domino - 10 linii




English version of this tutorial