Flexbox
Używany do rozmieszczania elementów w wierszu lub kolumnie
Snake
Jako obiekt JavaScript
Snake RL
Dodatkowe metody umożliwiające tworzenie tabeli akcji
Gridbox
-
Sequence
-
+
Deklaracja klasy
Po utworzeniu klasy Snake
class Snake {}
do konstruktora klasy (jest to funkcja wykonywana po zainicjalizowaniu obiektu) przekazujemy parametr display, aby później uzyskać dostęp do określonego elementu HTML.
W konstruktorze pojawi się kilka zmiennych, w tym zmiana przypisania display do osobnej zmiennej, z której będzie mogła korzystać cała klasa.
constructor(dispay) {this.display = display; ... }
Konstrutor będzie zawierał także EventListener, który zmieni kierunek poruszania się postaci na podstawie naciśniętych klawiszy.
window.addEventListener("keydown", (e) => { ... });
Metody klasy
draw() { ... }
Istnieje kilka sposobów rysowania i w tym przypadku zamiast metody Canvas zostanie użyta metoda Grid.
display.innerHTML = "";
aby wyczyścić poprzednio narysowaną klatkę.
this.snake.forEach(segment => { ... })
metoda tablicy wykona podaną funkcję dla każdego segmentu tablicy, funkcja będzie działać następująco:
const cell = document.createElement("div"); ... display.appendChild(cell);
funkcja tworzy element HTML, ustawia atrybuty CSS dotyczące pozycjonowania na siatce, dodaje klasę active i na koniec dołącza komórkę z postacią do elementu display HTML.
checkCollision(obj, head=false, wall=false)
Metoda pomocnicza sprawdza kolizję na podstawie pozycji x i y podanego obiektu i postaci. Posiada również dwa parametry dla dodatkowych celów.
randomItemPos() { ... }
Metoda pomocnicza, która będzie używana za każdym razem, gdy trzeba będzie określić losową pozycję elementu na podstawie rozmiaru siatki i pozycji postaci.
update() { ... }
Metoda aktualizuje położenie wszystkich elementów i sprawdza stan niepowodzenia gry.
for(let i=0; i < this.add; i++) { ... }
Utworzona wcześniej zmienna this.add zostanie użyta do dodania komórki do postaci na podstawie ustawionej w niej wartości. Dla this.add za każdym razem, gdy do listy komórek snake'a dodawana jest nowa pozycja, operator rozprzestrzeniania "..." odtwarza zawartość ostatniej komórki snake'a i przywraca zmiennej add wartość zero, aby zatrzymać dalsze dodawanie this.add = 0
Przesunięcie tabeli postaci powodujące przemieszczanie się
for(let i = this.snake.length -2; i >= 0; i--) {}
pętla for ma długość snake'a -2, ponieważ indeks zaczyna się od 0, oraz -1, ponieważ trzeba wykluczyć jedną komórkę, pętla jest wykonywana do momentu, gdy i jest równe 0.
this.snake[0].x += this.direction.x
i to samo dla osi y, przesuwa głowę postaci o wektor this.direction.
Kolejne dwie linie sprawdzają, kolejno, kolizję postaci z przedmiotem, aby dodać komórkę, oraz kolizję ze ścianą lub kolizję własną, aby stwierdzić, że gra nie powiodła się.
reset() { ... }
metoda, która przywraca wartości do pozycji początkowych w celu ponownego uruchomienia gry.
Aby postać poruszała się samodzielnie, klasa Snake wymaga kilku istotnych zmian. Do konstruktora zostanie dodanych pięć dodatkowych zmiennych
this.Qtable = {} ...
Qtable służy do zapisywania stanu i akcji do wykorzystania, this.alpha to współczynnik tempa nauki, this.gamma to współczynnik limitujący nagrodę, this.loopPrevention, ponieważ snake ma tendencję do utknięcia w pętli, oraz this.itemCtrl do kontroli tej pętli.
states() { ... }
Zostanie dodana metoda pozwalająca uzyskać bieżący stan postaci, czyli możliwe kolizje z głową o 1 komórkę w przyszłości oraz bezwzględny kierunek itemu (lewo, prosto, prawo). Liczba stanów musi być ograniczona, ponieważ ilość możliwości staje się zbyt duża.
response(state) { ... }
ta metoda po utworzeniu dwóch zmiennych dotyczących qtable uzyskuje lokalną tablicę trzech akcji dla danego stanu. Następnie sortuje tablicę za pomocą funkcji strzałki porównania (zwracając 1 sortuje x przed y, -1 sortuje y przed x, a 0 zachowuje oryginał).
Następnie wrzuca do qf pierwszą posortowaną wartość i stosuje zapobieganie pętli. Na końcu zwraca akcję o największej wartości lub wybiera losowo akcję, jeśli więcej akcji ma taką samą maksymalną wartość. Przy aktywacji zapobiegania pętli zostanie wybrana całkowicie losowa akcja, która została podana z tablicy qtable.
action(a) { ... }
Metoda akcji działa podobnie jak sterowanie za pomocą klawiatury, pobiera aktualny kierunek i ustawia możliwy ruch na podstawie podanej akcji.
reward(s, a) { ... }
metoda ta jako całość wykorzystuje podstawowe równanie Bellmana do ustalania i aktualizowania wartości tablicy q. Instrukcja if wyznacza nagrodę za stan preferowany, czyli gdy zbliża się kolizja, to -1, a gdy item jest widoczny, to +1, z uwzględnieniem podjętych działań.
Flexbox jest używany do rozmieszczania elementów w wierszu lub kolumnie, aby nadać kontenerowi typ flex, div musi posiadać atrybut stylu
display: flex;
domyślnie kierunek jest ustawiony na wiersz
flex-direction: row;
flexbox jako pojemnik posiada oś główną i oś poprzeczną, które zmieniają się wraz z kierunkiem flex-direction
flex-direction: column;
element w pojemniku flexbox ma wartość grow, jeśli element nie ma stałej szerokości, wartość grow jest wartością bezjednostkową, która działa proporcjami.
flex-grow: 4;
na osi głównej elementy są rozmieszczone z
na osi poprzecznej elementy są rozmieszczone z
justify-content
na osi poprzecznej elementy są rozmieszczone z
align-item
1
2
3
4
class Snake {
constructor(display) {
this.display = display;
this.failed = false;
this.direction = {x: 0, y: 0};
this.gridSize = 8;
this.snake = [{x: parseInt(this.gridSize/2), y: parseInt(this.gridSize/2)}];
this.item = this.randomItemPos();
this.FPS = 5;
this.add = 0;
window.addEventListener("keydown", (e) => {
switch(e.key) {
case "ArrowUp":
if(this.direction.y !== 0) break
this.direction = {x: 0, y: -1}
break
case "ArrowDown":
if(this.direction.y !== 0) break
this.direction = {x: 0, y: 1}
break
case "ArrowLeft":
if(this.direction.x !== 0) break
this.direction = {x: -1, y: 0}
break
case "ArrowRight":
if(this.direction.x !== 0) break
this.direction = {x: 1, y: 0}
break
}
});
}
update() {
for (let i = 0; i < this.add; i++) {
this.snake.push({...this.snake[this.snake.length - 1]})
}
this.add = 0;
for(let i = this.snake.length - 2; i >= 0; i--) {
this.snake[i + 1] = {...this.snake[i]}
}
this.snake[0].x += this.direction.x;
this.snake[0].y += this.direction.y;
if(this.checkCollision(this.item)) {
this.add += 1;
this.item = this.randomItemPos();
}
if(this.checkCollision(this.snake[0], true, true)) {
this.failed = true;
}
}
draw() {
display.innerHTML = "";
this.snake.forEach(segment => {
const elementSnake = document.createElement("div");
elementSnake.style.gridRowStart = segment.y;
elementSnake.style.gridColumnStart = segment.x;
elementSnake.classList.add("active");
display.appendChild(elementSnake);
});
const elementItem = document.createElement("div");
elementItem.style.gridRowStart = this.item.y;
elementItem.style.gridColumnStart = this.item.x;
elementItem.classList.add("error");
display.appendChild(elementItem);
}
checkCollision(obj, head=false, wall=false) {
let result = this.snake.some((segment, index) => {
if(index === 0 && head) return false;
return segment.x === obj.x && segment.y === obj.y;
});
if(wall) {
result = result || (obj.x < 1 || obj.x > this.gridSize || obj.y < 1 || obj.y > this.gridSize)
} return result;
}
randomItemPos() {
let rand = {
x: Math.floor(Math.random() * this.gridSize) + 1,
y: Math.floor(Math.random() * this.gridSize) + 1
}
while(this.checkCollision(rand)) {
rand = {
x: Math.floor(Math.random() * this.gridSize) + 1,
y: Math.floor(Math.random() * this.gridSize) + 1
}
} return rand;
}
reset() {
this.failed = false;
this.direction = {x: 0, y: 0}
this.snake = [{x: parseInt(this.gridSize/2), y: parseInt(this.gridSize/2)}];
this.item = this.randomItemPos();
this.add = 0;
}
}
constructor(display) {
this.display = display;
this.failed = false;
this.direction = {x: 0, y: 0};
this.gridSize = 8;
this.snake = [{x: parseInt(this.gridSize/2), y: parseInt(this.gridSize/2)}];
this.item = this.randomItemPos();
this.FPS = 5;
this.add = 0;
window.addEventListener("keydown", (e) => {
switch(e.key) {
case "ArrowUp":
if(this.direction.y !== 0) break
this.direction = {x: 0, y: -1}
break
case "ArrowDown":
if(this.direction.y !== 0) break
this.direction = {x: 0, y: 1}
break
case "ArrowLeft":
if(this.direction.x !== 0) break
this.direction = {x: -1, y: 0}
break
case "ArrowRight":
if(this.direction.x !== 0) break
this.direction = {x: 1, y: 0}
break
}
});
}
update() {
for (let i = 0; i < this.add; i++) {
this.snake.push({...this.snake[this.snake.length - 1]})
}
this.add = 0;
for(let i = this.snake.length - 2; i >= 0; i--) {
this.snake[i + 1] = {...this.snake[i]}
}
this.snake[0].x += this.direction.x;
this.snake[0].y += this.direction.y;
if(this.checkCollision(this.item)) {
this.add += 1;
this.item = this.randomItemPos();
}
if(this.checkCollision(this.snake[0], true, true)) {
this.failed = true;
}
}
draw() {
display.innerHTML = "";
this.snake.forEach(segment => {
const elementSnake = document.createElement("div");
elementSnake.style.gridRowStart = segment.y;
elementSnake.style.gridColumnStart = segment.x;
elementSnake.classList.add("active");
display.appendChild(elementSnake);
});
const elementItem = document.createElement("div");
elementItem.style.gridRowStart = this.item.y;
elementItem.style.gridColumnStart = this.item.x;
elementItem.classList.add("error");
display.appendChild(elementItem);
}
checkCollision(obj, head=false, wall=false) {
let result = this.snake.some((segment, index) => {
if(index === 0 && head) return false;
return segment.x === obj.x && segment.y === obj.y;
});
if(wall) {
result = result || (obj.x < 1 || obj.x > this.gridSize || obj.y < 1 || obj.y > this.gridSize)
} return result;
}
randomItemPos() {
let rand = {
x: Math.floor(Math.random() * this.gridSize) + 1,
y: Math.floor(Math.random() * this.gridSize) + 1
}
while(this.checkCollision(rand)) {
rand = {
x: Math.floor(Math.random() * this.gridSize) + 1,
y: Math.floor(Math.random() * this.gridSize) + 1
}
} return rand;
}
reset() {
this.failed = false;
this.direction = {x: 0, y: 0}
this.snake = [{x: parseInt(this.gridSize/2), y: parseInt(this.gridSize/2)}];
this.item = this.randomItemPos();
this.add = 0;
}
}