golang

Сравнение кода DeepSeek и ChatGPT(Golang)

  • понедельник, 3 февраля 2025 г. в 00:00:13
https://habr.com/ru/articles/878766/

Весь этот шум заставил меня заинтресоваться DeepSeek. Самое банальное сравнить с ChatGPT. Ну и плюсом, у меня сегодня выходной, можно и поиграть немного. Код пусть будет на Go. Модели GPT-o1 и Deepseek-R1.

Заданием для нейронок будет написать две игры. Эти две игры это вообще первое что я когда либо написал в своей жизни, правда, это было не на Go.

Задание 1: Игра "Пинг-Понг" на Go

Промт:
Напиши с нуля консольное приложение на Go. В приложении должна быть реализована игра "Пинг-Понг". 2 Игрока. Управление левой ракеткой с помощью клавиш W (вверх) и S (вниз), управление правой с помощью стрелочек (вверх/вниз). Мяч отскакивает от ракеток и верхних/нижних границ экрана.

Ответ ChatGPT
package main

import (
	"time"

	"github.com/nsf/termbox-go"
)

// Параметры игры
const (
	paddleHeight = 4   // Высота ракетки
	paddleChar   = '|' // Символ для рисования ракетки
	ballChar     = 'O' // Символ для мяча
	tickDuration = 50 * time.Millisecond
)

// Paddle описывает положение ракетки
type Paddle struct {
	x, y int // x - координата столбца, y - координата верхней части ракетки
}

// Ball описывает положение мяча и его скорость
type Ball struct {
	x, y   int // позиция мяча
	vx, vy int // скорость по осям
}

var (
	leftPaddle  Paddle
	rightPaddle Paddle
	ball        Ball
)

// Инициализация игры (положение ракеток и мяча)
func initGame() {
	width, height := termbox.Size()
	leftPaddle = Paddle{
		x: 1,
		y: height/2 - paddleHeight/2,
	}
	rightPaddle = Paddle{
		x: width - 2,
		y: height/2 - paddleHeight/2,
	}
	ball = Ball{
		x:  width / 2,
		y:  height / 2,
		vx: 1,
		vy: 1,
	}
}

// Отрисовка игровых объектов
func draw() {
	termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
	// Отрисовка левой ракетки
	for i := 0; i < paddleHeight; i++ {
		termbox.SetCell(leftPaddle.x, leftPaddle.y+i, paddleChar, termbox.ColorWhite, termbox.ColorDefault)
	}
	// Отрисовка правой ракетки
	for i := 0; i < paddleHeight; i++ {
		termbox.SetCell(rightPaddle.x, rightPaddle.y+i, paddleChar, termbox.ColorWhite, termbox.ColorDefault)
	}
	// Отрисовка мяча
	termbox.SetCell(ball.x, ball.y, ballChar, termbox.ColorYellow, termbox.ColorDefault)
	termbox.Flush()
}

// Обновление позиции мяча и обработка столкновений
func update() {
	width, height := termbox.Size()

	// Обновляем позицию мяча
	ball.x += ball.vx
	ball.y += ball.vy

	// Отскок от верхней и нижней границы
	if ball.y <= 0 || ball.y >= height-1 {
		ball.vy = -ball.vy
	}

	// Столкновение с левой ракеткой
	if ball.x == leftPaddle.x+1 {
		if ball.y >= leftPaddle.y && ball.y < leftPaddle.y+paddleHeight {
			ball.vx = -ball.vx
		}
	}
	// Столкновение с правой ракеткой
	if ball.x == rightPaddle.x-1 {
		if ball.y >= rightPaddle.y && ball.y < rightPaddle.y+paddleHeight {
			ball.vx = -ball.vx
		}
	}

	// Если мяч вышел за левую или правую границу - сброс позиции
	if ball.x < 0 || ball.x >= width {
		ball.x = width / 2
		ball.y = height / 2
		// Переворачиваем направление, чтобы игра продолжалась
		ball.vx = -ball.vx
	}
}

