GO: Desafio do Quiz (simplificado)

Olá, leitores do CodeAspiras!

Quem participa do nosso canal no Discord sabe que, de tempos em tempos, nós lançamos alguns desafios de programação no canal #desafios. Alguns são bem simples, outros meio complexos. Eu vou destrinchar o que foi mais famoso até então por aqui pra disseminar conhecimento. Se você gostar e quiser buscar mais desafios, é só chegar junto no Discord e ir fazendo o que estiver por lá.

Ah, um detalhe antes de começar: esse é o desafio simplificado. O original exige um pouco mais de conhecimento, como leitura/escrita em arquivo, decodificar/encodar JSON, etc. Como ainda tem muito estudante no começo ainda, decidi retirar esses obstáculos e deixar o desafio mais simples, gerando o que verá a seguir.

O desafio

Faça um programa que inicializa algumas variáveis de um quiz, faz as perguntas para colher as respostas e imprime o resultado.

Obs.: não está sendo determinado como seu programa deve funcionar!

Impressão esperada no terminal

Pergunta 1: Essa seria a primeira pergunta...?
Opção 1: Sim...?
Opção 2: Não....?
Resposta: 1
--
Pergunta 2: Essa seria a terceira pergunta...?
Opção 1: Sim...?
Opção 2: Não....?
Resposta: 1
--
Resultado: você acertou 1 de 2 perguntas!!

E é isso, não tem mais nenhuma descrição. A ideia é que seu programa inicie já imprimindo:

Pergunta 1: Essa seria a primeira pergunta...?
Opção 1: Sim...?
Opção 2: Não....?
Resposta:

E aguarde o usuário responder. Daí o usuário responde 1, que é a resposta correta.

Então seu programa continuará o processo, imprimindo uma separação e a próxima pergunta:

--
Pergunta 2: Essa seria a terceira pergunta...?
Opção 1: Sim...?
Opção 2: Não....?
Resposta:

E aí, mais uma vez, irá aguardar o usuário responder. Respondendo 1, que é a resposta errada, o programa deverá então finalizar e imprimir uma separação e o resultado, informando que acertei apenas uma das duas perguntas:

--
Resultado: você acertou 1 de 2 perguntas!!

As únicas entradas do usuário são as respostas, que são números.

Sacou? Quer tentar desenvolver uma solução aí antes de continuar lendo?

Se for tentar, aprecie esse gif e só role a tela quando terminar!

A solução (em Golang)

A maior dificuldade que as pessoas tem ao tentar solucionar desafios como esse é dar o primeiro passo. É como se você estivesse dirigindo um carro bem velho, dando partida no meio de uma ladeira. Se você tentou pensar em alguma coisa e não conseguiu, espero que possa absorver um pouco da técnica que vou te passar agora, que é infalível para construção de linhas de raciocínio.

Essa técnica se chama Baby Steps, que significa Passos de Bebê, em inglês. A ideia é que, ao invés de tentar montar, na sua cabeça, toda uma história de "começo, meio e fim", você deveria dar um passo de cada vez. Conforme você se torna experiente, será capaz de montar a história inteira na mente antes de colocar a mão na massa, mas enquanto não chega lá, não faz mal algum dar pequenos passos de bebê.

Nesse caso, eu já dei uma ajudinha ali em cima descrevendo que o desafio não está determinando como deve ficar seu código e exibindo, passo-a-passo, como as coisas devem aparecer no terminal.

Nesse caso, você pode simplemente copiar tudo o que foi impresso no exemplo e... imprimir, Kiko?

Sim! Exatamente! Supondo que você ainda está aprendendo a fazer códigos em Golang, vamos por parte:

1 - Imprimindo todos os textos

package main

import "fmt"

func main() {
    // Pergunta 1
    fmt.Println("Pergunta 1: Essa seria a primeira pergunta...?")
    fmt.Println("Opção 1: Sim...?")
    fmt.Println("Opção 2: Não....?")
    fmt.Print("Resposta: ")
    // ler resposta
    fmt.Println("") // retirar essa quebra de linha temporaria
    fmt.Println("--")

    // Pergunta 2
    fmt.Println("Pergunta 2: Essa seria a terceira pergunta...?")
    fmt.Println("Opção 1: Sim...?")
    fmt.Println("Opção 2: Não....?")
    fmt.Print("Resposta: ")
    // ler resposta
    fmt.Println("") // retirar essa quebra de linha temporaria
    fmt.Println("--")

    // Conclusão
    fmt.Println("Resultado: você acertou 1 de 2 perguntas!!")
}

Veja o código no Playground: https://go.dev/play/p/HBrbJPnV_SY

O código acima irá imprimir:

