Quiz no Terminal em Zig — Tutorial Passo a Passo

Quiz no Terminal em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir um jogo de quiz interativo no terminal. O jogador responde perguntas de múltipla escolha, acumula pontos e recebe um ranking no final. Este projeto é excelente para praticar arrays, enums, structs e lógica de controle em Zig.

O Que Vamos Construir

Nosso quiz vai:

  • Apresentar perguntas de múltipla escolha com 4 alternativas
  • Organizar perguntas por categorias (Ciência, História, Tecnologia)
  • Calcular pontuação com bônus por respostas rápidas consecutivas
  • Embaralhar a ordem das perguntas
  • Exibir um ranking final (Mestre, Especialista, Novato, etc.)

Por Que Este Projeto?

Um quiz é um projeto que parece simples, mas nos força a pensar em como modelar dados estruturados em Zig. Precisamos de arrays de structs, enums para categorias, e lógica de seleção aleatória. É o tipo de projeto que consolida o uso de tipos compostos da linguagem.

Pré-requisitos

Passo 1: Estrutura do Projeto

mkdir quiz-terminal
cd quiz-terminal
zig init

Vamos trabalhar em src/main.zig.

Passo 2: Modelando os Dados

O primeiro passo é definir como representamos perguntas, alternativas e categorias. Em Zig, preferimos tipos explícitos e comptime-known sempre que possível.

const std = @import("std");
const io = std.io;
const fmt = std.fmt;
const mem = std.mem;

/// Categorias das perguntas.
/// Usamos enum para que o compilador garanta que tratamos todas as categorias.
const Categoria = enum {
    ciencia,
    historia,
    tecnologia,

    pub fn nome(self: Categoria) []const u8 {
        return switch (self) {
            .ciencia => "Ciência",
            .historia => "História",
            .tecnologia => "Tecnologia",
        };
    }

    pub fn cor(self: Categoria) []const u8 {
        return switch (self) {
            .ciencia => "\x1b[32m",   // verde
            .historia => "\x1b[33m",  // amarelo
            .tecnologia => "\x1b[36m", // ciano
        };
    }
};

/// Uma pergunta do quiz com suas alternativas.
/// As alternativas são um array fixo de 4 strings.
/// A resposta correta é indexada de 0 a 3.
const Pergunta = struct {
    texto: []const u8,
    alternativas: [4][]const u8,
    resposta_correta: u2, // 0-3, u2 é suficiente
    categoria: Categoria,
    dificuldade: u8, // 1-3, afeta pontuação
};

/// Resultado final do jogador.
const Ranking = enum {
    mestre,
    especialista,
    competente,
    aprendiz,
    novato,

    pub fn dePercentual(pct: f64) Ranking {
        if (pct >= 90.0) return .mestre;
        if (pct >= 70.0) return .especialista;
        if (pct >= 50.0) return .competente;
        if (pct >= 30.0) return .aprendiz;
        return .novato;
    }

    pub fn titulo(self: Ranking) []const u8 {
        return switch (self) {
            .mestre => "Mestre Supremo",
            .especialista => "Especialista",
            .competente => "Competente",
            .aprendiz => "Aprendiz",
            .novato => "Novato",
        };
    }

    pub fn emoji(self: Ranking) []const u8 {
        return switch (self) {
            .mestre => "[*****]",
            .especialista => "[****-]",
            .competente => "[***--]",
            .aprendiz => "[**---]",
            .novato => "[*----]",
        };
    }
};

Decisao de design: Usamos u2 para o indice da resposta correta. Como temos exatamente 4 alternativas (0-3), um inteiro de 2 bits e suficiente. Isso documenta no tipo que o valor nunca sera maior que 3.

Passo 3: Banco de Perguntas

