#[Сокровищница]

Кольцо Эйнштейна

38

Массивная эллиптическая галактика так сильно исказила свет далёкой голубой галактики, что та превратилась в почти идеальное кольцо — кольцо Эйнштейна

Объект LRG 3-757, впервые замеченный в 2007 году и позже снятый «Хабблом», может скрывать огромную чёрную дыру массой около 36 миллиардов солнц.

Как написать Tetris

Gamedev для самых маленьких

41

Вчера я остро ощутил, что очень давно не писал ничего простого, но функционального и интересного. А ещё я тут всё про игры да про игры, поэтому решил: пора!

Последнее, что я писал подобного – это змейку, которую можно было наблюдать во время обновлений сайта раньше (помните её?).

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

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

Начнём с простого: нам понадобится текстовый редактор любой. Хорошо, если с подсветкой синтаксиса. Для самого простого можно взять Notepad++, для случаев чуть серьёзнее Атом или ВС код.

Создаём html страничку, она будет нашей точкой входа, рядом с ней создаём файлик стилей и основной наш скриптец. Забегая вперёд: стили нам не то, чтобы прям сильно нужны.

Дожидаемся, пока вся страница загрузится, потом инициируем игру

document.addEventListener('DOMContentLoaded', () => {
    initGame();
});

Что в initGame?

const initGame = () => {
    field = new GameField(10, 20);

    canvas.width = canvas.offsetWidth;
    canvas.height = canvas.offsetHeight;
    recordEl.innerText = bestScore;

    resetGame();
};

Очевидно, что canvas, recordEl, field - это некие переменные, который мы где-то объявляем.

Это делается в самом начале скрипта. Там же получаем и экранные кнопки.

const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const recordEl = document.getElementById('record');
const restartBtn = document.getElementById('btn-restart');
const gameShell = document.getElementById('gameShell');
const gameOverOverlay = document.getElementById('gameOverOverlay');
const gameOverlayTitle = gameOverOverlay?.querySelector('strong');

const btnUp = document.getElementById('btn-up');
const btnDown = document.getElementById('btn-down');
const btnLeft = document.getElementById('btn-left');
const btnRight = document.getElementById('btn-right');

Пока что к самому алгоритму игры мы не приступили, это всё предварительные ласки.

В первую очередь давайте посмотрим на класс GameField.

class GameField {
    constructor(width, height) {
        this.width = width;
        this.height = height;
        this.blocks = [];
        for (let i = 0; i < this.height; i++) {
            let line = [];
            for (let j = 0; j < this.width; j++) {
                line.push(0);
            }
            this.blocks.push(line); //Заполняем массив ячеек поля нулями. поле по умолчанию пустое
        }
    }

    checkLines = async () => { // тут мы проверяем, а есть ли линии, которые надо сбросить
        for (let y = this.height - 1; y >= 0; y--) {
            if (this.currentLineIsFill(this.blocks[y])) {
                currentScore++; //плюсуем очки

                for (let x = 0; x < this.width; x++) {
                    this.blocks[y][x] = 0;
                    redraw();
                    await sleep(LINE_CLEAR_ANIMATION_DELAY); // это просто для красивой анимации исчезновения
                }
            }
        }
        currentScore = getComboPoints(currentScore); // этот метод отдельно, там считаем прирос за комбо
    }

    currentLineIsEmpty = (line) => {
        for (let x = 0; x < line.length; x++) {
            if (line[x] === 1) { // если хоть одна ячейка заполнена, то идёт нахер
                return false;
            }
        }

        return true;
    };

    currentLineIsFill = (line) => {
        for (let x = 0; x < line.length; x++) {
            if (line[x] === 0) { // если хоть одна ячейка пустая, строка идёт нахер
                return false;
            }
        }

        return true;
    };

    moveLines = () => { // сдвигаем после удаления
        let notEmptyLines = [];
        for (let y = 0; y < this.blocks.length; y++) {
            if (!this.currentLineIsEmpty(this.blocks[y])) {
                notEmptyLines.push([...this.blocks[y]]); // сначала заполняем массив непустымы строками
            }
        }

        let emptyLine = [];
        for (let x = 0; x < this.width; x++) {
            emptyLine.push(0); // потом досоздаём массив пустых строк
        }

        let newLines = []; // запиххиваем их в новый массив
        for (let x = 0; x < this.height - notEmptyLines.length; x++) {
            newLines.push([...emptyLine]);
        }
        for (let x = 0; x < notEmptyLines.length; x++) {
            newLines.push([...notEmptyLines[x]]);
        }

        this.blocks = newLines;
    }
}

