Game of Life em Zig — Tutorial Passo a Passo

Game of Life em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir o Conway’s Game of Life em Zig com renderização no terminal usando ANSI escape codes. O Game of Life é um autômato celular que gera padrões complexos a partir de regras simples.

O Que Vamos Construir

Nosso Game of Life vai:

  • Implementar as regras clássicas do Conway’s Game of Life
  • Renderizar no terminal com ANSI escape codes e cores
  • Incluir padrões clássicos pré-carregados (glider, pulsar, spaceship)
  • Suportar grid toroidal (bordas se conectam)
  • Permitir controle de velocidade e pausa
  • Usar double buffering para atualização suave

Por Que Este Projeto?

O Game of Life é um exemplo fascinante de emergência: regras simples gerando complexidade. Implementá-lo nos ensina sobre autômatos celulares, rendering no terminal, double buffering e gerenciamento de estado. Em Zig, fazemos com arrays fixos e zero alocação dinâmica.

Pré-requisitos

Passo 1: Estrutura do Projeto

mkdir game-of-life
cd game-of-life
zig init

Passo 2: O Grid do Game of Life

As regras são simples:

  1. Célula viva com 2 ou 3 vizinhos: sobrevive
  2. Célula morta com exatamente 3 vizinhos: nasce
  3. Todos os outros casos: morre
const std = @import("std");
const io = std.io;
const mem = std.mem;
const time = std.time;
const posix = std.posix;
const fmt = std.fmt;

const LARGURA = 60;
const ALTURA = 30;

/// Grid do Game of Life com double buffering.
/// Usa dois buffers para evitar que a atualização de uma célula
/// afete o cálculo das vizinhas na mesma geração.
const Grid = struct {
    celulas: [2][ALTURA][LARGURA]bool,
    buffer_atual: u1,
    geracao: u64,
    celulas_vivas: u32,

    const Self = @This();

    pub fn init() Self {
        var grid = Self{
            .celulas = [_][ALTURA][LARGURA]bool{
                [_][LARGURA]bool{[_]bool{false} ** LARGURA} ** ALTURA,
                [_][LARGURA]bool{[_]bool{false} ** LARGURA} ** ALTURA,
            },
            .buffer_atual = 0,
            .geracao = 0,
            .celulas_vivas = 0,
        };
        _ = &grid;
        return grid;
    }

    /// Retorna o estado de uma célula (com wrap toroidal).
    pub fn getCelula(self: *const Self, x: i32, y: i32) bool {
        const wx: usize = @intCast(@mod(x, @as(i32, LARGURA)));
        const wy: usize = @intCast(@mod(y, @as(i32, ALTURA)));
        return self.celulas[self.buffer_atual][wy][wx];
    }

    /// Define o estado de uma célula no buffer atual.
    pub fn setCelula(self: *Self, x: usize, y: usize, viva: bool) void {
        if (x < LARGURA and y < ALTURA) {
            self.celulas[self.buffer_atual][y][x] = viva;
        }
    }

    /// Conta vizinhos vivos de uma célula.
    fn contarVizinhos(self: *const Self, x: i32, y: i32) u8 {
        var count: u8 = 0;
        const offsets = [_][2]i32{
            .{ -1, -1 }, .{ 0, -1 }, .{ 1, -1 },
            .{ -1, 0 },              .{ 1, 0 },
            .{ -1, 1 },  .{ 0, 1 },  .{ 1, 1 },
        };
        for (offsets) |off| {
            if (self.getCelula(x + off[0], y + off[1])) {
                count += 1;
            }
        }
        return count;
    }

    /// Avança uma geração aplicando as regras do Game of Life.
    pub fn avancar(self: *Self) void {
        const proximo: u1 = self.buffer_atual ^ 1;
        var vivas: u32 = 0;

        for (0..ALTURA) |y| {
            for (0..LARGURA) |x| {
                const vizinhos = self.contarVizinhos(@intCast(x), @intCast(y));
                const viva = self.celulas[self.buffer_atual][y][x];

                const nova_viva = if (viva)
                    (vizinhos == 2 or vizinhos == 3)
                else
                    (vizinhos == 3);

                self.celulas[proximo][y][x] = nova_viva;
                if (nova_viva) vivas += 1;
            }
        }

        self.buffer_atual = proximo;
        self.geracao += 1;
        self.celulas_vivas = vivas;
    }

    /// Limpa todo o grid.
    pub fn limpar(self: *Self) void {
        for (0..ALTURA) |y| {
            for (0..LARGURA) |x| {
                self.celulas[self.buffer_atual][y][x] = false;
            }
        }
        self.geracao = 0;
        self.celulas_vivas = 0;
    }
};

