В предыдущей статье мы за 2 шага создали с помощью LLM игру для браузера «Шарики», Lenes (Color Lines).

На первом шаге одним большим подробным промптом мы создали основной рабочий код с визуализацией, логикой и функционалом.

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

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

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

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

В конце статьи будет ссылка на загрузку HTML-файла с полным кодом игры и спрайтами.

Сначала модель среднего уровня

Я снова буду использовать модель gemma-3-27b-it, так как она во всех моих опытах зарекомендовала себя хорошо в разных областях, в том числе и для кодинга по подробной инструкции, и полностью доступна.

Шаг 1. Первый промпт.

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

Шаг 2. Второй промпт.

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

Шаг 3. Третий промпт.

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

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

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

В результате получилось как раз то, что нужно.

Почему модель не справилась на третьем шаге?

Я думаю, что причин несколько.

Во-первых, быстрый рост усложнения структуры кода и его функциональности. Всё больше и больше требований, которые нужно соблюсти, обеспечив целостность и внутреннюю непротиворечивость. Начинается «когнитивная» перегрузка.

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

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

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

Теперь попробуем в деле по-настоящему серьёзную модель и сравним результаты.

Claude3.5-sonnet в экспериментах как ИИ-агент научилась неплохо играть в Super Mario, а вот сможет ли создать нашу игру Super Habrio за один шаг, если сделать подробную инструкцию?

У нас получился вот такой навороченный промпт.

Создайте лучшую версию игры Super Mario в формате HTML, но вместо Mario пусть будет Habrio.
Основные свойства игры:
- размер игрового поля 1200 x 600;
- титульный экран;
- длинный уровень;
- obstacles;
- Platforms
- облака на небе;
- кусты на заднем плане;
- Habrio может перемещаться влево и вправо с помощью клавиш управления курсором, платформы тоже двигаются, и Habrio может запрыгивать на них и спрыгивать вниз;
- Создай class Ground для основания, на котором стоит Habrio и obstacles. Используй для основания текстуру из файла "ground.jpg". Это основание также двигаться справа налево синхронно со скоростью obstacles;
- используй изображение Habrio из файла "habrio.png";
- Для obstacles используй изображение из файла "obstacles.png", obstacles всегда стоят на основании и их размер случайно немного изменяется;
- clouds сделай более разнообразными по форме;
- bushes сделай более разнообразными по форме и цвету;
- для Platform используй текстуру из файла "platform.jpg";
- При столкновении Habrio с obstacles сначала изобрази взрыв obstacle, который разлетается на части в разные стороны в виде нескольких маленьких квадратиков, а Habrio отбрасывает взрывом случайно вверх и в сторону по параболической траектории, и уже после его падения наступает Game Over.
У нас имеется спрайт для движения Habrio вправо в файле right.png размером 180 x 68, состоящий из 3 фреймов 60 x 68.
А также спрайт для движения Habrio влево в файле left.png, тоже размером 180 x 68, состоящий из 3 фреймов 60 x 68.
При движении Habrio вправо и влево сделай анимацию из этих спрайтов.
При прыжке — изображение из файла "jump.png".
Еще у нас имеется спрайт для сундука с деньгами в файле chest.png размером 180 x 63, состоящий из 3 фреймов 60 x 63.
В процессе игры случайно сверху вниз медленно падают сундуки в виде фрейма с индексом 1 из спрайта, и при их столкновении с препятствиями Ground, Obstacle, Platform падение прекращается, а сундук начинает двигаться справа налево со скоростью данного препятствия, с которым он столкнулся. В этот момент сундук заменяется на фрейм с индексом 0 из спрайта. При столкновении Habrio с сундуком сундук заменяется на фрейм с индексом 2 из спрайта. В каждом сундуке находится случайное количество денег от 100 до 1000 монет. Habrio получает эту сумму, если сталкивается с сундуком, и его общая сумма увеличивается. Ты должен показывать эту общую сумму на экране сверху. При столкновении Habrio с Obstacle происходит Explosion Effect и из общей суммы вычитается 100 монет. Если общая сумма больше 0, то игра продолжается, а если меньше, то Game Over.