Когда же мы будем всё это дёргать? И почему?

В методе, который описывает игровой цикл. Я постарался программировать так, чтоб даже при современном подходе можно было бы реализовать близкий по сути алгоритм на реальном камне, где, как известно многопоточность лишь выдуманная.

const resetGame = () => {
    clearTimeout(tickTimeout);

    field = new GameField(10, 20);
    currentFigure = null;
    nextFigure = new Figure();
    nextFigure.fillRandom();

    score = 0;
    currentScore = 0;
    gameMode = GameMode.PLAYING;
    isSoftDropping = false;
    isLoopRunning = false;

    scoreEl.innerText = '0';
    setGameOverView(false, 'Game Over', false);
    redrawNextFigure();
    redraw();

    tickTimeout = setTimeout(gameLoop, getCurrentTickDelay()); // <-вот он наш злодей
};

Раз в какое-то количество миллисекунд мы вызываем геймЛуп. «Почему же не сетинтервал», – спросит неокрепших неофит. А всё по тому, что это чревато как раз внезапной неконтролируемой асинхронностью, которая приведёт к гонке за ресурсы и будут беды. Поэтому метод будет вызывать сам себя только после того, как точно закончит.

const gameLoop = async () => {
    if (gameMode !== GameMode.PLAYING || isLoopRunning) {
        return;
    }

    isLoopRunning = true;

    if (currentFigure === null || typeof currentFigure === 'undefined') { // у нас нет фигуры? Так давай её сделаем из того, что стоит в очереди
        currentFigure = new Figure();
        currentFigure.cells = nextFigure.cells.map(row => [...row]);
        currentFigure.type = nextFigure.type;
        currentFigure.states = nextFigure.states.map(state => state.map(row => [...row]));
        currentFigure.rotationState = nextFigure.rotationState;
        currentFigure.setStartPosition();
        nextFigure.fillRandom(); // следующую фигуру херакнули в какую-нибудь новую рандомную

        redrawNextFigure();
    }

    currentFigure.moveDown(); //уронили я одну клеточку
    if (currentFigure.checkCollision()) {//пересеклисьс чем-нибудь? 
        currentFigure.moveUp(); //подняли обратно и зафиксировали с мировом пространстве
        const overflow = currentFigure.placeToField();
        currentFigure = null;

        if (overflow) {
            finishGame('top_out');
            redraw();
            isLoopRunning = false;
            return;
        }

        await field.checkLines(); // все фигуры на своих местах, можно проверить, как там у нас дела
        score += currentScore;

        scoreEl.innerText = score;

        currentScore = 0;

        field.moveLines(); // сдвинем, если надо сдвинуть
    }

    redraw();

    isLoopRunning = false;

    if (gameMode === GameMode.PLAYING) {
        tickTimeout = setTimeout(gameLoop, getCurrentTickDelay()); // снова запускаем всё сначала
    }
};

В целом, осталось только понять, что же такое фигура.

Сама геморная часть. Там больше всего буковок.

Фигура - это сущность, в которой хранится информация о том, какой она формы и как её крутить.

class Figure {
    constructor() {
        this.cells = [
            [0],
        ];
        this.x = 2;
        this.y = -5;
        this.rotationState = 0;
    }

    moveDown = () => {
        this.y++;
    };

    moveUp = () => {
        this.y--;
    };

    moveLeft = () => {
        this.x--;
        if (this.checkCollision()) {
            this.x++;
        }
    }

    moveRight = () => {
        this.x++;
        if (this.checkCollision()) {
            this.x--;
        }
    }

    fall = () => {
        while (!this.checkCollision()) {
            this.y++;
        }
        this.moveUp();
    };

