ONCE AI PUBLISHING디지털 핸드북 시리즈 #01
CHAPTER 05

VLM을 이용한 테트리스 학습

화면 전체를 읽고, 블록의 최종 위치를 결정하는 언어모델 기반 게임 에이전트

책 안에서 이 장이 맡는 역할

이 책의 제목을 'LLM으로 풀어 보는 옛날 문제'라고 잡는다면, 테트리스 장은 가장 직관적인 시각 문제다. 규칙은 오래됐고, 목표도 분명하다. 하지만 한 순간의 정답은 단순하지 않다. 지금 보이는 전체 판, 현재 블록, 다음 블록, 구멍, 높이, 표면의 울퉁불퉁함을 함께 보고 판단해야 한다.

기존 초안은 DQN 강화학습 중심이었다. 이번 개정에서는 DQN을 배경 기술로 낮추고, LLM/VLM이 화면을 인식해 의사결정하는 구조를 중심에 둔다. 즉, 이 장의 주인공은 '학습된 정책'이 아니라 '보드를 읽고 배치 결정을 내리는 모델'이다.

학습 목표

  • 테트리스 보드를 LLM이 읽을 수 있는 표현으로 바꾸는 방법을 이해한다.
  • VLM에게 보드 이미지를 보여 주고 target_rot, target_x를 JSON으로 받는 구조를 만든다.
  • 텍스트 LLM에게는 전체 화면을 ASCII 보드와 후보 목록으로 제공해 환각을 줄인다.
  • LLM의 자유 답변을 그대로 믿지 않고, 후보 생성과 검증을 결합해 안정적인 게임 조작으로 바꾼다.
  • LLM 판단을 실제 Qt 테트리스 화면에 연결해 플레이 가능한 에이전트로 만든다.

문제 정의: 블록을 어디에 놓을 것인가

테트리스에서 모델이 매 순간 해야 할 결정은 매우 구체적이다. 현재 떨어지는 블록을 몇 번 회전하고, 어느 열에 놓을 것인가. 이 장에서는 그 결정을 두 값으로 표현한다.

{"target_rot": 1, "target_x": 4}

target_rot은 시계 방향 회전 횟수이고, target_x는 블록을 놓을 목표 열이다. LLM은 이 두 값을 결정하고, 프로그램은 그 값을 실제 키 입력으로 번역한다. 이렇게 하면 LLM이 매 프레임 왼쪽, 오른쪽, 회전을 직접 누르는 부담이 줄어든다.

전체 설계

Qt Tetris screen/state
  -> grid extraction
  -> image or ASCII board
  -> LLM/VLM decision
  -> target_rot, target_x
  -> micro action translator
  -> Left/Right/Rotate/Drop command

파일

이 장에서의 역할

vlm_macro_client.py

보드 이미지를 VLM에게 보여 주고 목표 회전과 목표 열을 직접 받는 가장 직관적인 구현

llm_macro_client.py

후보 배치를 계산한 뒤 LLM에게 Top-K 중 하나를 고르게 하는 안정형 구현

vlm_teacher.py

관측 그리드를 컬러 이미지로 렌더링하고 VLM이 다음 행동과 점수를 반환하게 하는 교사 모듈

llm_teacher.py

관측 그리드를 ASCII로 바꾸어 텍스트 LLM이 판단할 수 있게 하는 교사 모듈

tetris/src/tetris_engine.*

규칙과 물리, 라인 삭제, 보드 상태를 제공하는 오래된 문제의 시뮬레이터

tetris/src/tetris_c_api.cpp

C++ 엔진을 Python에서 사용할 수 있게 연결하는 브릿지

1단계: 화면을 모델이 볼 수 있는 데이터로 바꾸기

LLM이 게임을 풀려면 먼저 화면을 읽어야 한다. 이 프로젝트에는 두 가지 표현 방식이 있다. 하나는 VLM용 이미지이고, 다른 하나는 텍스트 LLM용 ASCII 보드다.

이미지 표현