Pergunta 1: Essa seria a primeira pergunta...?
Opção 1: Sim...?
Opção 2: Não....?
Resposta: 
--
Pergunta 2: Essa seria a terceira pergunta...?
Opção 1: Sim...?
Opção 2: Não....?
Resposta: 
--
Resultado: você acertou 1 de 2 perguntas!!

Nesse momento, nós já estamos fazendo 90% do que foi pedido no desafio, concorda? Falta, literalmente, ler dois números. Caso não saiba como ler a entrada do terminal, basta pesquisar <linguagem> scanf e você vai achar alguns bons artigos explicando como fazer. Note que é importante ler mais do que um site pra tirar alguma conclusão, hein?

2 - Lendo respostas

No caso do nosso desafio, eu poderia usar o pacote bufio, que nos permite ter mais controle sobre qual stream de entrada estamos utilizando e tal, mas, para simplificar, quero utilizar a função Scanln do pacote fmt. É similar ao scanf de algumas outras linguagens por aí. Quer ver?

PS.:**daqui para baixo*, para rodar o programa, precisará criar o arquivo localmente e rodando na mão. Pra fazer isso,* baixe o Golang, instale, depois crie uma pasta em qualquer lugar, abra essa pasta pelo terminal e digitego mod init quizsimp. Daí crie o arquivomain.goe cole o código nesse arquivo. Para executar, rode:go run .

package main

import "fmt"

func main() {
    var resposta string

    // Pergunta 1
    fmt.Println("Pergunta 1: Essa seria a primeira pergunta...?")
    fmt.Println("Opção 1: Sim...?")
    fmt.Println("Opção 2: Não....?")

    // Resposta 1
    fmt.Print("Resposta: ")
    if _, err := fmt.Scanln(&resposta); err != nil {
        fmt.Printf("Um erro aconteceu ao ler a resposta: %s\n", err)
        return
    }

    fmt.Println("--")

    // Pergunta 2
    fmt.Println("Pergunta 2: Essa seria a terceira pergunta...?")
    fmt.Println("Opção 1: Sim...?")
    fmt.Println("Opção 2: Não....?")

    // Resposta 2
    fmt.Print("Resposta: ")
    if _, err := fmt.Scanln(&resposta); err != nil {
        fmt.Printf("Um erro aconteceu ao ler a resposta: %s\n", err)
        return
    }

    fmt.Println("--")

    // Conclusão
    fmt.Println("Resultado: você acertou 1 de 2 perguntas!!")
}

3 - Contabilizando acertos

Agora você está armazenando a resposta do usuário na nova variável resposta, certo? Só que não estamos fazendo nada com isso!! Que tal adicionarmos uma outra variável acertos que contabiliza quantas respostas foram certas?

Além disso, podemos mudar a impressão de resultados de Println para Printf e formatar o texto, injetando o número de acertos:

package main

import "fmt"

func main() {
    var resposta string
    acertos := 0

    // Pergunta 1
    fmt.Println("Pergunta 1: Essa seria a primeira pergunta...?")
    fmt.Println("Opção 1: Sim...?")
    fmt.Println("Opção 2: Não....?")

    // Resposta 1
    fmt.Print("Resposta: ")
    if _, err := fmt.Scanln(&resposta); err != nil {
        fmt.Printf("Um erro aconteceu ao ler a resposta: %s\n", err)
        return
    }

    if resposta == "1" {
        acertos++
    }

    fmt.Println("--")

    // Pergunta 2
    fmt.Println("Pergunta 2: Essa seria a terceira pergunta...?")
    fmt.Println("Opção 1: Sim...?")
    fmt.Println("Opção 2: Não....?")

    // Resposta 2
    fmt.Print("Resposta: ")
    if _, err := fmt.Scanln(&resposta); err != nil {
        fmt.Printf("Um erro aconteceu ao ler a resposta: %s\n", err)
        return
    }

    if resposta == "2" {
        acertos++
    }

    fmt.Println("--")

    // Conclusão
    fmt.Printf("Resultado: você acertou %d de 2 perguntas!!\n", acertos)
}

E pronto! Solucionamos o desafio............... Certo?

A verdade é que estamos gerando a mesma saída esperada que coloquei no começo desse desafio. Então, sim, solucionamos, porém não está nada elegante. Se quisermos fazer um Quiz com 100 perguntas, esse código ficaria gigantesco!

Nós podemos separar um pouco as responsabilidades aqui: que tal agruparmos todos os dados de cada pergunta em uma estrutura, deixando, então, o trecho de impressão/leitura fica responsável apenas por isso mesmo, simplificando os deveres?

4 - Organizando dados em estruturas

E o que teria nessa estrutura de pergunta, Kiko?