    checkCollision = () => {
        for (let i = 0; i < this.cells.length; i++) {
            for (let j = 0; j < this.cells[i].length; j++) {
                let cellX = j + this.x;
                let cellY = i + this.y;

                if (this.cells[i][j] === 0) {
                    continue;
                }

                // Проверяем границы по X всегда (независимо от Y)
                if (cellX < 0 || cellX >= field.width) {
                    return true;
                }

                // Для клеток выше видимой области не проверяем коллизии с полем
                if (cellY < 0) {
                    continue;
                }

                // Проверяем нижнюю границу и коллизии с заполненными клетками
                if (cellY >= field.height) {
                    return true;
                }

                if (field.blocks[cellY][cellX] === 1) {
                    return true;
                }
            }
        }
        return false;
    };

    rotate(withCollisions = true) {
        const from = this.rotationState;
        const to = (from + 1) % this.states.length;
        const key = `${from}>${to}`;
        const kickSet = (this.type === "I")
            ? SRS_KICKS.I[key]
            : (this.type === "O" ? SRS_KICKS.O[key] : SRS_KICKS.JLSTZ[key]);

        const originalX = this.x;
        const originalY = this.y;

        let rotated = this.states[to];

        if (!withCollisions) {
            this.cells = rotated;
            this.x = originalX;
            this.y = originalY;
            this.rotationState = to;
            return;
        }

        for (const [dx, dy] of kickSet) {
            this.cells = rotated;
            this.x = originalX + dx;
            this.y = originalY - dy;
            if (!this.checkCollision()) {
                this.rotationState = to;
                return;
            }
        }

        this.cells = this.states[from];
        this.x     = originalX;
        this.y     = originalY;
    }

    placeToField = () => {
        let overflow = false;

        for (let i = 0; i < this.cells.length; i++) {
            for (let j = 0; j < this.cells[i].length; j++) {
                let cellX = j + this.x;
                let cellY = i + this.y;

                if (this.cells[i][j] === 0) {
                    continue;
                }

                if (cellY < 0) {
                    overflow = true;
                    continue;
                }

                if (cellY >= field.height || cellX < 0 || cellX >= field.width) {
                    overflow = true;
                    continue;
                }

                field.blocks[cellY][cellX] = this.cells[i][j];
            }
        }

        return overflow;
    };

    setStartPosition = () => {
        this.x = Math.floor(field.width / 2) - Math.floor(this.cells[0].length / 2);

        if (this.type === "I" && this.rotationState === 1) {
            this.y = -2;
        } else if (this.type === "I") {
            this.y = -4;
        } else if (this.type === "O") {
            this.y = -2;
        } else if (this.rotationState === 3) {
            this.y = -2;
        } else {
            this.y = -3;
        }
    };

    fillRandom = () => {
        let figure = FIGURES[Math.floor(Math.random() * FIGURES.length)];

        this.cells         = figure.figure.states[0].map(row => [...row]);
        this.states        = figure.figure.states.map(state => state.map(row => [...row]));
        this.type          = figure.type;
        this.rotationState = 0;

        let rotateSteps = Math.floor(Math.random() * 4);
        for (let i = 0; i < rotateSteps; i++) {
            this.rotate(false);
        }
    };
}

Тут, конечно, кода дофига и надо объяснить, что тут происходит. Суть в том, что поворот в тетрисе – это прям отдельная задачка. Я её для себя упростил максимально, создав «спрайты» фигур во всех положениях заранее.

