Strona główna » Po Godzinach » Gry » 2048 - Kod źródłowy
2048
 

2048 - Kod źródłowy

· Gra · Kod źródłowy ·

Wstęp

Gra 2048 została napisana przy wykorzystaniu języka JavaScript. Kod nie wymaga żadnych zewnętrznych bibliotek, aby działać. Widoczna plansza to obiekty HTML, a nad ułożeniem wszystkiego na stronie czuwa CSS.

Implementacja

Wstęp

Napisanie gry 2048 wymaga zaprojektowania sposobu przetrzymywania informacji o rozgrywce, sposobu wprowadzania ruchu (w zależności od przeciagnięcia myszą lub dotknięciem) oraz funkcję czuwające nad poprawnością przebiegu rozgrywki.

Przygotowanie rozgrywki

W momencie, gdy strona zostanie załadowana powinien zostać uruchomiony skrypt, który (2.) zainicjalizuje pole rozgrywki oraz (3.) przygotuje gre.

  1. document.addEventListener("DOMContentLoaded", function(){
  2.  initialize();
  3.  prepareGame(4,4);
  4. }, false);

W trakcie przygotowanie gry są resetowane wszystkie wartości początkowe. Z tego powodu można ją wywołać ponownie w celu zrestartowania rozgrywki. Wystarczy tylko ponownie podać te same argumenty co podczas uruchomienia.

  1. function prepareGame(gsizex, gsizey) {
  2.  reportStatus("Przygotowywanie planszy...");
  3.  createBoard(gsizex, gsizey);
  4.  addRandomElement();
  5.  gameRuns = true;
  6.  reportStatus("Gra została rozpoczęta!");
  7. }

Funkcja prepareGame() tworzy planszę o zadanym rozmiarze. Domyślnie jest to niezmiennie wartość 4×4, ale istnieje możliwość utworzenia większej planszy. Dla każdego pola tworzone są dwa elementy: kwadrat reprezentujący tło pola oraz pole tekstowe, które będzie wyświetlać wartość pola.

Ze względu na to, że pole ma kolor tła uzależniony od wartości pola to istnieje potrzeba skryptu, który będzie odpowiednio ustalał kolor pola. W celu wyliczenia tej wartości deklarowane są na początku skryptu dodatkowe dane dotyczące odcienia koloru (basicHue) oraz poziomu jasności koloru minimalnej (LIGHT_MIN) i maksymalnej (LIGHT_MAX). Dodatkowo należy określić jaka jest maksymalna wartość pola (MAX_VAL). W tym przypadku jest to 11 co oznacza 211 = 2048.

  1. var basicHue = 0, LIGHT_MIN = 40, LIGHT_MAX = 90;
  2. var MAX_VAL = 11;

Skrypt, który aktualizuje wartość pola oraz jego styl wygląda następująco:

  1. function setTextBoxValue(x,y,val){
  2.  var hue = basicHue;
  3.  var sat = 100;
  4.  var light = LIGHT_MAX - Math.floor((Math.log(val,2)/MAX_VAL) * (LIGHT_MAX - LIGHT_MIN));
  5.  getField(x,y).setAttribute("fill", "hsl("+hue+","+sat+"%,"+light+"%)");
  6.  getTextBox(x, y).innerHTML = ((val == 1) ? "" : val);
  7.  getTextBox(x, y).setAttribute("font-size", 65 - (Math.log(val,10))*5);
  8.  map[x][y] = val;
  9. }

(2.) Ustal odcień, (3.) poziom nasycenia oraz (4.) oblicz jasność koloru. (5.) Wyliczony styl zastosuj do pola. Następnie (6.) aktualizuj wartość pola oraz (7.) rozmiar czcionki. (8.) Nową wartość pola zapisz w zmiennej map.

Ruch

W celu rozpoznania ruchu należy odczytać pozycję wciśnięcia klawisza myszy, albo dotknięcia palcem. Wartości te zostaną przechowane w zmiennych globalnych clickX i clickY. Warunkiem zapamiętanie ruchu jest sprawdzenie czy gra się cały czas toczy tj. wartość zmiennej gameRuns jest true.

  1. function onPointerDown(pX, pY){
  2.  if(gameRuns){
  3.   clickX = pX;
  4.   clickY = pY;
  5.  }
  6. }