vlm_teacher.py와 vlm_macro_client.py는 보드의 숫자 그리드를 색상 이미지로 바꾼다. 빈 칸은 어두운 색, I/O/T/S/Z/J/L 블록은 서로 다른 색으로 칠한다. 그 다음 각 칸을 cell_px 크기로 확대해 VLM이 보기 쉬운 이미지로 만든다.

rgb = palette[g]
rgb = np.repeat(np.repeat(rgb, cell_px, axis=0), cell_px, axis=1)
image = Image.fromarray(rgb, mode="RGB")

텍스트 표현

텍스트 LLM은 이미지를 직접 보지 못하므로 보드를 문자열로 바꾼다. 빈 칸은 '.', 채워진 칸은 '#', 또는 조각 번호로 표시한다. 행 번호를 붙이면 모델이 위아래 위치를 더 안정적으로 읽을 수 있다.

00|..........|
01|..........|
18|..###.....|
19|.#####....|

2단계: VLM에게 전체 화면을 보여 주기

가장 책 제목에 잘 맞는 방식은 VLM 방식이다. 모델에게 현재 보드 이미지를 보여 주고, 지금 블록의 최종 배치를 JSON으로 답하게 한다. 이 접근은 사람이 게임 화면을 보고 판단하는 방식과 가장 비슷하다.

prompt = (
    "You are an expert Tetris AI.\n"
    "Look at the current board state and decide the best final resting position.\n"
    "Return JSON: {\"target_rot\": 0..3, \"target_x\": 0..9}."
)

vlm_macro_client.py는 새 블록이 나타날 때마다 VLM을 호출한다. 응답에서 target_rot과 target_x를 뽑고, 실제 조작은 별도의 변환 함수가 맡는다. LLM이 '왼쪽으로 세 번 가고 회전해'처럼 절차를 말하게 하지 않고, 최종 목표만 말하게 하는 것이 핵심이다.

3단계: 목표 위치를 실제 조작으로 번역하기

게임 엔진은 여전히 한 번에 하나의 행동만 받는다. 그래서 LLM의 목표 배치를 실제 키 입력으로 바꾸는 작은 컨트롤러가 필요하다.

if cur_rot != target_rot:
    return RotateCW or RotateCCW
if cur_x < target_x:
    return Right
if cur_x > target_x:
    return Left
return HardDrop

이 번역기 덕분에 LLM은 고수준 판단만 담당하고, 세부 입력은 프로그램이 처리한다. 이것이 LLM 게임 에이전트를 만들 때 매우 중요한 패턴이다. 모델에게 모든 것을 시키는 대신, 모델이 잘하는 판단과 코드가 잘하는 실행을 나눈다.

4단계: 텍스트 LLM에는 후보를 고르게 하기

VLM 방식은 직관적이지만, 모델이 잘못된 좌표를 말하거나 보드를 착각할 수 있다. llm_macro_client.py는 더 안전한 방식을 사용한다. 프로그램이 가능한 모든 배치를 먼저 계산하고, 각 후보의 점수를 만든 다음, LLM에게 Top-K 후보 중 하나만 고르게 한다.

후보 지표

의미

cleared

이 배치로 지워지는 줄 수

new_holes

새로 생기는 구멍 수

on_hole

이미 구멍이 있는 열 위에 쌓는 블록 수

bumpiness

표면이 얼마나 울퉁불퉁한지

max_h

가장 높은 열의 높이

landing_height

블록이 얼마나 높은 위치에 놓이는지

look

현재 후보 점수에 다음 블록의 최선 점수를 더한 1수 앞보기 점수

Pre-evaluated candidate placements:
[0] rot=0 x=4 cleared=1 new_holes=0 bump=8 max_h=6 look=+2.40
[1] rot=1 x=7 cleared=0 new_holes=1 bump=12 max_h=8 look=-4.10

Reply EXACTLY as: {"choice": 0}

이 방식은 LLM의 창의성을 줄이는 대신 안정성을 얻는다. 모델은 좌표를 새로 발명하지 않고, 이미 검증된 후보 중 하나를 고른다. 책에서는 이 패턴을 'LLM을 의사결정자, 코드를 검산자'로 쓰는 구조라고 설명하면 좋다.