func main() {
	// Инициализация termbox
	err := termbox.Init()
	if err != nil {
		panic(err)
	}
	defer termbox.Close()

	// Режим ввода (для обработки Esc и стрелок)
	termbox.SetInputMode(termbox.InputEsc | termbox.InputMouse)
	initGame()

	// Таймер для обновления игры
	ticker := time.NewTicker(tickDuration)
	defer ticker.Stop()

	// Канал для событий ввода
	eventQueue := make(chan termbox.Event)
	go func() {
		for {
			eventQueue <- termbox.PollEvent()
		}
	}()

	// Игровой цикл
	for {
		select {
		case ev := <-eventQueue:
			if ev.Type == termbox.EventKey {
				// Выход из игры
				if ev.Key == termbox.KeyEsc || ev.Ch == 'q' || ev.Ch == 'Q' {
					return
				}
				// Управление левой ракеткой: W (вверх) и S (вниз)
				if ev.Ch == 'w' || ev.Ch == 'W' {
					if leftPaddle.y > 0 {
						leftPaddle.y--
					}
				}
				if ev.Ch == 's' || ev.Ch == 'S' {
					_, height := termbox.Size()
					if leftPaddle.y+paddleHeight < height {
						leftPaddle.y++
					}
				}
				// Управление правой ракеткой: стрелка вверх и стрелка вниз
				if ev.Key == termbox.KeyArrowUp {
					if rightPaddle.y > 0 {
						rightPaddle.y--
					}
				}
				if ev.Key == termbox.KeyArrowDown {
					_, height := termbox.Size()
					if rightPaddle.y+paddleHeight < height {
						rightPaddle.y++
					}
				}
			}
		case <-ticker.C:
			update()
			draw()
		}
	}
}

Ответ DeepSeek
package main

import (
	"fmt"
	"math/rand"
	"time"

	"github.com/nsf/termbox-go"
)

const (
	width       = 40
	height      = 20
	paddleSize  = 4
	ballSpeed   = 1
	paddleSpeed = 1
)

type vector struct{ x, y int }

type game struct {
	ball         vector
	ballVelocity vector
	leftPaddle   int
	rightPaddle  int
	leftScore    int
	rightScore   int
}

func main() {
	err := termbox.Init()
	if err != nil {
		panic(err)
	}
	defer termbox.Close()

	rand.Seed(time.Now().UnixNano())
	g := &game{
		ball:         vector{x: width / 2, y: height / 2},
		ballVelocity: vector{x: 1, y: 1},
		leftPaddle:   height/2 - paddleSize/2,
		rightPaddle:  height/2 - paddleSize/2,
	}

	input := make(chan termbox.Event)
	go func() {
		for {
			input <- termbox.PollEvent()
		}
	}()

	ticker := time.NewTicker(50 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case ev := <-input:
			if ev.Type == termbox.EventKey {
				switch {
				case ev.Key == termbox.KeyArrowUp:
					g.rightPaddle = max(0, g.rightPaddle-paddleSpeed)
				case ev.Key == termbox.KeyArrowDown:
					g.rightPaddle = min(height-paddleSize, g.rightPaddle+paddleSpeed)
				case ev.Ch == 'w':
					g.leftPaddle = max(0, g.leftPaddle-paddleSpeed)
				case ev.Ch == 's':
					g.leftPaddle = min(height-paddleSize, g.leftPaddle+paddleSpeed)
				case ev.Key == termbox.KeyEsc || ev.Key == termbox.KeyCtrlC:
					return
				}
			}

		case <-ticker.C:
			g.update()
			g.draw()
		}
	}
}

func (g *game) update() {
	g.ball.x += g.ballVelocity.x * ballSpeed
	g.ball.y += g.ballVelocity.y * ballSpeed

	// Отскок от верхней/нижней границы
	if g.ball.y <= 0 || g.ball.y >= height-1 {
		g.ballVelocity.y *= -1
	}

	// Проверка столкновений с ракетками
	if g.ball.x == 1 && g.ball.y >= g.leftPaddle && g.ball.y < g.leftPaddle+paddleSize {
		g.ballVelocity.x *= -1
		g.ball.x = 2 // Чтобы не залипал
	}

	if g.ball.x == width-2 && g.ball.y >= g.rightPaddle && g.ball.y < g.rightPaddle+paddleSize {
		g.ballVelocity.x *= -1
		g.ball.x = width - 3
	}

	// Проверка счета
	if g.ball.x < 0 {
		g.rightScore++
		g.resetBall()
	}
	if g.ball.x >= width {
		g.leftScore++
		g.resetBall()
	}
}

func (g *game) resetBall() {
	g.ball = vector{x: width / 2, y: height / 2}
	if rand.Intn(2) == 0 {
		g.ballVelocity.x = 1
	} else {
		g.ballVelocity.x = -1
	}
	g.ballVelocity.y = []int{-1, 1}[rand.Intn(2)]
}

func (g *game) draw() {
	termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)

	// Отрисовка ракеток
	for i := 0; i < paddleSize; i++ {
		termbox.SetCell(0, g.leftPaddle+i, '│', termbox.ColorWhite, termbox.ColorDefault)
		termbox.SetCell(width-1, g.rightPaddle+i, '│', termbox.ColorWhite, termbox.ColorDefault)
	}

	// Мяч
	termbox.SetCell(g.ball.x, g.ball.y, '●', termbox.ColorWhite, termbox.ColorDefault)

	// Счет
	scoreStr := fmt.Sprintf("%d - %d", g.leftScore, g.rightScore)
	for i, c := range scoreStr {
		termbox.SetCell(width/2-2+i, 0, c, termbox.ColorWhite, termbox.ColorDefault)
	}

	termbox.Flush()
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

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

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

