
Objetivo
Hacer aprendizaje por refuerzo con nuestro propio motor en C++, sin usar entornos externos como gym-tetris o una ROM de NES. El entrenamiento se ejecuta sin pantalla, y la pantalla de Qt solo se conecta para ver los resultados.
El motor
- El tablero tiene 10 casillas de ancho y 26 de alto. Las 4 filas superiores son filas ocultas donde aparecen las piezas, así que en la pantalla y en la observación solo se ven 22 filas.
- Las piezas se sacan con un 7-bag. Se barajan los siete tipos y se reparte cada uno una vez, de modo que ninguna pieza deja de salir durante mucho tiempo.
- Al girar, prueba por orden 7 posiciones, desde el sitio original hasta los sitios desplazados a izquierda, derecha y arriba, y coloca la pieza en la primera en la que cabe.
- Después de un movimiento, un giro o no hacer nada, la pieza baja una fila por sí sola. Por eso, el número de acciones disponibles por pieza es igual a la altura del tablero.
- La recompensa se calcula en el momento en que la pieza se fija. La composición de la recompensa se trata en la siguiente entrada.
API en C y entorno Gym
El motor es una clase de C++, así que Python no puede llamarlo directamente. Se envuelve en 21 funciones extern "C" y se compila como biblioteca compartida (libtetris_c_api.dylib), que Python carga con ctypes. Esta biblioteca se compila sin Qt.
LocalTetrisEnv crea un entorno gymnasium a partir de estas funciones.
- Observación: 22×10 casillas. Las casillas vacías son 0 y los bloques, 1~7 según el tipo. No se distingue entre bloques fijados y la pieza en movimiento.
- Acciones: 7. No hacer nada, izquierda, derecha, giro horario, giro antihorario, bajar una fila y dejar caer de golpe.
- Recompensa: se usa tal cual el valor calculado por el motor.
El motor conoce las 5 piezas siguientes, pero no se incluyeron en la observación.
Pantalla de Qt y TCP
Al lanzar la app de Qt (tetris_rl) con --rl-port 5555, se abre un servidor TCP. La app envía el estado del tablero como una línea de JSON y el cliente devuelve un número de acción en una línea. El JSON contiene el tablero, el tipo, la posición y el giro de la pieza actual, y la lista de piezas siguientes. Este canal se usa para ejecutar en pantalla un modelo entrenado o para conectar los clientes de LLM y VLM que aparecen más adelante.
El código real de esta etapa
La API en C guarda un motor en un único handle, recibe un número de acción y avanza un paso.
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;
}El entorno de Python recorta las 4 filas ocultas del tablero que rellena el motor y usa el resto como observación.
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, infoLa siguiente entrada es "2. DQN durante 370,000 pasos y 0 líneas borradas".
COMMENTS · CHAPTER 01
Envolver un motor de Tetris en C++ como entorno de aprendizaje por refuerzo 0 comentarios
Todavía no hay comentarios. Deja una pregunta o algo que quieras añadir.
Para publicar un comentario, empieza con Google y registra un apodo y un correo.
Empezar con Google