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
- Zig 0.13+ instalado (guia de instalação)
- Terminal que suporta ANSI escape codes
- Familiaridade com arrays e loops
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:
- Célula viva com 2 ou 3 vizinhos: sobrevive
- Célula morta com exatamente 3 vizinhos: nasce
- 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
- Explore I/O de terminal para mais ANSI
- Veja o Ray Tracer para outra visualização
- Construa o Processador BMP para salvar frames
- Consulte arrays e loops na documentação