Strona główna » Po Godzinach » Gry » Saper - kod źródłowy
 

Saper - kod źródłowy

· Gra · Kod źródłowy ·

Wstęp

Aplikacja Saper została napisana przy pomocy JavaScript, które zarządza wyświetlaną treścią zmieniając kod HTML oraz CSS. Użyte funkcje są zgodne z najnowszym standardem HTML5.

Ekran gry

Część prezentowana użytkownikowi są to dwa obiekty typu DIV. Jeden z nich o id status wyświetla komunikaty użytkownikowi, a game pola rozgrywki. Są one całkowicie niezależne od siebie i nie trzeba ich ustawiać tak jak w przykładzie jeden za drugim:

  1. <div id="status"></div>
  2. <div id="game"></div>

Przygotowanie rozgrywki

Przed rozpoczęciem rozgrywki na początku rozgrywki są zmienne globalne, które sterują ustawieniami rozgrywki:

  1. var defX = 14, defY = 8, defM = 14;
  2. var MAXSIZEX = 640;
  3. var MAXSIZEY = 400;
  4. var toDiscover = 0;

(1.) Wszystkie zmienne def# są to zmienne, które są zmiennymi przechowującymi ustawienia aktualnie przygotowanej / trwającej rozgrywki. defX - określa ile pól ma być w wierszu, a defY - z ilu wierszy ma składać się z plansza. Ostatnia zmienna defM określa ile min ma zostać ukrytych na planszy. Następnie określona jest maksymalna (2.) szerokość i (3.) wysokość ekranu rozgrywki (bez obiektu status). (4.) Ostatnia zmienna w trakcie rozgrywki będzie przechowywała informacja ile pól powinno zostać jeszcze odkrytych.

Na podstawie podanych wartości można przejść do przygotowania rozgrywki. Odpowiada za nią funkcja prepareGame(), która przyjmuje trzy argumenty: ile pól w wierszu, ile wierszy i ile min ma być na planszy.

  1. function prepareGame(elx, ely, mines){
  2.  window.oncontextmenu = function (){return false;}
  3.  defX = elx; defY = ely; defM = mines;
  4.  var obj = document.getElementById("game");
  5.  var sizex = (MAXSIZEX) / elx;
  6.  var sizey = (MAXSIZEY) / ely;
  7.  var size = (sizex < sizey) ? sizex : sizey;

(2.) Wyłączenie prawego przycisku - w dalszej części kodu zostanie wyjaśnione jak zostaje przechwycony, aby można nim oznaczać pola na kolorowo. (3.) Zapamiętanie aktualnych ustawień rozgrywki na podstawie argumentów. (4.) Znalezienie obiektu game na stronie. (5. - 7.) Wyliczenie rozmiaru pola tak, aby plansza nie była większa niż maksymalny rozmiar.

Kolejny etap polega na przygotowaniu planszy.

  1.  toDiscover = 0;
  2.  obj.innerHTML = "";
  3.  obj.innerHTML += "<style>#game > div{width:"+size+"px;height:"+size+"px;line-height:"+size+"px;font-size:"+(size-2)+"px}</style>"
  4.  var list = [];
  5.  for(var i = 0; i < mines; i++)
  6.   list.push("1");
  7.  for(var i = mines; i < elx*ely; i++){
  8.   list.push("0");
  9.   toDiscover++;
  10.  }
  11.  for(var i = elx*ely - 1; i > 0; i--){
  12.   var chosen = Math.floor(Math.random() * i);
  13.   var swap = list[chosen];
  14.   list[chosen] = list[i];
  15.   list[i] = swap;
  16.  }

(1.) Wyzerowanie ile pól jest do odkrycia. (2.) Wyczyszczenie ekranu gry i (3.) dodanie stylu pól. Należy to zrobić, ponieważ w zależności od argumentów rozmiar pól może być inny za każdym razem. (4.) Utworzenie listy pomocniczej. (5. - 6.) Dla każdego pola z miną dodawany jest obiekt 1. (7.) Dla wszystkich pól bez miny: (8.) dodaj obiekt 0 i (9.) zwiększ ile takich pól będzie na planszy. (11. - 16.) Następnie potasuj listę. Tasowanie polega na wylosowaniu, który obiekt zostaje zamieniony z ostatnim w aktualnie rozpatrywanym zakresie. W i-tym kroku rozpatrywane są wszystkie elementy prócz i ostatnich. W ten sposób program losuje plansze.

Na podstawie listy zostanie teraz przygotowana plansza:

  1.  for(var i = 0; i < ely; i++){
  2.   for(var j = 0; j < elx; j++){
  3.    var div = document.createElement("div");
  4.    if(list[i*elx+j] == "1")
  5.     div.classList.add("bomb");
  6.    div.id = i + "x" + j;
  7.    div.addEventListener("mousedown", start);
  8.    div.addEventListener("touchstart", start);
  9.    div.addEventListener("click", click);
  10.    div.addEventListener("mouseout", cancel);
  11.    div.addEventListener("touchend", cancel);
  12.    div.addEventListener("touchleave", cancel);
  13.    div.addEventListener("touchcancel", cancel);
  14.    obj.appendChild(div);
  15.   }
  16.  }

(17.) Dla każdego wiersza: (18.) i każdego pola: (19.) utwórz obiekt DIV. (20.) Sprawdź na liście czy pole ma być bombą. (21.) Jeśli tak to dodaj do obiektu klasę bomb. (22.) Identyfikatorem obiektu będzie jego pozycja x, y na planszy - pozwoli to na prostsze pobieranie wartości z planszy. Następnie (23. - 29.) dodane zostaje co ma wykonać program na podstawie zdarzenia. Każde wydarzenie zostanie wyjaśnione w sekcji Sterowanie. (30.) dopisz element do ekranu rozgrywki.

  1.  obj.style.filter = "";
  2.  obj.style.width=((size + 2)*elx + 4)+"px";
  3.  obj.style.height=((size + 2)*ely + 4)+"px";
  4.  document.getElementById("status").style.width=((size + 2)*elx)+"px";
  5.  showMessage("Wykrytych min: " + mines);
  6. }