Potem na koniec przeciągnięcia myszą lub podniesienia palca z ekranu zostaje wywołana funkcja onPointerUp(), która na podstawie miejsca zakończenia akcji (pX, pY) określi kierunek przesunięcia. Funkcja upewnia się, że (6.) odległość przesunięcia była odpowiednia duża (tj.) nie był to przypadkowe przesunięcie i (7.) wylicza kąt przesunięcia. Na jego podstawie wybierany jest kierunek ruchu i przekazywany do funkcji moveElements(), która odpowiada za łączenie i przesuwanie elementów.

  1. function onPointerUp(pX, pY){
  2.  if(clickX == -1 || clickY == -1)
  3.   return;
  4.  var odl = Math.sqrt(Math.pow(clickX - pX,2)
  5.    + Math.pow(clickY - pY,2));
  6.  if(odl > 10){
  7.   var angle = Math.atan2(clickX - pX, clickY - pY);
  8.   angle = angle/Math.PI;
  9.   if(angle > -0.25 && angle < 0.25){
  10.    moveElements(0);
  11.   } else if(angle > 0.25 && angle < 0.75){
  12.    moveElements(3);
  13.   } else if(angle < -0.25 && angle > -0.75){
  14.    moveElements(1);
  15.   } else {
  16.    moveElements(2);
  17.   }
  18.  }
  19.  clickX = -1;
  20.  clickY = -1;
  21. }

Wykonane przesunięcia

Łączenie elementów polega na wywołaniu tego samego zestawu instrukcji dla każdego wiersza / kolumny. Jednak za każdym razem dane wejściowe są inne i należałoby rozpatrywać każdy kierunek przesunięcia oddzielnie (choć istniała by możliwość wyszczególnienia wspomnianego zestawu instrukcji). W celu uproszczenia kodu warto zastosować sztuczkę.

Przypuśćmy, że napisaliśmy kod, który działa tylko dla ruchu w prawo. Zadziała on dla innych przypadków jeśli przed jego wykonaniem tabela zostanie obrócona odpowiednią ilość razy. Po zakończeniu przesuwania wystarczy ponownie obrócić odpowiednią ilość razy i funkcja będzie działać dla dowolnego kierunku. O obracaniu tabeli kwadratowej można przeczytać w tym artykule, a kod odpowiedzialny za wykonanie ruchu elementów przedstawia się następująco:

  1. function moveElements(angle) {
  2.  var rotation = angle;
  3.  while(rotation != 2){
  4.   rotateMap90();
  5.   rotation = (rotation+1)%4;
  6.  }
  7.  var mapb = [];
  8.  for(var x = 0; x < gsizex; x++) {
  9.   var submap = [];
  10.   var last = 0;
  11.   for(var y = gsizey - 1; y >= 0; y--) {
  12.    var el = map[x][y];
  13.    if(el != 1){
  14.     if(last == el){
  15.      submap[submap.length - 1]*=2;
  16.      points += submap[submap.length - 1];
  17.      last = 0;
  18.     } else {
  19.      submap.push(el);
  20.      last = submap[submap.length - 1];
  21.     }
  22.    }
  23.   }
  24.   while(submap.length < 4){
  25.    submap.push(1);
  26.   }
  27.   submap.reverse();
  28.   mapb.push(submap);
  29.  }
  30.  map = mapb;
  31.  while(rotation != angle){
  32.   rotateMap90();
  33.   rotation = (rotation+1)%4;
  34.  }
  35.  updateMap();
  36.  if(checkWin()){
  37.   reportStatus('Zwycięstwo! Zdobytych punktów:'+points+', <a href="javascript:void(0)"
  38. onclick="restart()">zagraj ponownie</a>');
  39.   gameRuns = false;
  40.  } else {
  41.   reportStatus('Zdobytych punktów:'+points+'');
  42.   addRandomElement();
  43.  }
  44. }

(2. - 5.) Wstępne obrócenie tabeli. Następnie (6. - 30.) przesunięcie elementów w prawo i łączenie identycznych elementów oraz (31. - 35.) przypisanie nowej tabeli danych i jej obrócenia. Na koniec (36.) należy zaktulizować planszę i sprawdzić (37.) czy gracz spełnił warunek zwycięstwa.

Aktulizacja mapy to wywołanie funkcji updateMap() dla każdego pola na planszy. W przypadku wykonania ruchu nie wystarczy zaktualizować tylko wybranych pól.

  1. function updateMap(){
  2.  for(var y = 0; y < gsizey; y++) {
  3.   for(var x = 0; x < gsizex; x++) {
  4.    setTextBoxValue(x, y, map[x][y]);
  5.   }
  6.  }
  7. }

Sprawdzenie warunku zwycięstwa polega na sprawdzeniu czy istnieje pole o wartości 2048 (albo innej ustlaonej wartości przez MAX_VAL).

  1. function checkWin(){
  2.  for(var y = 0; y < gsizey; y++) {
  3.   for(var x = 0; x < gsizex; x++) {
  4.    if(Math.log2(map[x][y]) == MAX_VAL){
  5.     return true;
  6.    }
  7.   }
  8.  }
  9.  return false;
  10. }

Jeśli gracz nie osiągnął zwycięstwa to gra toczy się dalej. Z tego powodu należy dodać nowy element o wartości 2 na planszę. W tym celu najpierw (2. - 9.) sprawdzane są, które pola są wolne. Gra (10.) może zakończyć się porażką jeśli nie ma ani jednego wolnego pola na dołożenie elementu. W przciwnym razie (14.) wybierane jest dowolne pole i (15.) jego wartość jest zaktualizowana.