Bem... A pergunta em si, as opções e a resposta esperada (vulgo gabarito). Seria algo assim:

type QuizItem struct {
    Question string
    Options map[string]string
    RightOptionIndex string
}

E poderíamos organizar a primeira pergunta assim:

QuizItem{
    Question: "Essa seria a primeira pergunta...?",
    Options: map[string]string{
        "1": "Sim...?",
        "2": "Não....?",
    },
    RightOptionIndex: "1",
}

E, com isso, teríamos quase todas as informações para imprimir tudo, concorda?

Como assim, quase todas, Kiko?

É que falta sabermos qual é o número dessa pergunta. Como a ideia é termos uma lista de perguntas, teremos essa informação de outro lugar. Enfim, vamos transformar o código:

package main

import "fmt"

type QuizItem struct {
    Question         string
    Options          map[string]string
    RightOptionIndex string
}

func main() {
    // prepara o quiz
    quiz := []QuizItem{
        {
            Question: "Essa seria a primeira pergunta...?",
            Options: map[string]string{
                "1": "Sim...?",
                "2": "Não....?",
            },
            RightOptionIndex: "1",
        },
        {
            Question: "Essa seria a terceira pergunta...?",
            Options: map[string]string{
                "1": "Sim...?",
                "2": "Não....?",
            },
            RightOptionIndex: "2",
        },
    }

    // executa o quiz
    acertos := 0
    for index, item := range quiz {
        // número da pergunta vem do index+1 !
        fmt.Printf("Pergunta %d: %s\n", index+1, item.Question)

        for num, opt := range item.Options {
            fmt.Printf("Opção %s: %s\n", num, opt)
        }

        fmt.Print("Resposta: ")
        var resposta string
        if _, err := fmt.Scanln(&resposta); err != nil {
            fmt.Printf("Um erro aconteceu ao ler a resposta: %s\n", err)
            return
        }

        if resposta == item.RightOptionIndex {
            acertos++
        }

        fmt.Println("--")
    }

    // Conclusão
    fmt.Printf("Resultado: você acertou %d de 2 perguntas!!\n", acertos)
}

E podemos ir ainda além!! E se quisermos exibir o resultado em forma de gabarito? Bem, só faz sentido se você puder distinguir as opções corretas das erradas, concorda? E pra isso você pode usar símbolos e tal, mas eu prefiro COLORIR.

5 - Exibindo gabarito após resultado

Oi?! Dá pra colorir texto no terminal, Kiko?

Sim, dá! E para isso é só usarmos um comando que altera as propriedades de texto do terminal. Eu não sei exatamente qual é o nome disso, até tentei pesquisar mas parece que cada pessoa chama do jeito que quer, então não me ajudou muito, rs. Então vamos chamar do nosso jeito também! E deixo isso aberto para que alguém me corrija publicamente a vontade, hehe.

