
Objetivo
Fazer aprendizado por reforço com nosso próprio motor em C++, sem usar ambientes externos como gym-tetris ou uma ROM de NES. O treino roda sem tela, e a tela em Qt só é conectada para ver os resultados.
O motor
- O tabuleiro tem 10 casas de largura e 26 de altura. As 4 linhas de cima são linhas ocultas onde as peças surgem, então só 22 linhas aparecem na tela e na observação.
- As peças são sorteadas com 7-bag. Os sete tipos são embaralhados e saem uma vez cada, então nenhuma peça fica muito tempo sem aparecer.
- Ao girar, ele tenta 7 posições em ordem, da posição original até as posições empurradas para a esquerda, para a direita e para cima, e coloca a peça na primeira que couber.
- Depois de um movimento, uma rotação ou de ficar parado, a peça desce uma casa sozinha. Por isso, o número de ações disponíveis por peça é igual à altura do tabuleiro.
- A recompensa é calculada no momento em que a peça trava. A composição da recompensa fica para o próximo post.
C API e ambiente Gym
O motor é uma classe C++, então o Python não consegue chamá-lo diretamente. Ele é encapsulado em 21 funções extern "C" e compilado como uma biblioteca compartilhada (libtetris_c_api.dylib), que o Python carrega com ctypes. Essa biblioteca é compilada sem Qt.
O LocalTetrisEnv monta um ambiente gymnasium com essas funções.
- Observação: 22×10 casas. Casas vazias são 0, e os blocos são 1 a 7 conforme o tipo. Blocos travados e a peça em movimento não são diferenciados.
- Ações: 7. Ficar parado, esquerda, direita, girar no sentido horário, girar no sentido anti-horário, descer uma casa, soltar de vez.
- Recompensa: o valor calculado pelo motor, usado do jeito que vem.
O motor conhece as próximas 5 peças, mas elas não foram incluídas na observação.
Tela em Qt e TCP
Ao abrir o app em Qt (tetris_rl) com --rl-port 5555, um servidor TCP é aberto. O app envia o estado do tabuleiro como uma linha de JSON, e o cliente devolve o número da ação em uma linha. O JSON contém o tabuleiro, o tipo, a posição e a rotação da peça atual e a lista das próximas peças. Esse canal é usado para rodar na tela um modelo treinado ou para conectar os clientes de LLM e VLM que aparecem mais adiante.
O código real desta etapa
A C API guarda um motor em um único handle, recebe o número da ação e avança um passo.
tetris/src/tetris_c_api.cpp
struct TetrisHandle {
TetrisEngine engine;
std::vector<uint8_t> render_grid;
explicit TetrisHandle(const TetrisEngineConfig& cfg) : engine(cfg) {}
};
// Control
void tetris_set_seed(TetrisHandle* h, uint64_t seed) { h->engine.set_seed(seed); }
void tetris_reset(TetrisHandle* h) { h->engine.reset(); }
// Step returns 1 if alive, 0 if game over
int tetris_step(TetrisHandle* h, int action) {
const auto a = static_cast<TetrisAction>(static_cast<int8_t>(action));
const bool alive = h->engine.step(a);
return alive ? 1 : 0;
}O ambiente em Python corta as 4 linhas ocultas do tabuleiro preenchido pelo motor e usa o resto como observação.
local_tetris_env.py
def _obs(self) -> np.ndarray:
written = int(self._lib._dll.tetris_fill_render_grid(self._h, self._buf, self._w * self._hgt))
if written <= 0:
raise RuntimeError("tetris_fill_render_grid() 실패")
arr = np.frombuffer(self._buf, dtype=np.uint8, count=self._w * self._hgt)
arr = arr.reshape(self._hgt, self._w)
visible = arr[self._hidden :, :] # drop hidden rows
return visible[:, :, None]
def reset(self, *, seed: int | None = None, options: dict[str, Any] | None = None):
super().reset(seed=seed)
if seed is not None:
self._lib._dll.tetris_set_seed(self._h, int(seed))
self._lib._dll.tetris_reset(self._h)
return self._obs(), {}
def step(self, action: int):
alive = int(self._lib._dll.tetris_step(self._h, int(action)))
reward = float(self._lib._dll.tetris_last_reward(self._h))
terminated = (alive == 0) or (int(self._lib._dll.tetris_game_over(self._h)) == 1)
truncated = False
info = {
"lines_cleared_total": int(self._lib._dll.tetris_lines_cleared_total(self._h)),
"piece_index": int(self._lib._dll.tetris_piece_index(self._h)),
"stack_height": int(self._lib._dll.tetris_stack_height(self._h)),
"bumpiness": int(self._lib._dll.tetris_bumpiness(self._h)),
"holes": int(self._lib._dll.tetris_holes(self._h)),
}
return self._obs(), reward, terminated, truncated, infoO próximo post é "2. DQN por 370 mil passos, zero linhas eliminadas".
COMMENTS · CHAPTER 01
Encapsulando um motor de Tetris em C++ como ambiente de aprendizado por reforço 0 comentários
Ainda não há comentários. Deixe uma pergunta ou algo que queira acrescentar.
Para publicar um comentário, comece com o Google e registre um apelido e um e-mail.
Começar com o Google