5단계: 환각을 줄이는 프롬프트 설계

게임 에이전트에서 LLM 환각은 곧 잘못된 조작이다. 그래서 프롬프트는 자유로운 설명보다 제한된 출력 형식을 강하게 요구해야 한다.

  • 응답은 JSON 하나만 받는다.
  • 선택 가능한 행동 또는 후보 인덱스의 범위를 명시한다.
  • 구멍 생성 금지, 높이 낮추기, 표면 평탄화처럼 판단 기준의 우선순위를 적는다.
  • 모델이 직접 좌표를 만들게 하기보다 가능한 후보 목록을 주고 고르게 한다.
  • JSON 파싱 실패 시 휴리스틱 1위 후보로 되돌아가는 fallback을 둔다.

6단계: 오래된 게임과 최신 LLM의 만남

테트리스는 1980년대 게임이지만, LLM에게는 작지 않은 추론 문제다. 화면에는 숫자 계산, 공간 인식, 단기 계획, 위험 회피가 동시에 들어 있다. 그래서 이 장은 단순한 게임 자동화가 아니라, LLM을 시각적 의사결정 엔진으로 사용하는 예제가 된다.

실습 순서

  1. Qt 테트리스 실행: 테트리스 GUI를 실행하고 RL/agent 포트를 연다.
  2. VLM 클라이언트 실행: vlm_macro_client.py를 실행해 화면 이미지를 VLM에게 전달한다.
  3. JSON 응답 확인: target_rot과 target_x가 정상적으로 파싱되는지 로그를 확인한다.
  4. 조작 번역 확인: 목표 회전과 목표 열이 Left/Right/Rotate/Drop 명령으로 바뀌는 과정을 관찰한다.
  5. 텍스트 LLM 방식 비교: llm_macro_client.py로 후보 기반 선택을 실행해 VLM 방식과 안정성을 비교한다.
  6. 프롬프트 개선: 구멍 금지, 낮은 높이 유지, 라인 삭제 우선순위를 바꿔 플레이 스타일 변화를 본다.

실행 예시

./tetris/build/tetris_rl --rl-port 5555

python vlm_macro_client.py \
  --port 5555 \
  --model-dir llms/Qwen2.5-VL-3B-Instruct

python llm_macro_client.py \
  --port 5555 \
  --model-path llms/qwen2.5-3b-instruct-q4_k_m.gguf \
  --top-k 6

챕터 마무리

이 장에서 우리는 테트리스를 강화학습 점수 게임이 아니라 LLM 의사결정 문제로 다시 보았다. 화면 전체를 이미지나 텍스트로 표현하고, 모델에게 최종 배치를 묻고, 코드는 그 결정을 실제 조작으로 바꾸었다.

핵심은 LLM에게 모든 책임을 넘기지 않는 것이다. 화면 인식과 판단은 LLM이 맡고, 후보 생성, 좌표 검증, JSON 파싱, fallback, 키 입력 변환은 코드가 맡는다. 이 분업 구조가 오래된 문제를 최신 LLM으로 풀 때 가장 안정적인 기본형이다.

연습 문제

  • VLM 프롬프트에서 'clear lines if possible'을 제거하면 플레이가 어떻게 달라질지 예상해 보자.
  • target_x를 직접 말하게 하는 방식과 후보 index만 고르게 하는 방식의 장단점을 비교해 보자.
  • ASCII 보드에 행 번호와 열 번호를 모두 추가하면 LLM의 선택 안정성이 좋아질지 실험해 보자.
  • JSON 파싱 실패가 반복될 때 사용할 fallback 정책을 직접 설계해 보자.
CHAPTER 05 · VLM을 이용한 테트리스 학습1 / 1
책장을 누르거나 좌우로 밀어 넘기기
CHAPTER 05

VLM을 이용한 테트리스 학습

댓글 0

아직 댓글이 없습니다.