(33.) Usuń wszelkie filtry z planszy - czynność ta jest potrzebna po restarcie rozgrywki, aby usunąć rozmycie planszy na zakończenie rozgrywki. (34. - 35.) Ustal rozmiar ekranu gry. (36.) Dopasuj szerokość pola status do ekranu rozgrywki i (37.) pokaż ile min jest do znalezienia.

Wyświetlanie wiadomości polega na zmianie tekstu wyświetlanego w obiekcie status:

  1. function showMessage(msg){
  2.  document.getElementById("status").innerHTML = msg;
  3. }

Sterowanie

Ze względu na możliwość oznaczanie podejrzanych pól na dwa różne kolory potrzebne było dodanie własnych funkcji rozpoznających długie kliknięcie. Na komputerze wystarczyłoby rozróżnić tylko lewy i prawy przycisk, ale na urządzeniach mobilnych prawego przycisku nie ma. Z tego powodu został zastąpiony poprzez dłuższe przytrzymanie palca na ekranie. Rozwiązanie to też działa dla tradycyjnej myszy komputerowej. Na początek trzeba dodać dwie zmienne globalne:

  1. var longpress = false;
  2. var presstimer = null;

(1.) Przechowa czy kliknięcie było długie. (2.) Stoper, który sprawdzi ile zostało przytrzymane pole.

Na rozpoczęcie kliknięcia lub dotknięcia wywołana zostaje funkcja start():

  1. var start = function(e) {
  2.  if (e.type === "click" && e.button !== 0) {
  3.   return;
  4.  }
  5.  if (e.button == 2) {
  6.   markPos(e.target.id, "changemark");
  7.   return;
  8.  }
  9.  longpress = false;
  10.  if (presstimer === null) {
  11.   presstimer = setTimeout(function() {
  12.    markPos(e.target.id, "changemark");
  13.    longpress = true;
  14.   }, 500);
  15.  }
  16.  return false;
  17. };

(2. - 4.) Flitrowanie zbędnych zdarzeń. (5.) Jeśli wciśnięty został prawy przycisk to (6.) oznacz wybrane pole i (7.) zakończ funkcję. (9.) Jednak jeśli jest to lewy przycisk myszy lub dotknięcie to przypuść, że jest to krótkie wciśnięcie i (10.) jeśli aktualnie stoper nie jest używany to (11. - 14.) za 500ms wywołaj oznaczenie pola i ustal, że kliknięcie / dotknięcie było długie. W przypadku przerwania kliknięcia / dotyku wystarczy zatrzymać odliczanie. (16.) Zwróć fałsz.

Na kliknięcie program wykona funkcje click():

  1. var click = function(e) {
  2.  if (presstimer !== null) {
  3.   clearTimeout(presstimer);
  4.   presstimer = null;
  5.  }
  6.  if (longpress) {
  7.   return false;
  8.  }
  9.  markPos(e.target.id, "click");
  10. };

