Jogo de Adivinhação em Zig — Tutorial Passo a Passo
Neste tutorial, vamos construir um jogo de adivinhar números no terminal. O computador escolhe um número aleatório e o jogador tenta descobri-lo com dicas de “maior” ou “menor”. Parece simples, mas é uma excelente introdução a geradores de números aleatórios, loops de jogo e gerenciamento de estado.
O Que Vamos Construir
Nosso jogo vai:
- Gerar um número aleatório dentro de um intervalo configurável
- Dar dicas ao jogador (maior/menor/quente/frio)
- Contar tentativas e calcular pontuação
- Oferecer níveis de dificuldade (fácil, médio, difícil)
- Manter estatísticas da sessão de jogo
Pré-requisitos
- Zig 0.13+ instalado
- Conhecimentos básicos de Zig
Passo 1: Configuração do Jogo
const std = @import("std");
const io = std.io;
const fmt = std.fmt;
const mem = std.mem;
const rand = std.rand;
/// Níveis de dificuldade do jogo.
/// Cada nível define o intervalo de números e o máximo de tentativas.
/// Usamos um enum com dados associados via funções — isso é mais
/// idiomático em Zig do que um struct com campos opcionais.
const Dificuldade = enum {
facil,
medio,
dificil,
pub fn intervalo(self: Dificuldade) struct { min: u32, max: u32 } {
return switch (self) {
.facil => .{ .min = 1, .max = 50 },
.medio => .{ .min = 1, .max = 100 },
.dificil => .{ .min = 1, .max = 500 },
};
}
pub fn maxTentativas(self: Dificuldade) u32 {
return switch (self) {
.facil => 10,
.medio => 7,
.dificil => 9,
};
}
pub fn nome(self: Dificuldade) []const u8 {
return switch (self) {
.facil => "Fácil",
.medio => "Médio",
.dificil => "Difícil",
};
}
pub fn pontoBase(self: Dificuldade) u32 {
return switch (self) {
.facil => 100,
.medio => 250,
.dificil => 500,
};
}
};
/// Estatísticas da sessão de jogo.
const Estatisticas = struct {
jogos: u32 = 0,
vitorias: u32 = 0,
total_tentativas: u32 = 0,
pontuacao_total: u32 = 0,
melhor_jogo: ?u32 = null, // Menor número de tentativas
pub fn registrar(self: *Estatisticas, venceu: bool, tentativas: u32, pontos: u32) void {
self.jogos += 1;
self.total_tentativas += tentativas;
if (venceu) {
self.vitorias += 1;
self.pontuacao_total += pontos;
if (self.melhor_jogo == null or tentativas < self.melhor_jogo.?) {
self.melhor_jogo = tentativas;
}
}
}
pub fn exibir(self: *const Estatisticas, writer: anytype) !void {
try writer.print(
\\
\\ === Estatísticas ===
\\ Jogos: {d}
\\ Vitórias: {d} ({d:.0}%)
\\ Total de tentativas: {d}
\\ Pontuação total: {d}
, .{
self.jogos,
self.vitorias,
if (self.jogos > 0) @as(f64, @floatFromInt(self.vitorias)) / @as(f64, @floatFromInt(self.jogos)) * 100.0 else 0.0,
self.total_tentativas,
self.pontuacao_total,
});
if (self.melhor_jogo) |melhor| {
try writer.print("\n Melhor jogo: {d} tentativa(s)\n", .{melhor});
}
try writer.print("\n", .{});
}
};
Passo 2: Lógica de Dicas
/// Tipo de dica baseada na proximidade do palpite.
const Dica = enum {
acertou,
muito_quente, // Diferença <= 5
quente, // Diferença <= 15
morno, // Diferença <= 30
frio, // Diferença > 30
pub fn mensagem(self: Dica, maior: bool) []const u8 {
return switch (self) {
.acertou => "ACERTOU!",
.muito_quente => if (maior) "Muito perto! Um pouco MAIOR..." else "Muito perto! Um pouco MENOR...",
.quente => if (maior) "Quente! Tente um número MAIOR." else "Quente! Tente um número MENOR.",
.morno => if (maior) "Morno. O número é MAIOR." else "Morno. O número é MENOR.",
.frio => if (maior) "Frio! O número é bem MAIOR." else "Frio! O número é bem MENOR.",
};
}
};
/// Determina a dica baseada na diferença entre palpite e resposta.
fn calcularDica(palpite: u32, resposta: u32) Dica {
if (palpite == resposta) return .acertou;
const diff = if (palpite > resposta) palpite - resposta else resposta - palpite;
if (diff <= 5) return .muito_quente;
if (diff <= 15) return .quente;
if (diff <= 30) return .morno;
return .frio;
}
Passo 3: Loop de Uma Rodada
/// Executa uma rodada completa do jogo.
/// Retorna a pontuação obtida (0 se perdeu).
fn jogarRodada(
dificuldade: Dificuldade,
reader: anytype,
writer: anytype,
prng: *rand.DefaultPrng,
) !struct { venceu: bool, tentativas: u32, pontos: u32 } {
const range = dificuldade.intervalo();
const max_tent = dificuldade.maxTentativas();
// Gera número aleatório no intervalo
const resposta = prng.random().intRangeAtMost(u32, range.min, range.max);
try writer.print(
\\
\\ Pensei em um número entre {d} e {d}.
\\ Você tem {d} tentativas. Boa sorte!
\\
, .{ range.min, range.max, max_tent });
var buf: [64]u8 = undefined;
var tentativa: u32 = 0;
while (tentativa < max_tent) : (tentativa += 1) {
const restantes = max_tent - tentativa;
try writer.print("\n [{d} restante(s)] Seu palpite: ", .{restantes});
const linha = reader.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse break;
const limpa = mem.trim(u8, linha, " \t\r\n");
const palpite = fmt.parseInt(u32, limpa, 10) catch {
try writer.print(" Digite um número válido!\n", .{});
continue;
};
if (palpite < range.min or palpite > range.max) {
try writer.print(" Fora do intervalo! ({d} a {d})\n", .{ range.min, range.max });
continue;
}
const dica = calcularDica(palpite, resposta);
const maior = resposta > palpite;
try writer.print(" {s}\n", .{dica.mensagem(maior)});
if (dica == .acertou) {
const tentativas_usadas = tentativa + 1;
// Pontuação: base * (tentativas restantes / total)
const base = dificuldade.pontoBase();
const bonus = base * (max_tent - tentativa) / max_tent;
const pontos = bonus;
try writer.print(
\\
\\ Parabéns! Você acertou em {d} tentativa(s)!
\\ Pontuação: +{d} pontos
\\
, .{ tentativas_usadas, pontos });
return .{ .venceu = true, .tentativas = tentativas_usadas, .pontos = pontos };
}
}
try writer.print(
\\
\\ Suas tentativas acabaram! O número era {d}.
\\
, .{resposta});
return .{ .venceu = false, .tentativas = max_tent, .pontos = 0 };
}
Passo 4: Loop Principal
pub fn main() !void {
const stdout = io.getStdOut().writer();
const stdin = io.getStdIn().reader();
// Inicializa o PRNG com seed do sistema.
// Usamos DefaultPrng em vez de crypto.random porque para um jogo
// não precisamos de entropia criptográfica — apenas imprevisibilidade
// suficiente para diversão. PRNG é mais rápido e adequado aqui.
var prng = rand.DefaultPrng.init(blk: {
var seed: u64 = undefined;
std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable;
break :blk seed;
});
var stats = Estatisticas{};
var buf: [64]u8 = undefined;
try stdout.print(
\\
\\ ╔══════════════════════════════════╗
\\ ║ Jogo de Adivinhação Zig ║
\\ ║ Adivinhe o número secreto! ║
\\ ╚══════════════════════════════════╝
\\
, .{});
while (true) {
try stdout.print(
\\
\\ Escolha a dificuldade:
\\ [1] Fácil (1-50, 10 tentativas)
\\ [2] Médio (1-100, 7 tentativas)
\\ [3] Difícil (1-500, 9 tentativas)
\\ [4] Ver estatísticas
\\ [5] Sair
\\
\\ Opção:
, .{});
const linha = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse break;
const opcao = mem.trim(u8, linha, " \t\r\n");
if (mem.eql(u8, opcao, "5") or mem.eql(u8, opcao, "sair")) {
try stats.exibir(stdout);
try stdout.print(" Obrigado por jogar!\n", .{});
break;
}
if (mem.eql(u8, opcao, "4")) {
try stats.exibir(stdout);
continue;
}
const dificuldade: Dificuldade = if (mem.eql(u8, opcao, "1"))
.facil
else if (mem.eql(u8, opcao, "2"))
.medio
else if (mem.eql(u8, opcao, "3"))
.dificil
else {
try stdout.print(" Opção inválida.\n", .{});
continue;
};
try stdout.print("\n Dificuldade: {s}\n", .{dificuldade.nome()});
const resultado = try jogarRodada(dificuldade, stdin, stdout, &prng);
stats.registrar(resultado.venceu, resultado.tentativas, resultado.pontos);
}
}
Passo 5: Testes
test "calcular dica - acerto" {
try std.testing.expectEqual(Dica.acertou, calcularDica(42, 42));
}
test "calcular dica - muito quente" {
try std.testing.expectEqual(Dica.muito_quente, calcularDica(40, 42));
try std.testing.expectEqual(Dica.muito_quente, calcularDica(44, 42));
}
test "calcular dica - frio" {
try std.testing.expectEqual(Dica.frio, calcularDica(1, 100));
}
test "registrar estatísticas" {
var stats = Estatisticas{};
stats.registrar(true, 3, 200);
stats.registrar(false, 10, 0);
stats.registrar(true, 5, 150);
try std.testing.expectEqual(@as(u32, 3), stats.jogos);
try std.testing.expectEqual(@as(u32, 2), stats.vitorias);
try std.testing.expectEqual(@as(u32, 350), stats.pontuacao_total);
try std.testing.expectEqual(@as(u32, 3), stats.melhor_jogo.?);
}
Compilando e Executando
zig build test
zig build run
Conceitos Aprendidos
- PRNG vs aleatoriedade criptográfica e quando usar cada um
- Enums com funções associadas para dados de configuração
- Structs anônimos como tipos de retorno
- Loop de jogo com gerenciamento de estado
- Cálculo de pontuação e estatísticas
Próximos Passos
- Aprenda mais sobre números aleatórios em Zig
- Explore o módulo std.rand da biblioteca padrão
- Construa o próximo projeto: Contador de Palavras