Весь этот шум заставил меня заинтресоваться 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 проверяет возможность выигрыша для конкретного игрока, анализируя, может ли его символ (например, "X" или "O") составить выигрышную комбинацию. В отличие от этого, ChatGPT просто сравнивает значения клеток на поле (например, есть л�� в клетках "X" или "O"), чтобы определить, есть ли выигрыш. Когда обе модели проверяют ничью, они перебирают все клетки, чтобы понять, остались ли еще свободные ходы.
В целом, обе реализации корректны и работают исправно. Но Deepseek опять был более сообразительней, предусмотрев очистку поля, чтобы мы не смотрели на старые версии поля.
Обе модели выполнили задания. Недочёты, которые были у ChatGPT исправляются, доп запросами, но я оценивал именно первые ответы. Deepseek, как будто, более расположен к пользователю по ощущениям, но это всё мелочи. Сравнение мне понравилось, были даже какие-то нотки ностальгии, когда разбирал проекты нейронок.