const FIGURES = [
    {
        type: "I",
        figure:
            {
                states: [

                    [
                        [0, 1, 0, 0],
                        [0, 1, 0, 0],
                        [0, 1, 0, 0],
                        [0, 1, 0, 0],
                    ],
                    [
                        [0, 0, 0, 0],
                        [1, 1, 1, 1],
                        [0, 0, 0, 0],
                        [0, 0, 0, 0],
                    ],
                ]
            }
    },
    {
        type: "T",
        figure:
            {
                states: [
                    [
                        [0, 1, 0],
                        [0, 1, 1],
                        [0, 1, 0],
                    ],
                    [
                        [0, 0, 0],
                        [1, 1, 1],
                        [0, 1, 0],
                    ],
                    [
                        [0, 1, 0],
                        [1, 1, 0],
                        [0, 1, 0],
                    ],
                    [
                        [0, 1, 0],
                        [1, 1, 1],
                        [0, 0, 0],
                    ],
                ]
            }
    },
    {
        type: "S",
        figure:
            {
                states: [
                    [
                        [0, 1, 0],
                        [0, 1, 1],
                        [0, 0, 1],
                    ],
                    [
                        [0, 0, 0],
                        [0, 1, 1],
                        [1, 1, 0],
                    ]
                ]
            }
    },
    {
        type: "Z",
        figure:
            {
                states: [
                    [
                        [0, 0, 1],
                        [0, 1, 1],
                        [0, 1, 0],
                    ],
                    [
                        [0, 0, 0],
                        [1, 1, 0],
                        [0, 1, 1],
                    ]
                ]
            }
    },
    {
        type: "L",
        figure:
            {
                states: [
                    [
                        [0, 1, 0],
                        [0, 1, 0],
                        [0, 1, 1],
                    ],
                    [
                        [0, 0, 0],
                        [1, 1, 1],
                        [1, 0, 0],
                    ],
                    [
                        [1, 1, 0],
                        [0, 1, 0],
                        [0, 1, 0],
                    ],
                    [
                        [0, 0, 1],
                        [1, 1, 1],
                        [0, 0, 0],
                    ]
                ]
            }
    },
    {
        type: "J",
        figure:
            {
                states: [
                    [
                        [0, 1, 1],
                        [0, 1, 0],
                        [0, 1, 0],
                    ],
                    [
                        [0, 0, 0],
                        [1, 1, 1],
                        [0, 0, 1],
                    ],
                    [
                        [0, 1, 0],
                        [0, 1, 0],
                        [1, 1, 0],
                    ],
                    [
                        [1, 0, 0],
                        [1, 1, 1],
                        [0, 0, 0],
                    ]
                ]
            }
    },
    {
        type: "O",
        figure:
            {
                states: [
                    [
                        [1, 1],
                        [1, 1]
                    ]
                ]
            }
    },

];

А ещё есть такая штука как SRS – это прям общепринятый стандарт вращения фигур. Специальные таблицы описывают как необходимо проверять смещение фигур в пространстве игрового поля при переходе из одного состояния в другое, на случай столкновения со стенами или существующими блоками в момент вращения. Я, опять-таки, эту часть упростил максимально, вырезав очень много из стандарта, так как у меня, как минимум, нет вращение против часовой стрелки.

const SRS_KICKS = {
    JLSTZ: {
        "0>1": [[0, 0], [-1, 0], [1, 0], [-1, 1], [1, 1]],
        "1>0": [[0, 0], [0, -1]],
        "1>2": [[0, 0], [0, -1]],
        "2>3": [[0, 0], [0, -1], [-1, 0], [1, 0], [1, -1]],
        "3>0": [[0, 0], [-1, 0]],
    },
    I: {
        "0>1": [[0, 0], [-1, 0], [1, 0], [-2, 0], [2, 0]],
        "1>0": [[0, 0], [0, 1], [-1, 0], [0, 2], [-2, 2]],
    },
    O: {
        "0>0": [[0, 0]]
    }
};

Логика такая: после поворота к фигуре применяются смещения по иксу и игрику по очереди из массива, сначала 0-0 (не смещается), потом, к примеру 0-1 и так далее. За идеальное состояние, которое в данный момент всех устраивает, применяется то, после которого проверка коллизии фигуры показывает, что никто ни с кем не столкнулся. Если ни один из вариантов не подошёл, значит поворот не случился.

Классические таблицы подразумевают матрицы фигур одинакового размера и квадратные по своей сути. Я тут тоже отошёл от стандарта,

Как это в итоге играется

По коду там есть у меня есть усложнение с увеличением скорости падения фигур за каждые 30 полученных очков, сохранение рекорда и прочая мишура, которая к основному алгоритму отношения уже не имеет.

Как грицца: понятно, что нифига не понятно, так что спрашивайте, господа и дамы, отвечу на недостающие вопросы)

Всем хороших игр!

Гора Арсия

42

Этот снимок был получен аппаратом Марс Одиссей. Это - гора Арсия, один из высочайших вулканов Марса и одна из самых высоких гор Солнечной системы. Она поднимается на высоту почти 19 километров. Кальдера вулкана, видимая на снимке, имеет диаметр более 100 км

Отгадай-ка

49

В соцсетях шутят о том, что любимый ребенок в семье - это «икс child», потому что его имя используется в иксе. Что скрывает "икс"?

UPD @Rian дал верный ответ. password child.

