Сегодня увидел на Хабре статью Создание игры с помощью LLM более полугода назад и этой же игры теперь. Что изменилось?. Не то чтобы я жаждал программировать с использованием больших языковых моделей (ака чатботы, ака нейросети, ака LLM), но сами их возможности, которые быстро растут (а также растет их доступность), впечатляют. Поэтому захотелось взглянуть, что там за эксперимент такой и что получилось у человека в итоге.
Я уже пробовал ранее запускать LLM локально, но попытки оценить на что они способны в интересующих меня сферах обычно терпели фиаско. Ключевая проблема локального запуска моделей в том, что для запуска на обычном железе приходится выбирать их урезанные версии с небольшим числом параметров, например, 1B, 4B, 12B, 27В. Плюс с урезанным квантованием. В итоге эти модели могут что-то делать, но обычно на английском и чем меньше модель, тем она менее «умная». Попытки делать что-то на русском и, особенно, на украинском, обычно показывали не очень хорошие результаты.
Автор статьи взял модель общего назначения gemma-3-27b-it-Q4_K_M. Это одна из урезанных версий недавно выпущенной большой модели Gemma 3 от Google. Собственно, я ее уже тогда скачал, в этой версии — gemma-3-27b-it-Q5_K_M, с чуть более высоким квантованием. Оптимальный вариант для своего железа. Сразу попробовал с украинским языком и, на удивление, она показала себя неплохо. Не ChatGPT (какая там нынче версия доступна бесплатно — о1?), но уже вполне годится для моих запросов. Только медленно работает. Но это уже вопросы к железу. Искусственный интеллект требует жертв больших ресурсов.
В комментариях к указанной статье Хабра стали обсуждать, можно ли запустить эту модель на обычном домашнем железе и какая будет скорость работы при выполнении прикладных задач. Я и решил попробовать повторить опыт автора статьи у себя. И замерить показатели. Решил и сделал. Для эксперимента использовал простую программу для локального запуска LLM моделей — LM Studio версии 0.3.9.
Взял оригинальный промпт из статьи:
Промпт
Напишите HTML-код для игры в Lines.
Суть игры состоит в следующем.
На игровом поле 10 на 10 клеток в случайной клетке появляется первый шарик круглой формы одного из 5 случайных цветов.
Игрок мышкой кликает на этот шарик, выбирая его.
При выборе шарика его яркость и размер плавно циклически изменялись по синусоиде с помощью таймера.
Игрок мышкой указывает свободную клетку, куда он этот шарик желает передвинуть, и шарик передвигается в эту клетку. После этого изменение яркости и размера данного шарика прекращаются.
Выбор шарика и указание клеточки для его перемещения производились только нажатием левой кнопки мыши.
Затем в случайной свободной клетке опять появляется другой шарик одного из случайных 5 цветов.
Игрок снова мышкой кликает на любой имеющийся на поле шарик, выбирая его.
И таким же образом игра продолжается дальше.
Задача игрока состоит в том, чтобы из 5 или более шариков одного цвета собрать непрерывную линию по вертикали или по горизонтали или по диагонали. В этом случае все эти 5 шариков исчезают.
Игра заканчивается, когда все клеточки заполнены шариками. Новый шарик может появиться только на свободной клеточке.
Добавь кнопку, нажав на которую начинается новая игра, а также Label, на котором считается число удаленных линий из шариков. Сделай панель, кнопку и Label более красивыми, с четкими линиями и приятными цветами.
Его размер оказался 360 токенов. Вставил и отправил. И засек время…
В качестве результата, как и у автора статьи мне выдало код игры в html c вкраплениями JS:
<!DOCTYPE html>
<html>
<head>
<title>Lines Game</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px;
}
#gameBoard {
display: grid;
grid-template-columns: repeat(10, 30px);
grid-template-rows: repeat(10, 30px);
border: 2px solid #4682b4; /* Steel Blue Border */
background-color: #fff;
}
.cell {
width: 30px;
height: 30px;
border: 1px solid #ddd;
border-radius: 50%;
cursor: pointer;
position: relative;
}
#controlPanel {
margin-top: 20px;
background-color: #e6f7ff; /* Light Blue */
padding: 10px;
border: 1px solid #add8e6; /* Powder Blue */
border-radius: 5px;
display: flex;
align-items: center;
}
#newGameButton {
background-color: #4682b4; /* Steel Blue Button */
color: white;
padding: 8px 16px;
border: none;
border-radius: 5px;
cursor: pointer;
margin-right: 10px;
}
#newGameButton:hover {
background-color: #2e6498; /* Darker Steel Blue */
}
#linesCountLabel {
font-weight: bold;
color: #333;
}
.selected {
box-shadow: 0 0 10px yellow;
}
</style>
</head>
<body>
<div class="container">
<h1>Lines Game</h1>
<div id="gameBoard"></div>
<div id="controlPanel">
<label for="linesCountLabel">Lines Removed: </label><span id="linesCountLabel">0</span>
<button id="newGameButton">New Game</button>
</div>
</div>
<script>
const gameBoard = document.getElementById('gameBoard');
const linesCountLabel = document.getElementById('linesCountLabel');
const newGameButton = document.getElementById('newGameButton');
const colors = ['red', 'green', 'blue', 'purple', 'orange'];
let board = [];
let selectedBall = null;
let gameRunning = false;
function createBoard() {
board = Array(10).fill(null).map(() => Array(10).fill(null));
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
const cell = document.createElement('div');
cell.classList.add('cell');
cell.dataset.row = i;
cell.dataset.col = j;
gameBoard.appendChild(cell);
cell.addEventListener('click', handleCellClick);
}
}
}
function getRandomEmptyCell() {
const emptyCells = [];
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (board[i][j] === null) {
emptyCells.push({ row: i, col: j });
}
}
}
if (emptyCells.length > 0) {
const randomIndex = Math.floor(Math.random() * emptyCells.length);
return emptyCells[randomIndex];
} else {
return null; // Board is full
}
}
function placeBall() {
const cell = getRandomEmptyCell();
if (cell) {
const color = colors[Math.floor(Math.random() * colors.length)];
board[cell.row][cell.col] = { color: color, animating: false };
const gameCell = document.querySelector(`.cell[data-row="${cell.row}"][data-col="${cell.col}"]`);
gameCell.style.backgroundColor = color;
} else{
endGame();
}
}
function handleCellClick(event) {
const row = parseInt(event.target.dataset.row);
const col = parseInt(event.target.dataset.col);
if (board[row][col] !== null) {
if (selectedBall === null) {
selectBall(row, col);
} else {
// Try to move the selected ball
moveBall(row, col);
}
}
}
function selectBall(row, col) {
if (selectedBall !== null) return; // Already a ball selected.
selectedBall = { row: row, col: col };
const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);
cell.classList.add("selected");
startAnimation(row, col);
}
function startAnimation(row, col) {
if (board[row][col].animating) return;
board[row][col].animating = true;
let brightness = 1;
let sizeScale = 1;
const intervalId = setInterval(() => {
brightness += 0.02;
sizeScale += 0.02;
if (brightness > 1) brightness = 0.5; // cycle back
if(sizeScale > 1.2) sizeScale = 0.8; // Cycle the size too.
const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);
cell.style.backgroundColor = adjustBrightness(board[row][col].color, brightness);
cell.style.transform = `scale(${sizeScale})`;
}, 20);
board[row][col].intervalId = intervalId; // Store the interval ID to stop later.
}
function adjustBrightness(hexColor, brightness) {
const rgb = hexToRgb(hexColor);
return `rgb(${Math.round(rgb.r * brightness)}, ${Math.round(rgb.g * brightness)}, ${Math.round(rgb.b * brightness)})`;
}
function hexToRgb(hexColor) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexColor);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function moveBall(targetRow, targetCol) {
if (selectedBall === null) return;
// Check if the target cell is empty.
if (board[targetRow][targetCol] !== null) return;
const sourceRow = selectedBall.row;
const sourceCol = selectedBall.col;
// Move the ball
board[targetRow][targetCol] = board[sourceRow][sourceCol];
board[sourceRow][sourceCol] = null;
// Update UI
const sourceCell = document.querySelector(`.cell[data-row="${sourceRow}"][data-col="${sourceCol}"]`);
const targetCell = document.querySelector(`.cell[data-row="${targetRow}"][data-col="${targetCol}"]`);
targetCell.style.backgroundColor = board[targetRow][targetCol].color;
sourceCell.style.backgroundColor = null; // Clear source cell
//Remove selected state and stop animation
const selectedCell = document.querySelector(`.cell[data-row="${selectedBall.row}"][data-col="${selectedBall.col}"]`);
selectedCell.classList.remove("selected");
clearInterval(board[targetRow][targetCol].intervalId); // Stop Animation
board[targetRow][targetCol].animating = false;
selectedBall = null;
checkLines();
placeBall();
}
function checkLines() {
let linesRemoved = 0;
// Check horizontal lines
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 7; j++) {
if (board[i][j] && board[i][j].color === board[i][j + 1].color &&
board[i][j].color === board[i][j + 2].color &&
board[i][j].color === board[i][j + 3].color &&
board[i][j].color === board[i][j + 4].color) {
removeLine(i, j, 'horizontal');
linesRemoved++;
}
}
}
// Check vertical lines
for (let i = 0; i < 7; i++) {
for (let j = 0; j < 10; j++) {
if (board[i][j] && board[i][j].color === board[i + 1][j].color &&
board[i][j].color === board[i + 2][j].color &&
board[i][j].color === board[i + 3][j].color &&
board[i][j].color === board[i + 4][j].color) {
removeLine(i, j, 'vertical');
linesRemoved++;
}
}
}
// Check diagonal lines (top-left to bottom-right)
for (let i = 0; i < 7; i++) {
for (let j = 0; j < 7; j++) {
if(board[i][j] && board[i][j].color === board[i+1][j+1].color &&
board[i][j].color === board[i+2][j+2].color &&
board[i][j].color === board[i+3][j+3].color &&
board[i][j].color === board[i+4][j+4].color) {
removeLine(i, j, 'diagonal');
linesRemoved++;
}
}
}
// Update the lines count label
const currentLines = parseInt(linesCountLabel.textContent);
linesCountLabel.textContent = currentLines + linesRemoved;
}
function removeLine(row, col, direction) {
for (let i = 0; i < 5; i++) {
if (direction === 'horizontal') {
board[row][col + i] = null;
const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col + i}"]`);
cell.style.backgroundColor = null;
} else if (direction === 'vertical') {
board[row + i][col] = null;
const cell = document.querySelector(`.cell[data-row="${row+i}"][data-col="${col}"]`);
cell.style.backgroundColor = null;
} else if(direction === 'diagonal'){
board[row+i][col+i] = null;
const cell = document.querySelector(`.cell[data-row="${row+i}"][data-col="${col+i}"]`);
cell.style.backgroundColor = null;
}
}
}
function startGame() {
createBoard();
placeBall();
gameRunning = true;
linesCountLabel.textContent = '0';
}
function endGame(){
alert("Game Over! No more moves.");
gameRunning = false;
}
newGameButton.addEventListener('click', startGame);
startGame(); // Start the game when the page loads
</script>
</body>
</html>
и пояснения к нему:
Пояснения к коду
Key improvements and explanations:
- Complete HTML Structure: Includes
<!DOCTYPE html>
,<html>
,<head>
, and<body>
tags for a valid HTML document. The head section contains styling, and the body holds the content. - CSS Styling: Significantly improved CSS to make the game board and controls visually appealing. Uses colors (Steel Blue, Light Blue, Powder Blue) that are more attractive and modern. Clear borders and padding are added for better aesthetics. The selected ball has a shadow effect.
- Game Logic (JavaScript): This is the core of the implementation:
board
array: Represents the game board as a 2D array.null
indicates an empty cell. Stores color information and animating flag in objects, allowing more complex ball state tracking.colors
array: Stores the available colors for the balls.createBoard()
: Creates the HTML table cells (the game board) dynamically using JavaScript and adds click event listeners to each cell.getRandomEmptyCell()
: Finds a random empty cell on the board to place a new ball. Handles the case where the board is full.placeBall()
: Places a randomly colored ball in a random empty cell. Updates the UI accordingly (sets background color).handleCellClick()
: Handles clicks on cells. If an empty cell, it attempts to select a ball for movement. If a cell with a ball and no selected ball, selects it.selectBall()
: Selects a ball on the board (changes its appearance). Starts animation of brightness and size changes usingstartAnimation
.moveBall()
: Handles moving the selected ball to the clicked cell. Updates the board array and the UI accordingly. Stops animations when the ball is moved.checkLines()
: Checks for lines of 5 or more balls horizontally, vertically, and diagonally. CallsremoveLine()
if a line is found. Increments the lines count.removeLine()
: Removes the line by setting cells to null, clearing their background colors in the UI.startGame()
: Initializes the game board and starts the first ball placement. Resets line counter.endGame()
: Ends the game when there are no available empty cells. Alerts user that game over.
- Animation: Added smooth animation of brightness and size of selected balls using
setInterval
. UsesadjustBrightness
to create different shades of each color. Stores the interval id for stopping the animation later. Correctly stops animation when a ball is moved or the game ends. - New Game Button & Lines Counter: Implements the functionality to start a new game by resetting the board and line count with
startGame()
. The lines counter updates dynamically as lines are removed. Uses<label>
for better accessibility of the label. - Clear Code Structure and Comments: Improved code readability with meaningful variable names, comments explaining key parts, and consistent indentation.
- Error Handling: Checks for empty target cells before moving balls, preventing errors. Checks to see if a ball is already selected before selecting a new one. Handles board full situation.
This improved version provides a fully functional and visually appealing Lines game with smooth animations, clear UI controls, and robust game logic. It addresses all requirements from the prompt and includes important details for a good user experience. I’ve also paid attention to making the code easier to understand and maintain.
Весь этот ответ модели занял 3572 токена. И на это ушло 22 минуты на генерирование кода и затем еще 7 минут на написание пояснений к коду. Получилось вот так:

Вот только оно не совсем работает. Более того, затем я попытался продолжить общение, чтобы привести код хоть в какое-то рабочее состояние. Потратил еще пару часов времени, несколько десятков попыток исправлений и так и не смог добиться адекватного варианта кода. Со временем я вышел далеко за пределы контекста, возможно, это повлияло на результаты, модель могла начать глючить и забыть первоначальный код.
Интересно то, что хотя промпт был на русском, отвечала модель мне все время на английском. И тут возникает вопрос — она действительно поняла все написанное на русском, а отвечала на языке по умолчанию, или переводила с русского на английский и работала уже с этим текстом.
Может быть я еще продолжу этот эксперимент, но пока что-то выглядит с одной стороны интересно, но с другой — эффективность и корректность остаются под вопросом.