#Программирование

Как написать 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 полученных очков, сохранение рекорда и прочая мишура, которая к основному алгоритму отношения уже не имеет.

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

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

Дозагрузка видео при слабой сети

45

Всем доброго утра!

Не далее как вчера товарищ @ZenitTTLMir1B писал о сложности загрузке видео при нестабильном соединении.

Я ночью всобачил на хостинг поддержку дозагрузки и отслеживания наличия сети.

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

Снова Алиас и немного про нейрокодинг

34

Если вы помните, некоторое время я писал о том, что решил запилить свою веб-версию игры "Алиас" при помощи нейросетей. После предыдущего поста, я доработал игру: добавил функционал присоединения по ссылке, возможность изменять ник в игровой комнате и зрителей как тип участников, сделал возможность менять настройки игры, после окончания сессии её можно перезапустить, ну и, конечно же, пополнил словарь слов (сейчас в нём более 1000 слов). В настоящий момент я не добавил динамические команды (больше двух), так как для этого придётся серьёзно переписывать всю логику игры и не настроил правильную передачу лидерства внутри команды. Однако, эксперимент, могу точно сказать, удался: я успешно создал вполне работающую игру с расширенным функционалом практически полностью полагаясь на нейросети. Да, конечно же, я принимал решения по логике игры и её архитектуре, давал комментарии относительно принимаемых ИИ решений (если видел, что он делает не то, что мне надо) и где-то даже вручную подправлял небольшие детали кода, но в целом это "нейроигра" и "нейрокод".

Сегодня, помимо хвастовства, я бы хотел поделиться с вами своим небольшим опытом. Я считаю, что программирование с помощью ИИ - это уже вполне обыденная реальность. Он гораздо лучше разбирает код, чем человек (по крайней мере, человек с такими навыками, как у меня), знает множество методов решения проблемы от примитивного говнокода (если сам об этом попросишь) до довольно технологичных решений. Как я писал в прошлый раз, знание кода всё же сильно приветствуется - просто для того, чтобы понимать что тебе предлагает нейросеть, особенно если ты вносишь какие-то правки с её помощью, но виртуозно владеть кодингом уже не нужно.

Теперь относительно доступных решений. Я уже ссылался на такой инструмент, как openrouter.ai - интерфейс, позволяющий работать сразу с несколькими ИИ, однако в последнее время я не очень им доволен. Появились ошибки, связанные с историей общения (периодически ИИ перестаёт понимать ход действий и начинает как бы с нуля), некоторые нейросети оказываются недоступными, хотя работают в их родном интерфейсе и т.п. В общем, пришлось временно от него отказаться и перейти к Qwen. Он показался мне более надёжным, чем DeepSeek - меньше ошибается, стабильнее работает. В его интерфейсе есть один неприятный недостаток: для вопроса нельзя загружать файлы, отличные от txt. То есть, все файлы js, php, sql приходится предварительно переименовывать. Openrouter мне в своё время понравился ещё и тем, что в нём такой проблемы нет. Бесплатная версия Qwen обрабатывает довольно большой (для моих задач) объём информации, хотя и в рамках кодинга отвечает с ощутимой задержкой. Также мне нравится Grok, но он без VPN недоступен, а сам VPN я могу подключить далеко не всегда.

Как именно общаться с нейросетью? На самом деле, вопрос без точного ответа. ИИ хорошо понимает семантику запроса, умеет оперировать синонимами и образами. Например, у меня есть иконка короны для отображения статуса лидера - это текстовый символ, но если я называю его "короной", Qwen понимает о чём речь. Если вместо "лидер" (от leader в коде) я напишу "ведущий", она также распознает какую роль я имею в виду. В то же время, нужно очень аккуратно строить запрос: если пытаться разжёвывать сети задачу как человеку, ты скорее запутаешь её, но в то же время примеры логики могут оказаться полезными.