По структуре кода у Deepseek вся логика инкапсулирована в структуру game, которая хранит состояние игры (позицию мяча, скорость, положение ракеток, счёт). Используется структура vector для координат и скорости. Тогда как ChatGPT использует глобальные переменные для ракеток и мяча, без отдельной структуры, объединяющей состояние игры.

 У Deepseek размеры поля заданы константами (width = 40, height = 20), что делает поле фиксированным. ChatGPT размеры игрового поля сделал динамическими через termbox.Size(), что позволяет игре подстраиваться под размер терминала.

У Deepseek для рисования ракеток используется символ │, для мяча — ●. Кроме того, DeepSeek додумался отрисовывавать счёт. ChatGPT ракетки рисует символом '|', мяч — символом 'O'. Счёт отсутствует, играем на интерес.

У Deepseek при столкновении с ракетками мяч отскакивает, а при выходе за границу происходит увеличение счёта и сброс мяча в центр в случайное направление. У ChatGPT при столкновении мяча с ракетками так-же происходит смена направления, а при выходе за границу мяч просто сбрасывается в центр с изменением направления по горизонтали (без учета счёта).

В обеих программах управление осуществляется с помощью клавиш (W/S для левой ракетки и стрелки вверх/вниз для правой), но позиционирование ракеток немного различается: 

  • Deepseek: Левая ракетка отрисовывается на нулевой колонке, правая — на последней (ширина – 1). 

  • ChatGPT: Левая ракетка расположена на 1-й позиции по оси X, правая — на предпоследней (ширина – 2).

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

И в итоге, оба подхода рабочие, DeepSeek сделал счёт, а ChatGPT видимо не любит соперничество. Зато сделал динамический размер поля. Но играть интересней конечно у DeepSeek.

Задание 2: Игра "Крестики-нолики" на Go

Промт: Напиши консольное приложение на Go для игры в "Крестики-нолики"

Ответ ChatGPT
package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

// Игровое поле представлено срезом строк.
// Вначале в ячейках записаны номера от 1 до 9 для удобства выбора.
var board = []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"}

// Функция для вывода игрового поля в консоль.
func printBoard() {
	fmt.Println()
	fmt.Println("-------------")
	for i := 0; i < 9; i += 3 {
		fmt.Printf("| %s | %s | %s |\n", board[i], board[i+1], board[i+2])
		fmt.Println("-------------")
	}
	fmt.Println()
}

// Функция проверки победителя.
// Возвращает "X" или "O", если один из игроков выиграл, или пустую строку, если победителя нет.
func checkWinner() string {
	// Возможные выигрышные комбинации (индексы в срезе board)
	winPatterns := [][3]int{
		{0, 1, 2}, // первая строка
		{3, 4, 5}, // вторая строка
		{6, 7, 8}, // третья строка
		{0, 3, 6}, // первый столбец
		{1, 4, 7}, // второй столбец
		{2, 5, 8}, // третий столбец
		{0, 4, 8}, // главная диагональ
		{2, 4, 6}, // побочная диагональ
	}

	for _, pattern := range winPatterns {
		if board[pattern[0]] == board[pattern[1]] && board[pattern[1]] == board[pattern[2]] {
			return board[pattern[0]]
		}
	}

	return ""
}

// Функция проверки, остались ли свободные ячейки на поле.
func boardFull() bool {
	for i := 0; i < 9; i++ {
		// Если в ячейке не стоит "X" или "O", значит, она ещё свободна.
		if board[i] != "X" && board[i] != "O" {
			return false
		}
	}
	return true
}

func main() {
	reader := bufio.NewReader(os.Stdin)
	currentPlayer := "X" // Первый ход за игроком "X"

	for {
		printBoard()
		fmt.Printf("Ход игрока %s. Введите номер ячейки (1-9): ", currentPlayer)

		// Считываем ввод пользователя
		input, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("Ошибка при чтении ввода. Попробуйте еще раз.")
			continue
		}
		input = strings.TrimSpace(input)
		move, err := strconv.Atoi(input)
		if err != nil || move < 1 || move > 9 {
			fmt.Println("Неверный ввод. Пожалуйста, введите число от 1 до 9.")
			continue
		}

		index := move - 1
		// Проверяем, свободна ли выбранная ячейка
		if board[index] == "X" || board[index] == "O" {
			fmt.Println("Эта ячейка уже занята. Попробуйте выбрать другую.")
			continue
		}

		// Выполняем ход: отмечаем выбранную ячейку символом текущего игрока
		board[index] = currentPlayer

		// Проверяем, есть ли победитель
		winner := checkWinner()
		if winner != "" {
			printBoard()
			fmt.Printf("Поздравляем! Игрок %s выиграл!\n", winner)
			break
		}

		// Если поле заполнено и победителя нет, объявляем ничью
		if boardFull() {
			printBoard()
			fmt.Println("Ничья!")
			break
		}

		// Смена игрока
		if currentPlayer == "X" {
			currentPlayer = "O"
		} else {
			currentPlayer = "X"
		}
	}
}