Эти морозы -20 днем. Ноги замёрзли

44

Здравствуйте.

Не помогли портянки войлочные в рабочих кирзовых сапогах. Хотя было тепло -20 в тени , а на солнце -16. Однако ноги начали замерзать. От этого потом усталость появилась.

Руки у нас замерзали. Однако я опытный... было две пары перчаток. Не собираюсь экономить на своих руках как некоторые. Лекарства стоят дороже!..

Баф помогал очень хорошо. Закрывая нос и щеки. Можно надышать там ртом тепло и радоваться . После баф намокает , но его можно повернуть сухой строй к себе. Так становится теплее.. Зайдя в помещение он высыхает за пару часов.

_____

На веле быстрее чем пешком. Менее замерзать и надёжнее чем на машине. Даже у магазина не все могут завести машину. Морозы!

Шипы на месте. Хорошо вцепляются в лёд и снег твердый. Пока некогда их подрезать . Надеюсь успею до апреля 😒🤣.

Заднее колесо без шипов. Хватает токо переднего шипованного. В основном переднее колесо на льду выбивает из рук. Заднее колесо более устойчиво.

Зимой плавное руление с плавной скоростью = кости целее... И да утепляйте сиденья на веле ( хоть М или Ж велосипедисты)

_____

Завтра будет холоднее нам и сложнее. Досадую, что не купил валенки. Буду искать себе их по доступной цене.

Продолжение следует...

Мясной хлеб

НЕ ленивая кухня. Фотофокс

39

Всем привет!
Раз уж у нас тема фотофокса "Мясо", то надо что-нибудь интересненькое приготовить, да?

Например, мясной хлеб
Например, мясной хлеб

Идем в магазин и скупаем все по рецепту

Ингредиенты
1. Свиной фарш (хрюкофокс). У меня 1200 грамм вышло
2. Батон хлеба
3. Молоко
4. Лук
5. Чеснок
6. Яйцо - 2 шт
7. Масло растительное
8. Горчица
9. Кетчуп
10. Приправы
11. Пара форм для запекания
Самое сложное не съесть до начала приготовления...
Самое сложное не съесть до начала приготовления...
Выковыриваем из булки всю мякоть и заливаем ее молоком. Оставляем
Выковыриваем из булки всю мякоть и заливаем ее молоком. Оставляем
Чистим и нарезаем одну луковицу. Я по приколу смешал лук двух цветов
Чистим и нарезаем одну луковицу. Я по приколу смешал лук двух цветов
Жарим
Жарим
Натираем чеснок. Вот эта малюсенькая терочка вполне достойна пары слов и занесения в Ленивую кухню. Легко моется, маленькая, натереть чеснок или сыр одна минута.
Натираем чеснок. Вот эта малюсенькая терочка вполне достойна пары слов и занесения в Ленивую кухню. Легко моется, маленькая, натереть чеснок или сыр одна минута.
Вытаскиваем мякиш и выжимаем его
Вытаскиваем мякиш и выжимаем его
Смешиваем все ингредиенты. Фарш+мякиш+лук+чеснок+горчица+два яйца+базилик. Солим. Я солил смесью соли с приправами половину столовой ложки.
Смешиваем все ингредиенты. Фарш+мякиш+лук+чеснок+горчица+два яйца+базилик. Солим. Я солил смесью соли с приправами половину столовой ложки.
Перемешиваем
Перемешиваем
Фарш плотно укладываем в смазанные растительным маслом формы. Сверху каким-нибудь подсобным предметом формируем "разрезы".
Фарш плотно укладываем в смазанные растительным маслом формы. Сверху каким-нибудь подсобным предметом формируем "разрезы".

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

Заталкиваем это все в духовку на 180 градусов. У меня вышло 1 час 10 минут до готовности. Делал на своей любимой конвекции, опасался, что верх поджарится, но нет.

Фарш дал сок и мясо наполовину варилось, наполовину жарилось. Ножницами разрезаем и убираем формы.
Фарш дал сок и мясо наполовину варилось, наполовину жарилось. Ножницами разрезаем и убираем формы.
Мммм. Смотрится обалденно! Ждем когда остынет...
Мммм. Смотрится обалденно! Ждем когда остынет...
... и нарезаем!
... и нарезаем!

Консистенция очень нежная и вкуснааааааая!!!