Важный момент: при появлении ошибок, ты можешь описать ему эти ошибки простым языком - что не так в твоём понимании. Да, это не отменяет необходимости скидывать логи, ошибки из консоли, иногда сильно помогает указание на статусы в базе данных - всё это нужно ИИ также, как и человеку-программисту для лучшего понимания ситуации. Также возможна ситуация, когда нейросеть заходит в тупик и не может решить задачу, гоняя один код по кругу - особенно этим раньше страдала DeepSeek. В таком случае приходится самому принимать решение: просить добавить дополнительную отладку, внимательно следить за исполнением логики кода и т.п. И ещё одно наблюдение: утром, часов до 12 по МСК нейросети загружены заметно меньше, потом, видимо, просыпаются китайцы и время ответа сильно увеличивается.

И тут вы, наверное, спросите: неужто всё так идеально? На самом деле, нет. Помните, я написал, что так и не победил проблему с передачей лидерства? Над ней мы с ИИ трудимся уже несколько дней и я так и не могу добиться получения корректного работающего кода. Отчасти это связано с особенностями архитектуры кода - как раз в силу того, что я хотел получить более-менее понятные самому себе скрипты. Отчасти с тем, что это решение требует достаточно комплексной правки, которую за раз нейросеть осилить не может, а при нескольких итерациях она начинает путаться и вся игра в итоге ломается.

Если бы я занимался таким своеобразным кодингом более плотно, то, конечно же, взял бы платную версию - для неё меньше время ожидания в очереди, больше объём запроса и ответа, в некоторых случаях более актуальные базы. Вероятно, и баг с лидерами я бы тогда уже решил. Однако простые задачи можно решать и так. Это довольно интересное занятие, к тому же какой-никакой опыт я всё же получаю даже при таком подходе, ведь у сети всегда можно попросить объяснить конкретное решение.

P.S. Кстати, смену лидеров починил в итоге, так что игра вполне рабочая на данный момент.

Level Design в Unity

Вводный пост

36

Всем здравия!

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

План будет следующим:

  1. Препродакш
  2. Прототипирование
  3. Детализация
  4. Оптимизация
  5. Ландшафт
  6. Точки интереса
  7. Свет и цвет
  8. Звук

Всё постараюсь сделать с актуальными на сегодняшних день скринами.

Если есть вопросы, предложения, пожелания, пишите. Если чувствуете, что в моём списке не хватает каких-то важных тем, тоже не забудьте мне об этом сказать.

Первый пост вывалю завтра.

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

Как я "написал" веб-версию Алиаса

40

В последнее время у нас стало слишком много нейросетей, однако причины понятны: их алгоритмы становятся всё более прогрессивными, а функционал доступным. Нет никакого смысла не пользоваться достижениями технологий, особенно если они так просты, да ещё и во многих случаях бесплатны. Помимо вездесущих картинок, большой интерес для меня представляют текстовые сети в части написания кода. Уже достаточно долгое время я скармливаю различные задачи по доработке своего сайта Дипсику и Квену (они оба имеют бесплатную версию и доступны без VPN), но в этот раз решил попробовать что-то большое.

Если вы помните, некоторое время назад я закидывал удочку насчёт совместных сессий в умные игры, в частности Алиас. В принципе, сайт с таким функционалом уже есть, но мне неудержимо захотелось получить собственную версию, не завязанную на чужой хостинг и с тем функционалом, какой хочу лично я. При этом, если PHP я хотя бы немного знаю, а в JS фрагментарно ориентируюсь, в остальных веб-языках вообще ноль. Поэтому задачей было сделать игру максимально простым способом: чтобы я мог хотя бы примерно понимать структуру проекта и в случае чего указывать нейросети где искать ошибки. В итоге, вооружившись openrouter (он позволяет через единый интерфейс давать задачи сразу разным сетям), я приступил к проектированию.

Для "Алиаса" по сути чего-то особо сложного и ненужно: игра просто должна выводить слова на экран для ведущего, а его команда угадывает их. Но команды меняются и ведущие в командах тоже меняются - это первое условие. Время на объяснение нужно ограничить - это вторая деталь. Угаданные слова нужно считать и суммировать очки за них - три. Наконец, 20 очков и более - это победа. Кажется, всё просто, однако далее начинаются хитрые детали.

