Pular para o conteúdo

Jogo de terminal

Jogo da velha com um adversário que não perde — minimax com poda.

Jogo é o projeto que mais cedo ensina a separar estado de apresentação: o tabuleiro é um dado, a jogada é uma função pura sobre ele, e o terminal só desenha. Com isso a IA consegue simular milhares de jogos sem imprimir nada.

PeçaO que ela exercita
cluster de 9 casaso estado inteiro do jogo
recursãoo minimax desce até o fim de cada linha de jogo
poda alfa-betaa mesma resposta visitando uma fração dos nós
função purajogar devolve um tabuleiro novo

Estrutura#

text
jogo-da-velha/
  src/
    tabuleiro.df   jogar, vencedor, casas livres
    ia.df          minimax
    tela.df        desenhar e ler a jogada
    main.df
  tests/
forge.toml
[project]
name = "jogo-da-velha"
version = "0.1.0"
description = "Jogo da velha com IA"
entry = "src/main.df"
dataforge = ">=1.1"

[dependencies]

[scripts]
start = "run src/main.df"
test = "test tests/"

O núcleo#

Este bloco roda sozinho — copie para um arquivo e rode dataforge run. Ele termina com assert, e é assim que esta página é conferida a cada build.

src/ia.df
steady LINHAS := [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]]

action vencedor(t):
    cycle l in LINHAS:
        given t[l[0]] is not " " and t[l[0]] is t[l[1]] and t[l[1]] is t[l[2]]:
            yield t[l[0]]
    yield void

action livres(t):
    yield [i cycle i in range(0, 9) given t[i] is " "]

action jogar(t, casa, peca):
    novo := [...t]
    novo[casa] := peca
    yield novo

action outro(p):
    yield "O" given p is "X" otherwise "X"

// Pontua do ponto de vista de 'eu'. A profundidade entra na conta para a
// IA preferir ganhar AGORA a ganhar daqui a tres jogadas.
action minimax(t, vez, eu, alfa, beta, prof):
    v := vencedor(t)
    given v is eu:
        yield 10 - prof
    given v is not void:
        yield prof - 10
    casas := livres(t)
    given len(casas) is 0:
        yield 0
    given vez is eu:
        melhor := -100
        cycle c in casas:
            melhor := max(melhor, minimax(jogar(t, c, vez), outro(vez), eu, alfa, beta, prof + 1))
            alfa := max(alfa, melhor)
            given alfa bigger_eq beta:
                halt
        yield melhor
    pior := 100
    cycle c in casas:
        pior := min(pior, minimax(jogar(t, c, vez), outro(vez), eu, alfa, beta, prof + 1))
        beta := min(beta, pior)
        given alfa bigger_eq beta:
            halt
    yield pior

action melhor_jogada(t, eu):
    melhor := -1000
    escolha := -1
    cycle c in livres(t):
        nota := minimax(jogar(t, c, eu), outro(eu), eu, -1000, 1000, 1)
        given nota bigger melhor:
            melhor := nota
            escolha := c
    yield escolha

action desenhar(t):
    yield [$" {t[0]} | {t[1]} | {t[2]}", $" {t[3]} | {t[4]} | {t[5]}", $" {t[6]} | {t[7]} | {t[8]}"].join("\n---+---+---\n")

// X tem duas em linha: a IA (O) precisa bloquear na casa 2.
t := ["X", "X", " ", " ", "O", " ", " ", " ", " "]
assert melhor_jogada(t, "O") is 2

// O tem duas em linha: ganhar vale mais que bloquear.
t2 := ["X", "X", " ", "O", "O", " ", "X", " ", " "]
assert melhor_jogada(t2, "O") is 5

// IA contra IA termina sempre empatada.
jogo := [" " cycle _ in range(0, 9)]
vez := "X"
persist vencedor(jogo) is void and len(livres(jogo)) bigger 0:
    jogo := jogar(jogo, melhor_jogada(jogo, vez), vez)
    vez := outro(vez)
out desenhar(jogo)
assert vencedor(jogo) is void

O teste#

No projeto, a regra mora em src/ e o teste a importa pelo caminho relativo — dataforge test tests/ descobre o arquivo sozinho.

tests/nucleo_test.df
adopt ../src/ia as IA

crucible "ia":
    trial "bloqueia a linha do adversario":
        t := ["X", "X", " ", " ", "O", " ", " ", " ", " "]
        expect IA.melhor_jogada(t, "O") is 2

As decisões#

DecisãoSem ela
o tabuleiro não sabe desenhara IA imprime cada jogo simulado
jogar devolve cópiaa simulação de uma linha estraga o tabuleiro da seguinte
a profundidade na notaa IA enrola: vê a vitória em uma e prefere a de três
poda alfa-betaa primeira jogada visita 549 mil nós em vez de ~20 mil

Para ir além#

  • Meça a poda com Arcane.Bench antes e depois — Medir.
  • Troque para um tabuleiro 4x4 e veja por que o minimax puro deixa de servir.
  • Um jogo de verdade tem laço de eventos: Arcane.Laco.

Volte para todos os tipos de projeto.