Claude3.5-sonnet за 1 шаг сразу выдал мне полный рабочий код игры, практически полностью лишенный каких-либо недостатков. Было только одно небольшое логическое нарушение, которое легко исправляется (наш Habrio сваливался с платформы сразу, как только на неё запрыгивал).

Попробуем сделать второй шаг, увеличив сложность логики и графики.

Внеси в этот код следующие изменения и дополнения, сохраняя всю имеющуюся функциональность:
- высота прыжка должна зависеть от длительности нажатия ArrowUp или Space, но не превышать заданного уровня;
- добавь объекты, «провал в земле» (яма с горячей лавой на дне), которые появляются на ground случайно, аналогично obstacles. Они должны иметь разную ширину. Habrio может перепрыгивать через эти провалы. Если он не сумеет перепрыгнуть через провал, то он падает вниз и сгорает в горячей лаве (изобрази анимацию этого процесса) и наступает Game Over;
- Через заданное время, например, 2 минуты, игра заканчивается победой, если Habrio остается жив. Изобрази радостные прыжки Habrio и анимацию цветных фейерверков.

Опять успешный результат, но уже есть небольшие нарушения в логике игры и визуальном представлении.
Тем не менее, это всё легко исправляется и настраивается. Объём ручных исправлений 2-3 процента от общего объема кода.

Посмотреть игру в песочнице. (на страничке нужно выбрать — Yes, proceed to preview).

Обратите внимание на грамотную программную реализацию сложных визуальных эффектов: движение облаков, взрывы, кипящая лава, горение Habrio, фейерверки.