Passo 3: Padrões Clássicos

/// Padrões clássicos do Game of Life.
const Padroes = struct {
    /// Glider: se move diagonalmente.
    pub fn glider(grid: *Grid, x: usize, y: usize) void {
        grid.setCelula(x + 1, y, true);
        grid.setCelula(x + 2, y + 1, true);
        grid.setCelula(x, y + 2, true);
        grid.setCelula(x + 1, y + 2, true);
        grid.setCelula(x + 2, y + 2, true);
    }

    /// Pulsar: oscilador de período 3.
    pub fn pulsar(grid: *Grid, x: usize, y: usize) void {
        const coords = [_][2]usize{
            // Top
            .{ 2, 0 }, .{ 3, 0 }, .{ 4, 0 }, .{ 8, 0 }, .{ 9, 0 }, .{ 10, 0 },
            // Upper
            .{ 0, 2 }, .{ 5, 2 }, .{ 7, 2 }, .{ 12, 2 },
            .{ 0, 3 }, .{ 5, 3 }, .{ 7, 3 }, .{ 12, 3 },
            .{ 0, 4 }, .{ 5, 4 }, .{ 7, 4 }, .{ 12, 4 },
            .{ 2, 5 }, .{ 3, 5 }, .{ 4, 5 }, .{ 8, 5 }, .{ 9, 5 }, .{ 10, 5 },
            // Lower (mirror)
            .{ 2, 7 }, .{ 3, 7 }, .{ 4, 7 }, .{ 8, 7 }, .{ 9, 7 }, .{ 10, 7 },
            .{ 0, 8 }, .{ 5, 8 }, .{ 7, 8 }, .{ 12, 8 },
            .{ 0, 9 }, .{ 5, 9 }, .{ 7, 9 }, .{ 12, 9 },
            .{ 0, 10 }, .{ 5, 10 }, .{ 7, 10 }, .{ 12, 10 },
            .{ 2, 12 }, .{ 3, 12 }, .{ 4, 12 }, .{ 8, 12 }, .{ 9, 12 }, .{ 10, 12 },
        };
        for (coords) |c| {
            grid.setCelula(x + c[0], y + c[1], true);
        }
    }

    /// LWSS (Lightweight Spaceship): se move horizontalmente.
    pub fn lwss(grid: *Grid, x: usize, y: usize) void {
        grid.setCelula(x + 1, y, true);
        grid.setCelula(x + 4, y, true);
        grid.setCelula(x, y + 1, true);
        grid.setCelula(x, y + 2, true);
        grid.setCelula(x + 4, y + 2, true);
        grid.setCelula(x, y + 3, true);
        grid.setCelula(x + 1, y + 3, true);
        grid.setCelula(x + 2, y + 3, true);
        grid.setCelula(x + 3, y + 3, true);
    }

    /// Gosper Glider Gun: gera gliders continuamente.
    pub fn gliderGun(grid: *Grid, x: usize, y: usize) void {
        const coords = [_][2]usize{
            .{ 0, 4 }, .{ 0, 5 }, .{ 1, 4 }, .{ 1, 5 },
            .{ 10, 4 }, .{ 10, 5 }, .{ 10, 6 },
            .{ 11, 3 }, .{ 11, 7 },
            .{ 12, 2 }, .{ 12, 8 }, .{ 13, 2 }, .{ 13, 8 },
            .{ 14, 5 },
            .{ 15, 3 }, .{ 15, 7 },
            .{ 16, 4 }, .{ 16, 5 }, .{ 16, 6 },
            .{ 17, 5 },
            .{ 20, 2 }, .{ 20, 3 }, .{ 20, 4 },
            .{ 21, 2 }, .{ 21, 3 }, .{ 21, 4 },
            .{ 22, 1 }, .{ 22, 5 },
            .{ 24, 0 }, .{ 24, 1 }, .{ 24, 5 }, .{ 24, 6 },
            .{ 34, 2 }, .{ 34, 3 }, .{ 35, 2 }, .{ 35, 3 },
        };
        for (coords) |c| {
            grid.setCelula(x + c[0], y + c[1], true);
        }
    }

    /// Padrão aleatório.
    pub fn aleatorio(grid: *Grid) void {
        // Usar timestamp como seed pseudo-aleatória
        var seed: u64 = @intCast(time.milliTimestamp() & 0xFFFFFFFF);
        for (0..ALTURA) |y| {
            for (0..LARGURA) |x| {
                seed = seed *% 6364136223846793005 +% 1442695040888963407;
                grid.setCelula(x, y, (seed >> 33) % 4 == 0); // ~25% de densidade
            }
        }
    }
};

