Сравнение кода DeepSeek и ChatGPT(Golang)
- понедельник, 3 февраля 2025 г. в 00:00:13
Весь этот шум заставил меня заинтресоваться DeepSeek. Самое банальное сравнить с ChatGPT. Ну и плюсом, у меня сегодня выходной, можно и поиграть немного. Код пусть будет на Go. Модели GPT-o1 и Deepseek-R1.
Заданием для нейронок будет написать две игры. Эти две игры это вообще первое что я когда либо написал в своей жизни, правда, это было не на Go.
Промт:
Напиши с нуля консольное приложение на Go. В приложении должна быть реализована игра "Пинг-Понг". 2 Игрока. Управление левой ракеткой с помощью клавиш W (вверх) и S (вниз), управление правой с помощью стрелочек (вверх/вниз). Мяч отскакивает от ракеток и верхних/нижних границ экрана.
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()
}
}
}
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.
Промт: Напиши консольное приложение на Go для игры в "Крестики-нолики"
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"
}
}
}
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, как будто, более расположен к пользователю по ощущениям, но это всё мелочи. Сравнение мне понравилось, были даже какие-то нотки ностальгии, когда разбирал проекты нейронок.