Приветствую, в этой статье мы сделаем игру «Слоты» внутри названия сайта(пример, TitleRun). Механика игры будет очень простая. нажимаешь на кнопку — получаешь случайные слоты, если все слоты совпадают — выигрываешь.

image

Зачем вообще делать игры в названии сайта?


Да, возможно это и звучит как нечто совсем странное, но, всё же, это довольно интересно. Во всяком случае, интереснее, чем делать игру обычным способом.

Анимации


Анимации можно реализовать разными способами:

setInterval
function animation() {
    let i = 0;
    setInterval(() => {
        document.title = `Таймер - ${i}`;
        i++;
    });
}


setTimeout
let timer = 0;
function animation() {
    document.title = `Таймер - ${timer}`;
    timer++;
    setTimeout(animation, 1000)
}


while и await
Этот способ был в
исходниках TitleRun`a

function sleep(ms) {
    return new Promise((r) => setTimeout(r, ms));
}
async function animation() {
    let i = 0;
    while(true) {
        document.title = `Таймер - ${i}`;
        await sleep(1000);
    }
}


Механика


Как слоты я буду использовать эмодзи(смайлики, emoji, как вам удобнее):

image

Вот так я смогу получать случайные слоты:

const getSlot = () => {
    const slot = Math.ceil(Math.random()*slots.length-1);
    return slots[slot];
};
const getSlots = () => {
    return slots.map(getSlot);
};

При нажатии на кнопку, будет анимация разных слотов, скорость которой будет постепенно увеличиваться:

const spin = document.getElementById('spin');
const slotsAnimation = async () => {
    let speed = 20;
    while (true) {
        //получаю слоты
        const slotsRandom = getSlots();
        //вывожу
        document.title = slotsRandom.join('');
        if(speed >= 1000) {
            checkWin(slotsRandom);
            break;
        }
        speed += 50;
        await sleep(speed);
    }
};
spin.addEventListener('click', StartGame);

checkWin — функция для проверки выигрыша:

const checkWin = (slots) => {
    for(let i=0;i<slots.length-1;i++) {
        //Если слот не совпадёт со следующим
        if(slots[i] !== slots[i+1]) {
            alert('GAME OVER');
            return;
        }
    }
    alert('WIN!!');
};

Теперь это уже играбельно.

Но я хотел бы добавить больше анимаций(например, анимации выигрыша и главного меню)
Это можно сделать, если ввести глобальные переменные, типа:

let playing = false;
let win = false;

и на основе их показывать анимации:

const slotsAnimation = async () => {
    playing = true
    let speed = 20;
    while (playing) {
        const slotsRandom = getSlots();
        document.title = slotsRandom.join('');
        if(speed >= 1000) {
            checkWin(slotsRandom);
            playing = false
        }
        speed += 50;
        await sleep(speed);
    }
};
const winAnimation = async () => {
    win = true;
    while(win && !playing) {
        document.title = 'WIN!*';
        await sleep(1000);
        document.title = '*WIN!';
        await sleep(1000);
    }
};
//Эта анимация будет просто циклично дублировать слоты
const mainAnimation = async () => {
    playing = false
    let i = 0;
    while (!playing && !win) {
        if(i === slots.length - 1) i = 0;
        else i++;
        document.title = slots[i].repeat(slots.length);
        await sleep(1000);
    }
};

Делаем удобнее


Первое, что я хотел бы сделать — функция, которая заменит логику winAnimation. Она будет выводить сообщение и после запускать другую функцию. В качестве аргумента она будет принимать объект с текстов, кол-вом итераций, скоростью и колбеком.

const message = async ({text = [], count = 'infinity', speed = 1000, callback = null}) =>  {
    if(!text[0]) throw new Error('need array for text');
    let i = 0;
    let textIndex = 0;
    while(true) {
        if(count <= i && count !== 'infinity') {
            // в конце вызовем колбек, если он есть
            if(callback) callback();
            break;
        }
        if(textIndex >= text.length) textIndex = 0;
        //За каждую итерацию цикла будем показывать 1 сообщение из массива сообщений
        document.title = text[textIndex];
        i++;
        textIndex++;
        await sleep(speed)
    }
};
// Использование
message({text: ['?WINNER', 'WINNER?'], count: 3, speed: 800, callback: mainAnimation})


Ещё я хотел бы сделать менеджер анимаций, с помощью которого можно будет их запускать и контролировать. Это будет класс с хранилищем анимаций и несколькими методами.
class Animations {
    animations = [];
    setAnimation(animation, args = {}) {
        // Выключаем все анимации
        for(const item of this.animations) {
            item.play = false
        }
        // Находим анимацию, включаем и запускаем функцию
        const Animation = this.animations.find(item => item.name === animation);
        if(!Animation) return;
        Animation.play = true;
        Animation.callback(args)
    };
    isPlay(animation) {
        return this.animations.find(item => item.name === animation).play
    }
    createAnimation(name, callback) {
        this.animations.push({
            play: false,
            name,
            callback,
        })
    }
}

Описание методов:
setAnimation — вызывает анимацию.
isPlay — функция-условие для проверки «играет» ли анимация.
createAnimation — добавляет анимацию в хранилище.

Теперь код будет выглядеть вот так:

const checkWin = (slots) => {
    for(let i=0;i<slots.length-1;i++) {
        if(slots[i] !== slots[i+1]) {
            alert('GAME OVER');
            animations.setAnimation('main');
            return;
        }
    }
    alert('WIN!!');
    animations.setAnimation('message',
        {text: ['?WINNER', 'WINNER?'], count: 3, speed: 800, callback: () => animations.setAnimation('main')})
};
const slotsAnimation = async () => {
    let speed = 20;
    while (animations.isPlay('game')) {
        const slotsRandom = getSlots();
        document.title = slotsRandom.join('');
        if(speed >= 1000) {
            checkWin(slotsRandom);
        }
        speed += 50;
        await sleep(speed);
    }
};
const mainAnimation = async () => {
    let i = 0;
    while (animations.isPlay('main')) {
        if(i === slots.length - 1) i = 0;
        else i++;
        document.title = slots[i].repeat(slots.length);
        await sleep(1000);
    }
};
// Добавление анимаций
animations.createAnimation('main', mainAnimation);
animations.createAnimation('game', slotsAnimation);
animations.createAnimation('message', message);
// Запуск
animations.setAnimation('main');

Возможность добавить слот


Для разнообразия добавим возможность добавить случайный слот.
(Тут мы просто берём emoji из API, добавляем в массив и включаем главную анимацию)

const AddSlot = async () => {
    // Показываем сообщение пока ждём ответа от API
    animations.setAnimation('message', {text: ['Getting slot','Getting slot'], speed: 500});
    try {
        const data = await fetch(`https://emoji-api.com/emojis?access_key=КЛЮЧ`);
        const body = await data.json();
        const emoji = await body[Math.floor(Math.random() * body.length -1)];
        slots.push(emoji.character);
    } catch (e) {
        // Если не получилось
        console.log(e);
        animations.setAnimation('none');
        document.title = 'Error, try again';
        await sleep(800);
    } finally {
        animations.setAnimation('main');
    }
};

Вот и всё. Надеюсь вы узнали что-нибудь новое.

Репозиторий на GitHub — ссылка.
Сайт-пример — ссылка.