Здесь, для примера, код нашего персонажа
const habrio = {
	x: 100,
	y: 400,
	width: 60,
	height: 68,
	velocityX: 0,
	velocityY: 0,
	speed: 5,
	jumping: false,
	falling: false,
	frameIndex: 0,
	tickCount: 0,
	ticksPerFrame: 5,
	direction: 'right',
	animationActive: false,
	coins: 0,
	celebratingVictory: false,
	celebrationJumps: 0,
	maxCelebrationJumps: 10,
	burning: false,
	burningTicks: 0,
	burningMaxTicks: 260,
	update() {
		// Check if burning animation
		if (this.burning) {
			gameSpeed = 0;
			this.burningTicks++;
			// Burning animation complete
			if (this.burningTicks >= this.burningMaxTicks) {
				endGame();
			}
			return;
		}
		
		// Check if celebrating victory
		if (this.celebratingVictory) {
			gameSpeed = 0;
			// Make Habrio jump automatically
			if (!this.jumping && !this.falling) {
				this.velocityY = -12;
				this.jumping = true;
				this.celebrationJumps++;
				
				// Create fireworks on each jump
				for (let i = 0; i < 3; i++) {
					createFirework();
				}
				
				// Stop celebration after max jumps
				if (this.celebrationJumps >= this.maxCelebrationJumps) {
					this.celebratingVictory = false;
					setTimeout(() => {
						showVictoryScreen();
					}, 2000);
				}
			}
		}
		
		this.extendJump();
		
		// Apply gravity
		if (!onPlatform){
			this.velocityY += gravity;
		}
		
		// Update position
		this.x += this.velocityX;
		this.y += this.velocityY;
		
		// Ground collision
		const groundLevel = canvas.height - this.height - 50; // 50 is ground height
		if (this.y > groundLevel) {
			this.y = groundLevel;
			this.velocityY = 0;
			this.jumping = false;
			this.falling = false;
		}

		// Boundaries
		if (this.x < 0) this.x = 0;
		if (this.x + this.width > canvas.width) this.x = canvas.width - this.width;

		// Animation
		if (this.animationActive) {
			this.tickCount++;
			if (this.tickCount > this.ticksPerFrame) {
				this.tickCount = 0;
				this.frameIndex = (this.frameIndex + 1) % 3;
			}
		} else {
			this.frameIndex = 0;
		}
	},
	draw() {
		if (this.burning) {
			// Draw burning animation
			ctx.save();
			// Flash red/orange
			const flickerIntensity = Math.random() * 0.5 + 0.5;
			ctx.globalAlpha = flickerIntensity;
			
			// Base character
			ctx.drawImage(
				this.direction === 'right' ? images.habrio_right : images.habrio_left, 
				this.frameIndex * 60, 0, 
				60, 68, 
				this.x, this.y, 
				this.width, this.height
			);
			
			// Flames overlay
			ctx.globalAlpha = 0.8;
			
			// Draw fire particles
			for (let i = 0; i < 15; i++) {
				const fireX = this.x + Math.random() * this.width;
				const fireY = this.y + Math.random() * this.height;
				const fireSize = 5 + Math.random() * 15;
				
				// Create gradient for fire
				const gradient = ctx.createRadialGradient(
					fireX, fireY, 0,
					fireX, fireY, fireSize
				);
				gradient.addColorStop(0, 'rgba(255, 255, 0, 0.8)');
				gradient.addColorStop(0.5, 'rgba(255, 120, 0, 0.6)');
				gradient.addColorStop(1, 'rgba(255, 0, 0, 0)');
				
				ctx.fillStyle = gradient;
				ctx.beginPath();
				ctx.arc(fireX, fireY, fireSize, 0, Math.PI * 2);
				ctx.fill();
			}
			
			ctx.restore();
		} else if (this.jumping) {
			ctx.drawImage(images.habrio_jump, 0, 0, 60, 68, this.x, this.y, this.width, this.height);
		} else {
			const spriteSheet = this.direction === 'right' ? images.habrio_right : images.habrio_left;
			ctx.drawImage(
				spriteSheet, 
				this.frameIndex * 60, 0, 
				60, 68, 
				this.x, this.y, 
				this.width, this.height
			);
		}
	},
	jump() {
		if (!this.jumping && !this.falling) {
			// Start with minimum jump power
			this.velocityY = minJumpPower;
			this.jumping = true;
			onPlatform = false;
			jumpHoldTime = 0;
			jumpPressed = true;
		}
	},
	extendJump() {
		// Extend jump if button is still held
		if (this.jumping && jumpPressed) {
			jumpHoldTime++;
			// Calculate additional jump power based on hold time
			const additionalPower = Math.min(jumpHoldTime / 10, 1) * (maxJumpPower - minJumpPower);
			this.velocityY += additionalPower;
		}
	},
	releaseJump() {
		jumpPressed = false;
	},
	burn() {
		if (!this.burning) {
			this.burning = true;
			this.burningTicks = 0;
			this.velocityX = 0;
			this.velocityY = 0;
		}
	},
	celebrate() {
		this.celebratingVictory = true;
		this.celebrationJumps = 0;
	}
};

Смог бы Claude3.5-sonnet сделать далее несколько последовательных шагов по совершенствованию игры, я не стал уже проверять. Если да, то это было бы ещё более эффектно.

Визуальное оформление, качество кода, его логичность и структура — всё на высоком уровне.
Даже такой важный нюанс, как согласование высокой частоты обновления и более низкой частоты анимации спрайтов, был грамотно учтен.
Claude3.5-sonnet справился отлично, и это действительно настоящий ассистент, которому можно поручать серьёзные задачи.

А вот наша подопытная модель gemma-3-27b-it с этим же промптом не справилась, несколько попыток окончились неудачно.

Выводы

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

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

Этот опыт показывает три важных результата.

1. Программировать с помощью LLM удобно и эффективно. Это разгружает и повышает эффективность в широком смысле слова.

2. Нужно понимать ограниченность возможностей и не требовать от модели всего и сразу. Лучше структурировать свою задачу и решать задачи программирования по частям. А также поэтапно, в несколько шагов, методом от простого к сложному, улучшая код на каждой итерации постепенно. При этом на первый план выходит контроль и коррекция написанного моделью кода.

3. От ручного программирования избавиться совсем нереально. По крайней мере, на данном этапе и, наверное, в ближайшем будущем. Однако объем кодирования вручную можно точно сократить в разы и сосредоточиться на творческих аспектах и планировании.

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

P.S. скачать архив можно здесь.