Passo 4: Renderização no Terminal

/// ANSI escape codes para controle do terminal.
const Ansi = struct {
    const CLEAR = "\x1b[2J";
    const HOME = "\x1b[H";
    const HIDE_CURSOR = "\x1b[?25l";
    const SHOW_CURSOR = "\x1b[?25h";
    const BOLD = "\x1b[1m";
    const RESET = "\x1b[0m";
    const GREEN = "\x1b[32m";
    const DARK_GREEN = "\x1b[2;32m";
    const CYAN = "\x1b[36m";
    const WHITE = "\x1b[37m";
};

/// Renderiza o grid no terminal.
fn renderizar(grid: *const Grid, writer: anytype) !void {
    // Mover cursor para o início
    try writer.writeAll(Ansi.HOME);

    // Header
    try writer.print("{s}{s}  Game of Life  |  Geracao: {d}  |  Vivas: {d}/{d}  |  {d}x{d}{s}\n", .{
        Ansi.BOLD, Ansi.CYAN,
        grid.geracao, grid.celulas_vivas, LARGURA * ALTURA,
        LARGURA, ALTURA, Ansi.RESET,
    });

    // Borda superior
    try writer.writeAll("  +");
    for (0..LARGURA) |_| try writer.writeAll("-");
    try writer.writeAll("+\n");

    // Grid
    for (0..ALTURA) |y| {
        try writer.writeAll("  |");
        for (0..LARGURA) |x| {
            if (grid.celulas[grid.buffer_atual][y][x]) {
                // Célula viva: colorida
                const vizinhos = grid.contarVizinhos(@intCast(x), @intCast(y));
                if (vizinhos == 2 or vizinhos == 3) {
                    try writer.print("{s}#{s}", .{ Ansi.GREEN, Ansi.RESET });
                } else {
                    try writer.print("{s}#{s}", .{ Ansi.DARK_GREEN, Ansi.RESET });
                }
            } else {
                try writer.writeAll(" ");
            }
        }
        try writer.writeAll("|\n");
    }

    // Borda inferior
    try writer.writeAll("  +");
    for (0..LARGURA) |_| try writer.writeAll("-");
    try writer.writeAll("+\n");

    // Controles
    try writer.print("  Controles: [Ctrl+C] sair\n", .{});
}

Passo 5: Função Main

