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
- Zig 0.13+ instalado (guia de instalação)
- Familiaridade com conceitos básicos de Zig (fundamentos)
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
- Perguntas de arquivo — carregue perguntas de um arquivo JSON usando parsing JSON
- Temporizador — adicione um limite de tempo por pergunta
- Categorias selecionaveis — deixe o jogador escolher quais categorias quer
- Persistencia de recordes — salve os melhores scores em arquivo
Conceitos Aprendidos
- Modelagem de dados com
structeenum - 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
- Aprenda mais sobre manipulacao de strings em Zig
- Explore o modulo std.Random da biblioteca padrao
- Construa o proximo projeto: Cifra de Cesar