Jogo de Adivinhação em Zig — Tutorial Passo a Passo

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

Continue aprendendo Zig

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