В базе нас должна быть возможность пропустить слово и перейти к следующему - тут я решил извратиться и сделать две кнопки, чтобы ведущий мог сразу отмечать верно ли угадано слово или пропустить его. Кроме того, логично, что у других игроков должна быть возможность не просто видеть историю слов, но и отмечать их "угадывание" в обратное направление - на случай если ведущий ошибся кнопкой или нарушил правила. Система при этом должна работать параллельно, поэтому над структурой базы данных для хранения пришлось подумать самостоятельно. Я решил использовать две основные таблицы: комнаты и история слов. В таблице комнат содержится основная информация - список участников, количество очков, статус. В словах - собственно, все слова за игровую сессию. Хранить их все до окончания игры (а не только за отдельный раунд) нужно ещё и для того, чтобы контролировать неповторяемость слов.

В ряде моментов нейросеть, помимо обычных багов (вроде аварийной остановки при написании ответа), просто начинала тупить и раз за разом выдавала ошибочное решение. Приходилось самому пытаться понять, что может быть не так, допиливать структуру базы и прямо указывать на возможные пути решения. Благо, ИИ хорошо справляется, если скармливать ему логи ошибок - тогда он сопоставляет их с кодом и находит решение. Также очевидно, что моих навыков архитектора не хватает: определённые вещи нужно планировать сразу, чтобы потом не переписывать большие куски кода, как это, например, было с функцией выбора команды (изначально игра автоматически распределяла игроков). Долго пришлось посидеть над функцией выявления победителя, ведь важно не просто автоматически завершать игру как только одна команда достигнет 20 очков: нужно дать возможность завершить раунд обеим командам (в любом случае, если 20 и более набирает первая, у второй должен быть шанс отыграть свой). Сейчас игра определяет победителя после полного круга и даже умеет учитывать ничью.

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

В итоге с перерывами на работу на создание работающего прототипа с большинством хотелок ушло около 3 дней. Результат вот он: https://alias.onlyfox.ru/alias.html

Есть в текущем "билде" места, которые нужно допилить. Пока что не реализована возможность подключаться к уже идущей игре, нужно предусмотреть создание больше трёх команд, дать возможность настраивать параметры комнаты (время на ответ, количество победных очков и т.п.). Но опыт в любом случае интересный: и в плане построения логики игры, и в плане работы с нейросетями. Я использовал их бесплатные версии с ограничением на длину ответа и количеством обрабатываемой информации, поэтому задания нужно выдавать кусками, а это в свою очередь означает, что сразу всё не напишешь, но думаю, в платной версии тоже всю игру за раз сеть не выдаст. В любом случае, результат есть и, что самое главное, на его примере понятно, что уже сейчас ИИ способна реализовывать хотелки даже таких профанов в коддинге, как я.

Преобразовываем PDF в книгу (буклет/брошюру)

37

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

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

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

Что ж, как и любой бесплатный аналог платного ПО, моя программа просто обязана была иметь убогий интерфейс. Я пошёл дальше, и решил, что не буду делать его вовсе. Это консольное приложение. Но мне искренне было впадлу рисовать кнопочки и поля, а потом писать под них обработчики, когда нам надо знать всего три параметра для работы.

1 параметр обязательный, второй и третий можно опустить, тогда начинать программа будет с первой страницы и верстать по 4 листа (16 страниц) в одной тетради.

А для первого параметра в винде есть "киллер фича": клацаем правой кнопкой мыши по файлу с зажатым при этом шифтом и там вот такая вот строчка будет

Пара скриншотов из цикла было-стало

С итоговым файлом нужно поступить следующим образом: просто отправить его на печать, поставив галочку двусторонней печати. Можно хоть на обычном принтере домашнем, хоть пойти в ближайшую типографию. Двусторонняя печать и вы получаете набор готовых к сшиванию тетрадей.

Сделал программку только для ПДФок, так как это универсальный формат для печати. В Ворде уже есть удобный встроенный инструмент брошюрования, а любой другой формат можно быстро перегнать в тот же ПДФ при желании.

Собстна, ссылка, кому вдруг надо https://disk.yandex.ru/d/kSXRul0yiX4pOQ

Ну а если кто-то найдёт какой-то баг, не стесняйтесь, пишите тут или мне на почту: nick(собака)variussoft.ru

P.S. Ах,да, забыл упомянуть. Эта программка собрана под дот нет кор, так что работать должна и на линуксах(но я не пробовал)