Ответ DeepSeek
package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func clearScreen() {
	fmt.Print("\033[2J\033[H")
}

func printBoard(board [9]rune) {
	clearScreen()
	fmt.Println()
	for i := 0; i < 9; i++ {
		if board[i] == ' ' {
			fmt.Printf(" %d ", i+1)
		} else {
			fmt.Printf(" %c ", board[i])
		}
		
		if (i+1)%3 != 0 {
			fmt.Print("|")
		} else {
			if i < 6 {
				fmt.Println("\n-----------")
			}
		}
	}
	fmt.Println("\n")
}


func checkWin(board [9]rune, player rune) bool {
	winPatterns := [][]int{
		{0, 1, 2}, {3, 4, 5}, {6, 7, 8},
		{0, 3, 6}, {1, 4, 7}, {2, 5, 8},
		{0, 4, 8}, {2, 4, 6},
	}

	for _, pattern := range winPatterns {
		if board[pattern[0]] == player &&
			board[pattern[1]] == player &&
			board[pattern[2]] == player {
			return true
		}
	}
	return false
}

func checkDraw(board [9]rune) bool {
	for _, cell := range board {
		if cell == ' ' {
			return false
		}
	}
	return true
}

func getMove(board [9]rune, player rune) int {
	reader := bufio.NewReader(os.Stdin)
	for {
		fmt.Printf("Игрок %c, введите номер клетки (1-9): ", player)
		input, _ := reader.ReadString('\n')
		input = strings.TrimSpace(input)
		num, err := strconv.Atoi(input)

		if err != nil {
			fmt.Println("Пожалуйста, введите число от 1 до 9")
			continue
		}

		if num < 1 || num > 9 {
			fmt.Println("Номер должен быть от 1 до 9")
			continue
		}

		pos := num - 1
		if board[pos] != ' ' {
			fmt.Println("Эта клетка уже занята!")
			continue
		}

		return pos
	}
}

func main() {
	board := [9]rune{' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '}
	currentPlayer := 'X'

	fmt.Println("Добро пожаловать в игру Крестики-нолики!")
	fmt.Println("Для выбора клетки используйте цифры 1-9, как на телефонной клавиатуре")

	for {
		printBoard(board)
		pos := getMove(board, currentPlayer)
		board[pos] = currentPlayer

		if checkWin(board, currentPlayer) {
			printBoard(board)
			fmt.Printf("Игрок %c победил!\n", currentPlayer)
			break
		}

		if checkDraw(board) {
			printBoard(board)
			fmt.Println("Ничья!")
			break
		}

		if currentPlayer == 'X' {
			currentPlayer = 'O'
		} else {
			currentPlayer = 'X'
		}
	}
}

Оба варианта рабочие и разницу видно сразу.

Начнём с игрового поля. Deepseek использует массив с типом rune и пустые символы для пустых клеток, что позволяет отличать типичные значения игроков ('X' и 'O') от пустых клеток. В версии ChatGPT используется срез строк, где клетки сразу содержат номера, что упрощает выбор клетки игроком, но может потребовать дополнительных преобразований при обновлении поля.

DeepSeek использует использует ANSI escape-коды (\033[2J\033[H) для очистки экрана перед каждым выводом доски. Также разделяет клетки вертикальными и горизонтальными разделителями. В коде ChatGPT отсутствует очистка экрана, поле просто выводится в консоль с разделителями.

В реализации Deepseek ввод вынесен в отдельную функцию getMove, что повышает модульность и читабельность кода. У ChatGPT всё сделано непосредственно в main(), что несколько усложняет основной цикл.

Обе реализации используют похожий подход проверки выигрыша — перебор фиксированного набора индексов для определения выигрышной комбинации. Главное отличие в том, что Deepseek проверяет для конкретного игрока, а ChatGPT функция сравнивает значения клеток напрямую. При проверки ничьей обе модели перебирают все клетки, чтобы определить, есть ли ещё свободные ходы.

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

Обе модели выполнили задания. Недочёты, которые были у ChatGPT исправляются, доп запросами, но я оценивал имено первые ответы. Deepseek, как будто, более расположен к пользователю по ощущениям, но это всё мелочи. Сравнение мне понравилось, были даже какие-то нотки ностальгии, когда разбирал проекты нейронок.