(2.) Jeśli stoper jest aktywny to (3. - 4.) wyczyść go. (6.) Jednak stoper mógł zakończyć działanie i wywołać akcję po długim wciśnięciu, dlatego należy sprawdzić czy to zrobił. Jeśli tak to (7.) wystarczy zakończy działanie, ale jeśli nie to (9.) należy wywołać kliknięcie na wybranym obiekcie.

W przypadku innych zdarzeń takich jak wyjście poza obszar przez mysz lub dotknięcie, anulowanie dotknięcia wywołana zostanie funkcja cancel(). Jej zadanie sprowadza się do wyczyszczenia stopera, aby nie zostało wykonane długie wciśnięcie, a krótkie, na obiekcie.

  1. var cancel = function(e) {
  2.  if (presstimer !== null) {
  3.   clearTimeout(presstimer);
  4.   presstimer = null;
  5.  }
  6. };

Rozgrywka

Podczas rozgrywki niezależnie od rodzaju kliknięcia zostaje wywołana funkcja markPos(), która przyjmuje dwa argumenty - identyfikator klikniętego obiektu oraz typ zdarzenia. Funkcja ma za zadanie oznaczyć, albo odkryć wybrane pole.

  1. function markPos (id, type) {
  2.  var obj = document.getElementById(id);
  3.  if(obj == null)
  4.   return;

Na początek funkcja musi upewnić się, że żądany obiekt faktycznie istnieje. Ma to znaczenie, dlatego, że po odkryciu pustego pola wywołany zostaje kod, który odkrywa sąsiadujące niepuste pola. Funkcja ta nie zwraca uwagi na to czy obiekt istnieje, a jedynie określa, że dane pole ma być odkryte.

  1.  switch(type){
  2.   case "changemark":
  3.    if(obj.classList.contains("mbomb")){
  4.     obj.classList.remove("mbomb");
  5.     obj.classList.add("midk");
  6.    } else if (obj.classList.contains("midk")){
  7.     obj.classList.remove("midk");
  8.    } else {
  9.     obj.classList.add("mbomb");
  10.    }
  11.   break;

W przypadku zdarzenia changemark, które jest wywoływana po długim dotknięciu / kliknięciu lub na prawy przycisk myszy program zmienia rodzaj oznaczenia. Z pustego na czerwone, z czerwonego na niebieskie i z niebieskiego na brak oznaczenia. Oznaczenia zostają dokonane poprzez dodawanie / usuwanie / sprawdzanie klas obiektu.

  1.   case "click":
  2.    if (!(obj.classList.contains("sel") || obj.classList.contains("sbomb"))){
  3.     if(obj.classList.contains("bomb")){
  4.      obj.classList.add("sbomb");
  5.      showMessage('Koniec gry, <a href="javascript:void" onclick="prepareGame(defX, defY, defM)">rozpocznij od nowa</a>');
  6.      endGame();

(16.) W przypadku kliknięcia pola: (17.) sprawdź czy pole, ani nie jest odkryte, albo czy jest bombą. (18.) Jeśli pole ma minę to: (19.) dodaj klasę nadającą styl minie, (20.) pokaż komunikat o przegranej i (21.) zakończ grę.

  1.     } else {
  2.      obj.classList.add("sel");
  3.      toDiscover--;
  4.      var balls = countBalls(obj);
  5.      obj.classList.add("sel" + balls);
  6.      if(balls == 0){
  7.       var pos = obj.id.split("x");
  8.       for(var x = parseInt(pos[0]) - 1; x <= parseInt(pos[0]) + 1; x++){
  9.        for(var y = parseInt(pos[1]) - 1; y <= parseInt(pos[1]) + 1; y++){
  10.         markPos(x + "x" + y, type);
  11.        }
  12.       }
  13.      } else {
  14.       obj.innerHTML = balls;
  15.      }
  16.     }
  17.    }
  18.   break;
  19.  }

(23.) Jednak jeśli pole nie ma miny to: (24.) dodaj, że pola zostało odkryte tj. dodaj klasę sel. (25.) Zmniejsz ilość pól do odkrycia. (26.) Policz ile jest min dookoła klikniętego pola. (27.) Dodaj styl dla pola otoczonego przez balls min. (28.) Jeśli wokół nie ma min to (29. - 34.) odkryj wszystkie pola dookoła. (35.) Jednak jeśli ilość min jest większa do zera to: (36.) wyświetla na polu ich ilość.

  1.  if(toDiscover == 0){
  2.   showMessage('Gra zakończona sukcesem ! <a href="javascript:void" onclick="prepareGame(defX, defY, defM)">Zagraj jeszcze raz</a>');
  3.   endGame();
  4.  }
  5. }