/// Banco de perguntas compilado estaticamente.
/// Em Zig, arrays const em escopo de arquivo sao armazenados
/// na secao .rodata do binario — sem alocacao em runtime.
const perguntas = [_]Pergunta{
    .{
        .texto = "Qual é o elemento químico com símbolo 'O'?",
        .alternativas = .{ "Ouro", "Oxigênio", "Ósmio", "Oganesson" },
        .resposta_correta = 1,
        .categoria = .ciencia,
        .dificuldade = 1,
    },
    .{
        .texto = "Em que ano o Brasil se tornou independente?",
        .alternativas = .{ "1808", "1822", "1889", "1500" },
        .resposta_correta = 1,
        .categoria = .historia,
        .dificuldade = 1,
    },
    .{
        .texto = "Quem criou a linguagem de programação Zig?",
        .alternativas = .{ "Graydon Hoare", "Andrew Kelley", "Bjarne Stroustrup", "Guido van Rossum" },
        .resposta_correta = 1,
        .categoria = .tecnologia,
        .dificuldade = 2,
    },
    .{
        .texto = "Qual é a velocidade da luz no vácuo (aprox.)?",
        .alternativas = .{ "300.000 km/s", "150.000 km/s", "1.000.000 km/s", "30.000 km/s" },
        .resposta_correta = 0,
        .categoria = .ciencia,
        .dificuldade = 2,
    },
    .{
        .texto = "Qual civilização construiu Machu Picchu?",
        .alternativas = .{ "Maia", "Asteca", "Inca", "Olmeca" },
        .resposta_correta = 2,
        .categoria = .historia,
        .dificuldade = 2,
    },
    .{
        .texto = "O que significa 'comptime' em Zig?",
        .alternativas = .{ "Compile Time", "Computer Time", "Complete Time", "Compact Time" },
        .resposta_correta = 0,
        .categoria = .tecnologia,
        .dificuldade = 2,
    },
    .{
        .texto = "Qual é o maior osso do corpo humano?",
        .alternativas = .{ "Úmero", "Tíbia", "Fêmur", "Fíbula" },
        .resposta_correta = 2,
        .categoria = .ciencia,
        .dificuldade = 1,
    },
    .{
        .texto = "Em que ano começou a Primeira Guerra Mundial?",
        .alternativas = .{ "1912", "1914", "1916", "1918" },
        .resposta_correta = 1,
        .categoria = .historia,
        .dificuldade = 1,
    },
    .{
        .texto = "Qual sistema operacional Zig usa como target padrão de compilação cruzada?",
        .alternativas = .{ "Windows", "macOS", "Linux (musl)", "FreeBSD" },
        .resposta_correta = 2,
        .categoria = .tecnologia,
        .dificuldade = 3,
    },
    .{
        .texto = "Qual partícula subatômica tem carga negativa?",
        .alternativas = .{ "Próton", "Nêutron", "Elétron", "Fóton" },
        .resposta_correta = 2,
        .categoria = .ciencia,
        .dificuldade = 1,
    },
    .{
        .texto = "Quem pintou a Mona Lisa?",
        .alternativas = .{ "Michelangelo", "Rafael", "Leonardo da Vinci", "Donatello" },
        .resposta_correta = 2,
        .categoria = .historia,
        .dificuldade = 1,
    },
    .{
        .texto = "O que é um 'allocator' em Zig?",
        .alternativas = .{ "Um tipo de loop", "Um gerenciador de memória", "Um tipo de erro", "Um compilador" },
        .resposta_correta = 1,
        .categoria = .tecnologia,
        .dificuldade = 2,
    },
};

Passo 4: Embaralhamento

Para tornar o quiz mais interessante, embaralhamos as perguntas a cada execucao.

/// Embaralha um array in-place usando o algoritmo Fisher-Yates.
/// Este é o algoritmo padrão para embaralhamento uniforme —
/// cada permutação tem a mesma probabilidade de ocorrer.
fn embaralhar(comptime T: type, slice: []T, rng: std.Random) void {
    if (slice.len <= 1) return;

    var i: usize = slice.len - 1;
    while (i > 0) : (i -= 1) {
        const j = rng.intRangeAtMost(usize, 0, i);
        const tmp = slice[i];
        slice[i] = slice[j];
        slice[j] = tmp;
    }
}

Passo 5: Logica do Jogo

