Игра Змейка на JavaScript
Первая версия змейки появилась в 1976 году, а последняя - сегодня.
Пока проходил курс по JS, стало немного скучновато и я решил найти какой-нибудь гайд на ютубе, чтобы попрактиковаться.
У автора этой змейки есть две версии: в первом видео он делает змейку одним JS файлом, а во-втором видео уже на модулях с ООП (Объектно ориентированное программирование). Я сделал оба варианта, но именно второй мне захотелось разобрать по частям, чтоб лучше понять что к чему, т.к. он был намного менее понятен чем первый, там много классов и сложная модульная структура.
Гитхаб автора со змейкой: https://github.com/EpicLegend/snake2d-opp
Все что будет ниже, я писал для того, чтоб просто разобрать непонятные мне части кода, но в процессе начал вносить в змейку и свои доработки. Поэтому код немного отличается от кода автора.
Мой Гитхаб с моей змейкой: https://github.com/Buninman/Snake
Буду рад на указание ошибок, даже орфографических🙂
Основные изменения моей змейки:
- Поменял дизайн игрового поля, добавил вниз блок с подсказкой;
- Сделал счетчик рекорда, он запоминает наибольший накопленный результат;
- Добавил возможность менять скорость змейки клавишами PgUp, PgDwn;
- Теперь есть режим бога, в котором змейка не может себя съесть. Кнопка G;
- Убрал возможность змейке разворачиваться на 180º и кусать себя. Больше она не умирает от случайного нажатия.
Начало
Набросал схему устройства приложения, чтоб было понятнее что куда идет в плане импорта объектов. По такому порядку и пойдем:
supportFunction.js
В модуле всего одна функция, подробнее о ней можно посмотреть в документации, она целиком оттуда.
export const getRandomInt = (min, max) =>
Math.floor( Math.random() * (max - min) + min )
// Math.random() возвращает псевдослучайное число
// с плавающей запятой в диапазоне от 0 до менее 1
// (включая 0, но не 1)
// Math.floor() возвращает наибольшее целое число,
// меньшее или равное заданному числу. ОкругляетЭта функция возвращает случайное число между указанными значениями. Возвращаемое значение не меньше (и, возможно, равно) minи меньше (и не равно)max.
getRandomInt() вызывается в файле berry.js и имеет такой вид:
Для x: getRandomInt(0, this.canvas.element.width / this.config.sizeCell) * this.config.sizeCell Для y: getRandomInt(0, this.canvas.element.height / this.config.sizeCell) * this.config.sizeCell
Соответственно на min передается 0, а на max передается:
Для x: ширинаПоля (688) / размерЯчейки (16) - это 43 Для y: высотаПоля (480) / размерЯчейки (16) - это 30
Таким образом, когда функция срабатывает, мы получаем число в диапазоне от 1 до 42 для x и от 1 до 49 для y.
config.js
Этот модуль претерпел самые большие изменения, вместо 8 строк кода, теперь тут 38.
Значения config.js используются в berry.js, snake.js и gameloop.js
export default class Config {
constructor() {
this.speedBlock = document.querySelector('#speed')
this.godModeColor = document.querySelector('.game-header')
this.textBlock = document.querySelector('.keys-conf')
// Ищем значения в HTML коде, по названиям классов и id
// Тут мы ищем индикатор скорости, цвет плашки под ним
// и блок текста с инструкцией
this.step = 0
this.maxStep = 6
// step и maxStep нужны чтоб пропускать игровой цикл,
// их использует функция в файле gameLoop.js.
// Также maxStep регулирует скорость змейки,
// чем выше значение тем реже перерисовывается змейка
this.sizeCell = 16
this.sizeBerry = this.sizeCell / 4
// sizeCell это размер ячейки, а sizeBerry это размер ягодки
this.godMode = false
// Переменная отвечает за режим бога. Изначально он выключен
this.colorLight = '#A55E00'
this.colorDark = '#5E3908'
// Переменные с цветами змейки. Есть темный оттенок
// и светлый, для головы змейки
this.drawSpeed()
// Метод отрисовывает значение скорости
}
speedLevel() {
return 10 - this.maxStep
// Чтоб значения скорости были понятнее я сделал
// функцию которая меняет порядок чисел, теперь 1
// это самая медленная скорость, а 8 - самая быстрая
}
drawSpeed() {
this.speedBlock.innerHTML = this.speedLevel()
// innerHTML обращается к значению в HTML
// и меняет на текущую скорость змейки
}
drawGodMode() {
if (this.godMode) {
// Если режим бога сейчас true то:
this.colorLight = '#FA0556'
this.colorDark = '#A00034'
// Меняем цвета в переменных на красные.
// Они также подхватываются змейкой
this.textBlock.innerHTML = 'God Mode: ON'
// Вместо инструкции пишем о включенном режиме
this.godModeColor.style.backgroundColor = this.colorDark
// style.backgroundColor получает значение цвета
// параметра background в css-файле.
// Тут мы меняем цвет плашки под счетом
} else {
// Если режим бога сейчас false:
this.colorLight = '#A55E00'
this.colorDark = '#5E3908'
// Меняем цвета в переменных на оранжевые
this.textBlock.innerHTML
='Use the Arrows and PageUp/PageDown'
// Пишем стандартную инструкцию внизу поля
this.godModeColor.style.backgroundColor = this.colorDark
// Меняем цвет плашки
}
}
}🙁Не смог сделать так, чтоб ягодка меняла цвет вместе со змейкой. Сделал ягодку белой.
Свойства step и maxStep используются в модуле gameLoop.js, внутри функции animate():
animate() {
requestAnimationFrame(this.animate)
// Вызывает повторно функцию animate()
// так образуется непрерывный цикл
if (++this.config.step < this.config.maxStep) {
return
// Если ++step меньше maxStep, то ничего не делать.
// Таким образом, надо 6 раз вызвать функцию,
// чтоб змейка сделала 1 шаг
}
this.config.step = 0
// После движения змейки, счетчик возвращается на 0
this.update()
this.draw()
}++ увеличивает значение на единицу и возвращает значение.Оператор ++ будет прибавлять 1 к step до тех пор, пока на шестом вызове функции она не пройдет дальше. После функция обнуляет значение step.
score.js
Модуль отвечает за отрисовку очков за съеденные ягодки. Сюда же я добавил функцию drawRecord(), которая вызывается в случае смерти змейки сохраняет наш прогресс в виде рекорда.
Значение score.js используются в game.js
export default class Score {
constructor(scoreBlock, recordBlock, score = 0) {
this.scoreBlock = document.querySelector(scoreBlock)
this.recordBlock = document.querySelector(recordBlock)
// Ищем значения в HTML коде, по id. Тут мы ищем
// счетчики очков и рекорда
this.score = score
// Создает свойство со значением очков
this.draw()
// Вызывает метод объекта draw() при создании экземпляра
}
incScore() {
this.score++
// Добавляет 1 к счету за съеденную ягодку
this.draw()
// Запускает метод draw() который поменяет значение счетчика
}
setToZero() {
this.score = 0
// Обнуляет счетчик очков. Это нужно в случае сметри
this.draw()
// Запускает метод draw()
}
drawRecord() {
if (this.score > this.record) {
// Если значение очков больше чем текущий рекорд, то
this.record = this.score
// Меняем значение рекорда на текущее значение очков
this.recordBlock.innerHTML = this.record
// Меняем значение рекорда в HTML
}
}
draw() {
this.scoreBlock.innerHTML = this.score
// innerHTML обращается к значению в HTML
// и меняет значение счета в HTML
}
}canvas.js
Модуль отвечает за создание и размеры игрового поля. Я менял только размер игрового поля.
Значение canvas.js используются в game.js
export default class Canvas {
constructor(container) {
this.element = document.createElement('canvas')
// .createElement создает HTML-элемент <canvas> в HTML-файле
this.context = this.element.getContext('2d')
// .getContext('2d') указывает JS что мы хотим рисовать в 2д.
// Нам становится доступен набор свойст и методов
// таких как beginPath(), которым нарисована ягодка
this.element.width = 688
this.element.height = 800
// Это высота и ширина 2д контекста, т.к. одна ячейка
// у нас 16рх, то значения должны быть кратны 16рх
container.appendChild(this.element)
// Как я понял, .appendChild создает дочерний элемент.
// Он помещает наш созданный 2д контекст (игровое поле)
// в <canvas> внутри HTML-файла
}
}berry.js
Модуль отвечает за все что происходит с ягодкой.
Значение berry.js используются в game.js
import Config from './config.js'
import {getRandomInt} from './supportFunction.js'
export default class Berry {
constructor(canvas) {
this.x = 0
this.y = 0
// Это координаты ягодки
this.canvas = canvas
// в Berry передаются параметры игрового поля,
// нам нужны оттуда только высота и ширина
this.config = new Config()
// Передаем данные с Config, нам оттуда нужны размеры ячейки
// и размер ягодки
this.randomPosition()
// Запускаем функцию получения рандомных кординат для ягодки
}
draw(context) {
// Функция рисования ягодки. Сюда передается наш 2д контекст
// из класса Canvas
context.beginPath()
// Создает новый векторый контур. Начинаем рисование
context.fillStyle = '#EBEBEB'
// Заливка этого контура белым цветом. Если поменять ее
// на динамичную переменную this.config.colorLight, то
// цвет ягодки все равно не будет меняться
// Я не понимаю почему🙁
context.arc(this.x + (this.config.sizeCell / 2),
// arc() создает векторную дугу. Замкнутая дуга это круг
// В arc() нужно передать координаты x и y,
// радиус, начальный угол и конечный угол
// Берем рандомную координату x, прибавляем
// к ней половину размера ячейки.
this.y + (this.config.sizeCell / 2),
// Тоже самое для координаты y,
// Это дает нам положение центра ячейки
// Центр нашей ягодки
this.config.sizeBerry, 0, 2 * Math.PI)
// Берем размер ягодки из Config, это радиус
// 0 - это начальный угол
// 2 * Math.PI - это конечный угол, такой
// параметр указан в учебнике по JS
context.fill()
// Метод fill() создает заливку для нашей ягодки,
}
randomPosition() {
this.x = getRandomInt(0, this.canvas.element.width /
this.config.sizeCell) *
this.config.sizeCell
this.y = getRandomInt(0, this.canvas.element.height /
this.config.sizeCell) *
this.config.sizeCell
// Функция получения рандомных координат, два раза
// вызывает функцию генерации случайных чисел
// и присваивает их x и y координатам
}
}Функция getRandomInt() расписана в модуле supportFunction.js
snake.js
Модуль за все что связано со змейкой. Я добавил функцию сохранение рекорда после смерти змейки, поменял параметры старта и изменил назначение клавиш.
Значение snake.js используются в game.js
import Config from './config.js'
export default class Snake {
constructor() {
this.config = new Config()
// Передаем данные с Config, нам нужен размер ячейки
this.x = 336
this.y = 480
// Начальные координаты змейки,
// я сделал чтоб она появлялась внизу посередине
this.dx = 0
this.dy = -this.config.sizeCell
// Начальное направление движения змейки,
// по x - 0, а по y на -16рх, т.е. вверх.
// Ось кординат начинается сверху слева
// Ось y идет сверху вниз
this.tails = []
// Пустой массив для записи данных о хвосте змейки
this.maxTails = 1
// Параметр длины змейки. Я сделал по умолчанию 1,
// т.е. только голова
this.control()
// Запускаем функцию отвечающую за упарвление змейкой
}
update(berry, score, canvas) {
// Функция обновления змейки. Сюда передаются данные ягодки,
// счета и игрового поля
this.x += this.dx
this.y += this.dy
// Прибавляем к координатам змейки размер движения.
// Змейка перемещается путем прибавления или вычитания
// размера ячейки от следующих координат положения головы
if (this.x < 0) {
// Если координата x меньше 0 то
this.x = canvas.element.width - this.config.sizeCell
// Присваиваем ей значение ширины игрового поля
// минус одну ячейку.
// Т.е. если змейка доходит до края, то
// она появится с другой стороны
} else if (this.x >= canvas.element.width) {
// то, при условии, что координата x больше или равна
// ширине игрового поля
this.x = 0
// Кордината x = 0, начало поля.
// Т.е. если змейка доходит до края, то
// она появится с другой стороны
}
if (this.y < 0) {
this.y = canvas.element.height - this.config.sizeCell
} else if (this.y >= canvas.element.height) {
this.y = 0
// Логика с вертикальной осью такая же, но в расчет берем
// высоту поля, а не ширину
}
this.tails.unshift({ x: this.x, y: this.y})
// Метод .unshift() добавляет элемент в начало массива
// с хостом. Добавляем туда объект с координатами x и y.
// Это секция хвоста
if (this.tails.length > this.maxTails) {
// Если длина массива с хвостом больше чем параметр длинны
// хвоста, то
this.tails.pop()
// Удаляем последний элемент массива
// Метод .pop() удаляет элемент в конце массива
}
this.tails.forEach((el, index) => {
// Метод .forEach() перебирает все элементы массива,
// все элементы змейки, и применяет к ним функцию.
// В функцию передаются все элементы массива и их индексы
if (el.x === berry.x && el.y === berry.y) {
// Если координаты x и y в какой-то из секций змейки
// совпадают с координатами ягоды, то
this.maxTails++
// Добавляем 1 к параметру длины змейки
score.incScore()
// Вызываем метод incScore(). Это метод нашего
// объекта Score, он прибавляем 1 единичку к счету
berry.randomPosition()
// Вызываем метод randomPosition(). Это метод нашего
// объекта Berry, он переносит ягодку на новые
// случайные координаты
}
for (let i = index + 1; i < this.tails.length; i++) {
// Цикл for запускает функцию пока условие верно.
// Переменная i это индекс элементов массива, прибавляем
// к ней 1, чтоб игнорировать элемент 0, это голова.
// Цикл будет продолжаться пока i меньше длинны массива
// Каждый цикл увеличиваем i на 1
if ( el.x == this.tails[i].x &&
el.y == this.tails[i].y &&
!this.config.godMode) {
// Если координаты x и y какого элемента равны
// каким-то другим координатам из массива, кроме
// головы, и если режим бога НЕ включен, то
this.death()
// Метод death() возвращает все параметры
// на начальные, перезапускает игру
score.drawRecord()
// Метод объекта Score, записывает наш счет
// в рекорд, если текущий рекорд меньше
score.setToZero()
// Обнуляет счетчик очков
berry.randomPosition()
// Вызываем метод randomPosition(). Это метод
// объекта Berry, он переносим ягодку на новые
// случайные координаты
}
}
})
}
draw(context) {
// Функция рисования змейки. Сюда передается наш 2д контекст
// из класса Canvas
this.tails.forEach((el, index) => {
// Метод .forEach() перебирает все элементы массива
if (index == 0) {
// Если индекс элемента массива равен 0,
context.fillStyle = this.config.colorLight
// то красим в яркий цвет. Это голова
} else {
context.fillStyle = this.config.colorDark
// Иначе красим в темный цвет, это тело змейки
}
context.fillRect( el.x, el.y, this.config.sizeCell,
this.config.sizeCell)
// Метод fillRect() рисует прямоугольник по двум
// координатам и двум величинам, это размер ячейки
})
}
death() {
// Метод death() возвращает все параметры
// на начальные, перезапускает игру
this.x = 336
this.y = 480
// Ставит змейку на нужные координаты
this.dx = 0
this.dy = -this.config.sizeCell
// Выставляет напрвление движения
this.tails = []
// Обнуляем массив хвостов
this.maxTails = 1
// Делает параметр колическва хвостов равным 1
}
control() {
document.addEventListener("keydown", e => {
// Метод addEventListener() отслеживает события
// Первый аргумен указывает на тип события - кнопка нажата.
// Второй агрумент указывает на "слушателя", тот раздражитель
// который должен это событие вызвать и ту функцию которое
// это событие произведет. В нашем случае это будут кнопки
// клавиатуры
if (e.code == 'ArrowUp') {
// Если код слушателя равен клавише "Стрелка вверх" то
if (this.dx == 0 &&
this.dy == this.config.sizeCell) {
return
// Если сейчас прибавление координат идет по
// оси y, то ничего не делаем - return
// Эта проверка запрещает змейке начать
// двигаться в противоположную от текущей
} else {
this.dx = 0
this.dy = -this.config.sizeCell
// Иначе начинаем отнимать координаты
// от текущего положения головы змейки
// по оси y. Это заставит змейку двигаться
// вверх от текущих координат y
}
} else if (e.code == 'ArrowLeft') {
if (this.dx == this.config.sizeCell &&
this.dy == 0) {
return
} else {
this.dx = -this.config.sizeCell
this.dy = 0
// Для движения влево все тоже самое,
// но начинаем отнимать размер ячейки от
// оси x, не трогая значение y
}
} else if (e.code == 'ArrowDown') {
if (this.dx == 0 &&
this.dy == -this.config.sizeCell) {
return
} else {
this.dx = 0
this.dy = this.config.sizeCell
}
} else if (e.code == 'ArrowRight') {
if (this.dx == -this.config.sizeCell &&
this.dy == 0) {
return
} else {
this.dx = this.config.sizeCell
this.dy = 0
}
}
if (e.code == 'KeyG') {
// Если код слушателя равен клавише "G", то
if (this.config.godMode) {
// Если параметр godMode равен true (включен)
this.config.godMode = false
// То выключаем его, делая false
this.config.drawGodMode()
// Запускаем функцию смены цветов и надписей
// Интерфейс и змейка становятся оранжевыми
} else {
// Если параметр godMode равен false
this.config.godMode = true
// Включаем его, делая true
this.config.drawGodMode()
// Запускаем функцию смены цветов и надписей
// Интерфейс и змейка становятся красными
}
}
})
}
}Занятные детали компьютерной логики:
- Змейка никуда не движется и постоянно растет, каждый ход к ней прирастает новая голова и отваливаться одна клетка от хвоста.
gameLoop.js
Модуль отвечает за обновление игрового пространства. Сюда я добавил контроль нажатия клавиш, чтоб регулировать скорость движения змейки.
Значение gameLoop.js используются в game.js
import Config from './config.js'
export default class GameLoop {
constructor(update, draw) {
// В конструктор передаются методы из класса Game
this.update = update
// update() отвечает за изменения в игре
this.draw = draw
// а draw() за отрисовку на экране
this.config = new Config()
// Передаем данные Config
this.animate = this.animate.bind(this)
// animate будет содержать метод animate().
// Чтоб сохранить значение this, используем
// метод bind()
this.animate()
// Запускаем метод animate() она отвечает
// за непрерывное обновление игрового поля
this.control()
// Запускаем метод control() он отвечает
// за нажатие клавиш
}
animate() {
// Функция отвечает за непрерывный цикл внесения изменений
// в игру
requestAnimationFrame(this.animate)
// Вызывает повторно функцию animate()
// так образуется непрерывный цикл
if (++this.config.step < this.config.maxStep) {
return
// Если ++step меньше maxStep, то ничего не делать
// таким образом, надо 6 раз вызвать функцию,
// чтоб змейка сделала 1 шаг
}
this.config.step = 0
// После движения змейки, счетчик возвращается на 0
this.update()
// Вызывается функция update(), которая обновляет
// данные по змейке, ягодке и очкам
this.draw()
// Вызывается функция, которая запускает draw() у ягодки
// и змейки, предварительно отчистив игровое поле
}
control() {
// Функция отвечает за изменение скорости змейки.
// Чем выше значение maxStep, тем медленнее ползает змейка,
// я выбрал диапазон от 2 до 9
document.addEventListener('keydown', e => {
// Метод addEventListener() отслеживает события
// также как в snake.js
if (e.code == 'PageUp') {
if (this.config.maxStep > 2) {
// Если maxStep больше 2, то мы можем позволить
// себе уменьшить его значение
this.config.maxStep--
// Отнимаем 1 от maxStep
this.config.drawSpeed()
// Рисуем новое значение скорости
}
} else if (e.code == 'PageDown') {
if (this.config.maxStep < 9) {
// Если maxStep меньше 9, то мы можем позволить
// себе увеличить его значение
this.config.maxStep++
// Прибавляем 1 к maxStep
this.config.drawSpeed()
// Рисуем новое значение скорости
}
}
})
}
}game.js
import Canvas from './canvas.js'
import Snake from './snake.js'
import Berry from './berry.js'
import Score from './score.js'
import GameLoop from './gameLoop.js'
class Game {
constructor(container) {
this.canvas = new Canvas(container)
// Создается объект игрового поля,
// со значением ссылки на класс в HTML-файле
this.snake = new Snake()
// Создается объект змейки
this.berry = new Berry(this.canvas)
// Объект ягодки
this.score = new Score('#score', '#record', 0)
new GameLoop(this.update.bind(this), this.draw.bind(this))
// Создается объект GameLoop в аргументах которого передаютя
// container.update где container это значение в HTML-файле,
// а update это метод объекта.
// .bind(this) это метод, который передает методу update()
// нужное значение this, опять же значение в HTML-файле
}
update() {
this.snake.update(this.berry, this.score, this.canvas)
// Вызываем медод объекта Snake, передавая в него данные
// ягодки, счета и игрового поля
}
draw() {
// Функция отвечает за отрисовку всего на игровом поле
this.canvas.context.clearRect(0, 0,
this.canvas.element.width,
this.canvas.element.height)
// Метод .clearRect() очищает прямоугольную область пикселей.
// аргументы 0, 0, это начальные координаты, а ширина и высота
// игрового поля размер отчищаемой области
this.snake.draw(this.canvas.context)
this.berry.draw(this.canvas.context)
// Вызываем методы draw() у змейки и у ягодки, передавая
// туда свойства и функции 2D-контекста из Канваса
}
}
new Game(document.querySelector('.canvas-wrapper'))
// Это старт игры, создается объект Game.
// querySelector ищет значение в HTML.
// У нас это пустой <div> с классом .canvas-wrapperHTML & CSS
snake.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Snake</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/style.css">
<link rel="apple-touch-icon" sizes="180x180"
href="img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32"
href="img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16"
href="img/favicon-16x16.png">
</head>
<body>
<div id="game">
<div class="game-header">
<div class="game-score">
<span class="score-text">Speed:</span>
<span class="score-count" id="speed" >0</span>
</div>
<div class="game-score">
<span class="score-text">Score:</span>
<span class="score-count" id="score" >0</span>
</div>
<div class="game-score">
<span class="score-text">Record:</span>
<span class="score-count" id="record" >0</span>
</div>
<!--
Блоков game-score стало три, поэтому добавил
уникальные id, по которым теперь ориенируется
JavaScript вместо классов
--->
</div>
<div class="canvas-wrapper"></div>
<div class="game-footer">
<div class="keys-conf">
<span>Use the WASD keys</span>
</div>
</div>
<!--
Новый блок game-footer отвечает за текст под игровым полем.
Научил JS его менять в режиме бога
--->
</div>
<script src="js/game.js" type="module"></script>
</body>
</html>style.css
body {
margin: 0px;
font-family: sans-serif;
}
#game {
background: #353336;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#game-canvas {
display: block;
border-radius: 5px;
}
.game-header {
margin-bottom: 15px;
width: 708px;
height: 70px;
background: #5E3908;
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 10px;
}
.game-score {
padding: 10px 15px;
min-width: 160px;
height: 30px;
background: #121214;
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
}
.score-text {
font-size: 2vmin;
color: #EBEBEB;
}
.score-count {
font-size: 3vmin;
padding-left: 6px;
color: #EBEBEB;
font-weight: bold;
}
.canvas-wrapper {
background-color: #161618;
border-radius: 5px;
border: 10px solid #161618;
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg opacity='0.2'%3E%3Ccircle cx='8' cy='8' r='4' fill='%23525053'/%3E%3C/g%3E%3C/svg%3E%0A");
}
.game-footer {
padding-top: 1vmin;
padding-bottom: 1vmin;
display: flex;
justify-content: space-between;
}
.keys-conf {
width: 708px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
font-size: 2vmin;
color: #797979;
font-weight: regular;
}Основные изменения коснулись .game-header и .game-score, добавились классы .game-footer и .keys-conf.
Уверен как-то можно заменить уродскую url с svg-картинкой на нарисованную через css, если кто знает, напишите.
Наверх