Eu chamo esse comando de Editor de Fonte. Ele funciona como uma tag HTML, tendo um comando no início de onde você quer trocar a fonte e outro no final. Em algumas linguagens, você pode acessar esse escape pela inicial \e, mas em Golang nós acessamos pela inicial \033:

  • Começa com: \033[<nums>m

  • Termina com: \033[0m

Nesse caso, a ideia é que, no lugar de <nums>, você coloque os números que vão definir o estilo de fonte a utilizar no trecho seguinte. Depois que acabar de imprimir o texto no estilo desejado, você reseta o estilo novamente com \033[0m. Sim, 0 é um dos possíveis códigos a se colocar no lugar de <nums>. E quando utilizamos mais de um código, nós separamos eles com ponto-e-vírgula (;).

Eu não vou colocar nenhuma tabela dos possíveis estilos aqui, mas o que nos interessa no momento:

  • cor vermelha: 31;

  • cor verde: 32.

Então, se você quiser imprimir uma linha vermelha, é só imprimir:

fmt.Println("\033[31mEsse é o estilo com a cor vermelha\033[0m")

Dito isso, podemos fazer três constantes com as cores e uma função para fazer essa impressão formatada, o que acha?

const (
    COLOR_RED = "31"
    COLOR_GREEN = "32"
)

func printStyled(text string, styles ...string) {
    fmt.Println("\033[" + strings.Join(styles, ";") + "m" + text + "\033[0m")
}

Assim, é só chamar:

printStyled("Opção 1: ...", COLOR_GREEN) // para opções corretas
printStyled("Opção 2: ...", COLOR_RED) // para opções marcadas incorretamente

Mas ainda não estamos prontos, pois... Não salvamos nenhuma resposta do usuário.

Então vamos adicionar uma nova informação no QuizItem: Answer string, além de mudar o slice []QuizItem para []*QuizItem para usarmos sempre as mesmas referências, alterando e salvando as respostas na própria estrutura:

package main

import (
    "fmt"
)

type QuizItem struct {
    Question         string
    Options          map[string]string
    RightOptionIndex string
    Answer           string
}

func main() {
    // prepara o quiz
    quiz := []*QuizItem{
        {
            Question: "Essa seria a primeira pergunta...?",
            Options: map[string]string{
                "1": "Sim...?",
                "2": "Não....?",
            },
            RightOptionIndex: "1",
        },
        {
            Question: "Essa seria a terceira pergunta...?",
            Options: map[string]string{
                "1": "Sim...?",
                "2": "Não....?",
            },
            RightOptionIndex: "2",
        },
    }

    // executa o quiz
    acertos := 0
    for index, item := range quiz {
        fmt.Printf("Pergunta %d: %s\n", index+1, item.Question)

        for num, opt := range item.Options {
            fmt.Printf("Opção %s: %s\n", num, opt)
        }

        fmt.Print("Resposta: ")
        if _, err := fmt.Scanln(&item.Answer); err != nil {
            fmt.Printf("Um erro aconteceu ao ler a resposta: %s\n", err)
            return
        }

        if item.Answer == item.RightOptionIndex {
            acertos++
        }

        fmt.Println("--")
    }

    // Conclusão
    fmt.Printf("Resultado: você acertou %d de 2 perguntas!!\n", acertos)
}

E agora, podemos fazer um novo loop nas perguntas, colorindo as respostas corretas e erradas!!

package main

import (
    "fmt"
    "strings"
)

const (
    COLOR_RED    = "31"
    COLOR_GREEN  = "32"
)

type QuizItem struct {
    Question         string
    Options          map[string]string
    RightOptionIndex string
    Answer           string
}

func printStyled(text string, styles ...string) {
    fmt.Print("\033[" + strings.Join(styles, ";") + "m" + text + "\033[0m")
}

func main() {
    // prepara o quiz
    quiz := []*QuizItem{
        {
            Question: "Essa seria a primeira pergunta...?",
            Options: map[string]string{
                "1": "Sim...?",
                "2": "Não....?",
            },
            RightOptionIndex: "1",
        },
        {
            Question: "Essa seria a terceira pergunta...?",
            Options: map[string]string{
                "1": "Sim...?",
                "2": "Não....?",
            },
            RightOptionIndex: "2",
        },
    }

    // executa o quiz
    acertos := 0
    for index, item := range quiz {
        fmt.Printf("Pergunta %d: %s\n", index+1, item.Question)

        for num, opt := range item.Options {
            fmt.Printf("Opção %s: %s\n", num, opt)
        }

        fmt.Print("Resposta: ")
        if _, err := fmt.Scanln(&item.Answer); err != nil {
            fmt.Printf("Um erro aconteceu ao ler a resposta: %s\n", err)
            return
        }

        if item.Answer == item.RightOptionIndex {
            acertos++
        }

        fmt.Println("--")
    }

    // gabarito
    fmt.Println("Gabarito:")
    for index, item := range quiz {
        fmt.Printf("Pergunta %d: %s\n", index+1, item.Question)

        for num, opt := range item.Options {
            label := fmt.Sprintf("Opção %s: %s", num, opt)
            if num == item.RightOptionIndex {
                printStyled(label, COLOR_GREEN)
            } else if num == item.Answer {
                printStyled(label, COLOR_RED)
            } else {
                fmt.Print(label)
            }

            fmt.Println("")
        }

        fmt.Println("--")
    }

    // Conclusão
    fmt.Printf("Resultado: você acertou %d de 2 perguntas!!\n", acertos)
}

Print do resultado final do código final

Conclusão

Nesse desafio, nós fizemos um código estritamente imperativo, mas poderíamos evoluir ainda mais. Por exemplo, eu fiz minha versão do desafio com Bubbletea, sendo o desafio completo (lendo arquivos). Enfim, essa é uma excelente atividade para iniciantes! Vocês aprendem a colocar em prática várias coisas de uma vez só. Além disso, você pode brincar com seus colegas fazendo perguntas reais nos programas finais! E se puder aceitar múltiplas respostas? E se for um Quiz de scoring ao invés de gabarito? No scoring, não existe opção errada, apenas pontuações. No final, o total de pontuação do usuário varia pelas opções marcadas, encaixando ele em algum grupo de pontuações esperadas. O que acha de desenrolar esse desafio?


E por hoje é só! Curtiu? Comenta e compartilha!! Como falei no início, temos outros desafios lá no canal do Discord. Pode chegar que a casa é aberta, rs. Qualquer coisa, é só falar aí nos comentários!

Abraços...

Inté!