pub fn main() !void {
    const stdout = io.getStdOut().writer();
    const args = try std.process.argsAlloc(std.heap.page_allocator);
    defer std.process.argsFree(std.heap.page_allocator, args);

    var grid = Grid.init();

    // Escolher padrão
    var padrao: []const u8 = "random";
    var velocidade_ms: u64 = 100;

    if (args.len >= 2) padrao = args[1];
    if (args.len >= 3) {
        velocidade_ms = fmt.parseInt(u64, args[2], 10) catch 100;
    }

    if (mem.eql(u8, padrao, "glider")) {
        Padroes.glider(&grid, 5, 5);
        Padroes.glider(&grid, 15, 10);
        Padroes.glider(&grid, 25, 3);
    } else if (mem.eql(u8, padrao, "pulsar")) {
        Padroes.pulsar(&grid, 23, 8);
    } else if (mem.eql(u8, padrao, "lwss")) {
        Padroes.lwss(&grid, 5, 12);
    } else if (mem.eql(u8, padrao, "gun")) {
        Padroes.gliderGun(&grid, 1, 3);
    } else {
        Padroes.aleatorio(&grid);
    }

    // Esconder cursor
    try stdout.writeAll(Ansi.HIDE_CURSOR);
    try stdout.writeAll(Ansi.CLEAR);

    // Contagem de células vivas inicial
    var vivas: u32 = 0;
    for (0..ALTURA) |y| {
        for (0..LARGURA) |x| {
            if (grid.celulas[grid.buffer_atual][y][x]) vivas += 1;
        }
    }
    grid.celulas_vivas = vivas;

    // Loop principal
    while (true) {
        try renderizar(&grid, stdout);
        time.sleep(velocidade_ms * time.ns_per_ms);
        grid.avancar();

        // Parar se não há mais células vivas
        if (grid.celulas_vivas == 0) {
            try stdout.print("\n  Todas as celulas morreram na geracao {d}.\n", .{grid.geracao});
            break;
        }

        // Limite de gerações para demonstração
        if (grid.geracao >= 1000) {
            try stdout.print("\n  Limite de 1000 geracoes atingido.\n", .{});
            break;
        }
    }

    // Restaurar cursor
    try stdout.writeAll(Ansi.SHOW_CURSOR);
}

Testes

test "grid - celula viva e morta" {
    var grid = Grid.init();
    grid.setCelula(5, 5, true);
    try std.testing.expect(grid.getCelula(5, 5));
    try std.testing.expect(!grid.getCelula(4, 4));
}

test "grid - wrap toroidal" {
    var grid = Grid.init();
    grid.setCelula(0, 0, true);
    try std.testing.expect(grid.getCelula(-@as(i32, LARGURA), -@as(i32, ALTURA)));
}

test "grid - regra sobrevivencia" {
    var grid = Grid.init();
    // Bloco 2x2 (still life)
    grid.setCelula(1, 1, true);
    grid.setCelula(2, 1, true);
    grid.setCelula(1, 2, true);
    grid.setCelula(2, 2, true);

    grid.avancar();

    // Bloco deve permanecer
    try std.testing.expect(grid.getCelula(1, 1));
    try std.testing.expect(grid.getCelula(2, 1));
    try std.testing.expect(grid.getCelula(1, 2));
    try std.testing.expect(grid.getCelula(2, 2));
}

test "grid - regra nascimento" {
    var grid = Grid.init();
    // Blinker (oscilador período 2)
    grid.setCelula(1, 0, true);
    grid.setCelula(1, 1, true);
    grid.setCelula(1, 2, true);

    grid.avancar();

    // Deve virar horizontal
    try std.testing.expect(grid.getCelula(0, 1));
    try std.testing.expect(grid.getCelula(1, 1));
    try std.testing.expect(grid.getCelula(2, 1));
    try std.testing.expect(!grid.getCelula(1, 0));
    try std.testing.expect(!grid.getCelula(1, 2));
}

test "grid - contar vizinhos" {
    var grid = Grid.init();
    grid.setCelula(0, 0, true);
    grid.setCelula(1, 0, true);
    grid.setCelula(0, 1, true);

    const vizinhos = grid.contarVizinhos(1, 1);
    try std.testing.expectEqual(@as(u8, 3), vizinhos);
}

Compilando e Executando

# Padrão aleatório
zig build run

# Gliders
zig build run -- glider

# Pulsar
zig build run -- pulsar

# Gosper Glider Gun
zig build run -- gun

# Velocidade customizada (ms por frame)
zig build run -- random 50

# Rodar testes
zig build test

Conceitos Aprendidos

  • Autômatos celulares e regras de Conway
  • Double buffering para atualizações consistentes
  • ANSI escape codes para UI no terminal
  • Grid toroidal com aritmética modular
  • Arrays multidimensionais fixos sem alocação

Próximos Passos

Continue aprendendo Zig

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