/// Estado de uma sessao de quiz.
const SessaoQuiz = struct {
    acertos: u32 = 0,
    erros: u32 = 0,
    pontuacao: u32 = 0,
    sequencia: u32 = 0,       // Acertos consecutivos
    melhor_sequencia: u32 = 0,

    pub fn registrarResposta(self: *SessaoQuiz, correta: bool, dificuldade: u8) void {
        if (correta) {
            self.acertos += 1;
            self.sequencia += 1;
            if (self.sequencia > self.melhor_sequencia) {
                self.melhor_sequencia = self.sequencia;
            }
            // Pontos base * dificuldade + bonus de sequencia
            const base: u32 = 100 * @as(u32, dificuldade);
            const bonus: u32 = if (self.sequencia >= 3) 50 else 0;
            self.pontuacao += base + bonus;
        } else {
            self.erros += 1;
            self.sequencia = 0;
        }
    }

    pub fn total(self: *const SessaoQuiz) u32 {
        return self.acertos + self.erros;
    }

    pub fn percentual(self: *const SessaoQuiz) f64 {
        const t = self.total();
        if (t == 0) return 0.0;
        return @as(f64, @floatFromInt(self.acertos)) / @as(f64, @floatFromInt(t)) * 100.0;
    }
};

/// Apresenta uma pergunta e retorna se o jogador acertou.
fn apresentarPergunta(
    p: *const Pergunta,
    numero: usize,
    total_perguntas: usize,
    reader: anytype,
    writer: anytype,
) !bool {
    const reset = "\x1b[0m";
    const cor = p.categoria.cor();

    try writer.print("\n{s}[{s}]{s} Pergunta {d}/{d} (dificuldade: {d}/3)\n", .{
        cor, p.categoria.nome(), reset,
        numero, total_perguntas, p.dificuldade,
    });
    try writer.print("\n  {s}\n\n", .{p.texto});

    // Exibir alternativas
    const letras = "ABCD";
    for (p.alternativas, 0..) |alt, i| {
        try writer.print("    {c}) {s}\n", .{ letras[i], alt });
    }

    try writer.print("\n  Sua resposta (A/B/C/D): ", .{});

    var buf: [64]u8 = undefined;
    const linha = reader.readUntilDelimiterOrEof(&buf, '\n') catch return false orelse return false;
    const entrada = mem.trim(u8, linha, " \t\r\n");

    if (entrada.len != 1) {
        try writer.print("  Resposta inválida!\n", .{});
        return false;
    }

    const resposta: u8 = switch (entrada[0]) {
        'A', 'a' => 0,
        'B', 'b' => 1,
        'C', 'c' => 2,
        'D', 'd' => 3,
        else => {
            try writer.print("  Resposta inválida!\n", .{});
            return false;
        },
    };

    const correta = resposta == p.resposta_correta;
    if (correta) {
        try writer.print("  \x1b[32mCorreto!\x1b[0m\n", .{});
    } else {
        try writer.print("  \x1b[31mErrado!\x1b[0m A resposta era: {c}) {s}\n", .{
            letras[p.resposta_correta], p.alternativas[p.resposta_correta],
        });
    }

    return correta;
}

Passo 6: Loop Principal e Resultado