(42.) Po zakończeniu kliknięcia lub oznaczania pola sprawdź czy pozostały jeszcze pola do odkrycia. Jeśli ich ilość wynosi 0 to (43.) pokaż komunikat o zwycięstwie i (44.) zakończ rozgrywkę.

Funkcja markPos() jest wspierana przez trzy funkcje: countBalls(), isBomb() oraz endGame(). Pierwsza z nich ma za zadanie policzyć ile bomb znajduje się wokół pola:

  1. function countBalls(obj){
  2.  var pos = obj.id.split("x");
  3.  var bombsaround = 0;
  4.  for(var x = parseInt(pos[0]) - 1; x <= parseInt(pos[0]) + 1; x++){
  5.   for(var y = parseInt(pos[1]) - 1; y <= parseInt(pos[1]) + 1; y++){
  6.    bombsaround += isBomb(x + "x" + y) ? 1 : 0;
  7.   }
  8.  }
  9.  return bombsaround;
  10. }

Z kolei funkcja isBomb() sprawdza czy na danym polu jest mina:

  1. function isBomb(id){
  2.  var obj = document.getElementById(id);
  3.  if(obj != null){
  4.   return obj.classList.contains("bomb");
  5.  }
  6.  return false;
  7. }

Zakończenie gry polega na rozmyciu ekranu gry oraz dodaniu pola, które nie pozwoli na prowadzenie dalszej rozgrywki, a przy okazji przyciemni delikatnie ekran rozgrywki:

  1. function endGame(){
  2.  document.getElementById("game").style.filter = "blur(2px)";
  3.  document.getElementById("game").innerHTML += '<pstyle="position:absolute;width:'+document.getElementById("game").style.width+';height:'+document.getElementById("game").style.height+';background:rgba(0,0,0,0.2);margin:0"></p>';
  4. }

Styl rogrywki

Poniżej znajduje się pełny wykaz, stałych klas CSS, które odpowiadają za wyświetlanie ekranu rozgrywki. Pierwszy dotyczy ekranu rozgrywki:

  1. #game {
  2.  background:#eceff1;
  3.  transition:filter 2s;
  4.  margin:0 auto;
  5.  padding:4px;
  6. }

Styl pola (domyślny, nieodkryty)

  1. #game > div {
  2.  border:1px solid #000;
  3.  float:left;
  4.  background:#AAA;
  5.  transition:background 1s;
  6.  text-align:center;
  7.  border-style:outset;
  8. }
  9. #game > div:hover {background:#DDD;}

Styl czerwonego oznaczenia (przypuszczenie miny)

  1. #game > div.mbomb {background:#c62828;}
  2. #game > div.mbomb:hover {background:#ffcdd2;}

Styl niebieskiego oznaczenia (ciężko stwierdzić czy jest mina czy nie)

  1. #game > div.midk {background:#00695c;}
  2. #game > div.midk:hover {background:#b2dfdb;}

Styl odkrytej bomby

  1. #game > div.sbomb {
  2.  background:#000;
  3. }

Styl odkrytego pola (bez miny)

  1. #game > div.sel {
  2.  background:#E5E5E5;
  3.  font-weight:700;
  4. }

Styl tekstu odkrytego pola w zależności od jego wartości

  1. #game > div.sel1 {color:#33691e;}
  2. #game > div.sel2 {color:#827717;}
  3. #game > div.sel3 {color:#f57f17;}
  4. #game > div.sel4 {color:#ff6f00;}
  5. #game > div.sel5 {color:#e65100;}
  6. #game > div.sel6 {color:#bf360c;}
  7. #game > div.sel7 {color:#b71c1c;}
  8. #game > div.sel8 {color:#d50000;}

Styl obiektu z wiadomościami dla gracza

  1. #status {
  2.  background:#bbdefb;
  3.  height:30px;
  4.  line-height:30px;
  5.  margin:0 auto;
  6.  padding:4px;
  7. }

Styl przycisku

  1. .btn {
  2.  line-height:36px;
  3.  border:1px solid #000;
  4.  border-radius:8px;
  5.  text-decoration:none;
  6.  padding:4px;
  7. }
  8. .btn:hover {
  9.  color:#000;
  10. }