Источник:

Эксперименты с Горячим

Ну и лучшим заодно

56

Всем доброго утра, дорогие друзья!

Как вы могли заметить, а если нет, то я вам рассказываю, вкладка Лучшее у нас показывает что-то не совсем поддающееся логике.

А всё по тому, что никакого Лучшего у нас ещё нет, мы ещё думаем над алгоритмом, и параллельно ведём споры о том, как должно работать горячее

Статистика
Статистика

Из чего некоторые из членов команды решили, что сейчас порог слишком высокий, поэтому я изменил формулу рассчёта и 18, которые стояли сегодня утром, превратились в 16.

Будем ещё наблюдать, возможно, математика ещё изменится.

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

Не будьте равнодушны)

Dangeon Warder Panel

Вступительная часть

53

Всем доброго здравия, дорогие друзья!

Этим постом я начинаю серию публикаций о том, как я разрабатываю систему для ведения интерактивный НРИ. В тегах указана пятая редакция Подземелий, но на самом деле речь о любой НРИ. Привязки к конкретным правилам нет, подразумевается, что все необходимые действия (типа бросков кубиков) игроки будут совершать ручками за столом. А программа нужна только для визуализации перемещений, отображения карт и создания показательных боевых ситуаций. Ну и автоматическая выдача опыта, но это опционально и об этом потом.

И так, начнём с самых азов. На чём всё это безобразие будет работать?
На Unity. Почему? Да потому что на ней достаточно дёшево реализуется общение с ГПУ, а оно нам тут понадобится.

Полностью рассказывать о том, как и что я сделал смысла не вижу, но буду делиться какими интересными выкладками и, возможно, кусками кода, если они кому-то нужны.

Что мы сейчас имеем:
1. Система приключений
2. Внутри каждого приключения есть система игровых сессий
3. Внутри сессий и приключений есть возможность редактирования базы НИП, заданий, предметов и локаций

Кстати, UI в окне приключения реализован, как мне кажется, достаточно элегантно: он строится динамически, и в него легко буквально одной строчкой можно добавить новые разделы, которые сразу же отрисуются и кнопочкой и панелькой.

Для этого у меня создан класс SectionsManager, который висит в памяти приложения как Singleton. Он хранит в себе информацию о том, какие разделы мы будем использовать. Добавлять их туда можно через конфиги или через код, это уже вопрос того, кому как удобно.
А потом есть код, отвечающий чисто за UI и он ждёт от этого менеджера команду, когда тот всю необходимую информацию соберёт и скажет, что вот сейчас уже можно рисовать

private void Awake()
{
    SectionsManager.GetInstance().IsReadyToRender += Render;
}

private void Render()
{
    Sequence seq = DOTween.Sequence();
    foreach (IMenuSection section in SectionsManager.GetInstance().Sections) {
        SectionButton button = Instantiate(sectionButtonPrefab, transform);

        RectTransform rt = button.GetComponent<RectTransform>();
        CanvasGroup cg   = button.GetComponent<CanvasGroup>();
        
        cg.alpha = 0f;
        button.transform.localScale = Vector3.zero;

        button.Init(section.GetName(), () => { section.Show(transform.parent); });

        seq.AppendCallback(() =>
        {
            cg.DOFade(1f, 0.3f).SetEase(Ease.OutQuad);
            button.transform.DOScale(1f, 0.4f).SetEase(Ease.OutBack);
        });

        seq.AppendInterval(0.1f);
    }
}

Конечно, каждую секцию нужно предварительно настроить и сохранить как префаб, совсем магии тут нет, но зато управление такими секциями становится максимально простым. Не захочет мастер пользоваться функционалом заданий, зачем ему занимать память приложения и заграмождать интерфейс ненужным функционалом. Он его отключит, приложение его выгрузит и забудет до тех пор, пока Мастер снова не захочет им воспользоваться.

Понятия не имею, интересно ли это вообще хоть кому-то, но я искренне надеюсь, что среди пользователей сайта есть люди неравнодушные к программированию, геймдеву и НРИ.

Посему жду комментариев, вопросов, критики, чего угодно.

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