pub fn main() !void {
    const stdout = io.getStdOut().writer();
    const stdin = io.getStdIn().reader();

    // Inicializa PRNG
    var prng = std.Random.DefaultPrng.init(blk: {
        var seed: u64 = undefined;
        std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable;
        break :blk seed;
    });
    const rng = prng.random();

    try stdout.print(
        \\
        \\  =============================================
        \\       QUIZ ZIG - Teste Seus Conhecimentos
        \\  =============================================
        \\
        \\  Categorias: Ciencia, Historia, Tecnologia
        \\  Acerte em sequencia para ganhar bonus!
        \\
        \\  Pressione ENTER para comecar...
        \\
    , .{});

    var buf: [64]u8 = undefined;
    _ = stdin.readUntilDelimiterOrEof(&buf, '\n') catch {};

    // Copia e embaralha perguntas
    var perguntas_jogo = perguntas;
    embaralhar(Pergunta, &perguntas_jogo, rng);

    var sessao = SessaoQuiz{};
    const total_p = perguntas_jogo.len;

    for (perguntas_jogo, 0..) |*p, i| {
        const correta = try apresentarPergunta(p, i + 1, total_p, stdin, stdout);
        sessao.registrarResposta(correta, p.dificuldade);

        // Mostrar sequencia se >= 2
        if (sessao.sequencia >= 2) {
            try stdout.print("  Sequencia: {d} acertos seguidos!\n", .{sessao.sequencia});
        }
    }

    // Resultado final
    const pct = sessao.percentual();
    const ranking = Ranking.dePercentual(pct);

    try stdout.print(
        \\
        \\  =============================================
        \\           RESULTADO FINAL
        \\  =============================================
        \\
        \\  Acertos:          {d}/{d}
        \\  Percentual:       {d:.1}%
        \\  Pontuacao:        {d} pontos
        \\  Melhor sequencia: {d} acertos
        \\
        \\  Ranking: {s} {s}
        \\
        \\  =============================================
        \\
    , .{
        sessao.acertos, sessao.total(),
        pct,
        sessao.pontuacao,
        sessao.melhor_sequencia,
        ranking.emoji(), ranking.titulo(),
    });
}

Passo 7: Testes

test "ranking de percentual" {
    try std.testing.expectEqual(Ranking.mestre, Ranking.dePercentual(95.0));
    try std.testing.expectEqual(Ranking.especialista, Ranking.dePercentual(75.0));
    try std.testing.expectEqual(Ranking.competente, Ranking.dePercentual(55.0));
    try std.testing.expectEqual(Ranking.aprendiz, Ranking.dePercentual(35.0));
    try std.testing.expectEqual(Ranking.novato, Ranking.dePercentual(10.0));
}

test "sessao quiz - acerto com bonus" {
    var sessao = SessaoQuiz{};
    sessao.registrarResposta(true, 1);
    sessao.registrarResposta(true, 1);
    sessao.registrarResposta(true, 1); // 3a consecutiva = bonus

    try std.testing.expectEqual(@as(u32, 3), sessao.acertos);
    try std.testing.expectEqual(@as(u32, 350), sessao.pontuacao); // 100+100+150
    try std.testing.expectEqual(@as(u32, 3), sessao.melhor_sequencia);
}

test "sessao quiz - erro reseta sequencia" {
    var sessao = SessaoQuiz{};
    sessao.registrarResposta(true, 1);
    sessao.registrarResposta(true, 1);
    sessao.registrarResposta(false, 1);

    try std.testing.expectEqual(@as(u32, 0), sessao.sequencia);
    try std.testing.expectEqual(@as(u32, 2), sessao.melhor_sequencia);
}

test "embaralhar preserva elementos" {
    var prng_test = std.Random.DefaultPrng.init(42);
    var arr = [_]u32{ 1, 2, 3, 4, 5 };
    embaralhar(u32, &arr, prng_test.random());

    // Verifica que todos os elementos ainda existem
    var soma: u32 = 0;
    for (arr) |v| soma += v;
    try std.testing.expectEqual(@as(u32, 15), soma);
}

Compilando e Executando

zig build test   # Rodar testes
zig build run    # Jogar o quiz

Desafios Extras

  1. Perguntas de arquivo — carregue perguntas de um arquivo JSON usando parsing JSON
  2. Temporizador — adicione um limite de tempo por pergunta
  3. Categorias selecionaveis — deixe o jogador escolher quais categorias quer
  4. Persistencia de recordes — salve os melhores scores em arquivo

Conceitos Aprendidos

  • Modelagem de dados com struct e enum
  • Arrays fixos de structs em comptime
  • Algoritmo de embaralhamento Fisher-Yates
  • Tipos inteiros de tamanho preciso (u2)
  • Sequencias ANSI para cores no terminal
  • Testes unitarios nativos

Proximos Passos

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.