화면 전체를 읽고, 블록의 최종 위치를 결정하는 언어모델 기반 게임 에이전트
이 책의 제목을 'LLM으로 풀어 보는 옛날 문제'라고 잡는다면, 테트리스 장은 가장 직관적인 시각 문제다. 규칙은 오래됐고, 목표도 분명하다. 하지만 한 순간의 정답은 단순하지 않다. 지금 보이는 전체 판, 현재 블록, 다음 블록, 구멍, 높이, 표면의 울퉁불퉁함을 함께 보고 판단해야 한다.
기존 초안은 DQN 강화학습 중심이었다. 이번 개정에서는 DQN을 배경 기술로 낮추고, LLM/VLM이 화면을 인식해 의사결정하는 구조를 중심에 둔다. 즉, 이 장의 주인공은 '학습된 정책'이 아니라 '보드를 읽고 배치 결정을 내리는 모델'이다.
테트리스에서 모델이 매 순간 해야 할 결정은 매우 구체적이다. 현재 떨어지는 블록을 몇 번 회전하고, 어느 열에 놓을 것인가. 이 장에서는 그 결정을 두 값으로 표현한다.
{"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에서 사용할 수 있게 연결하는 브릿지 |
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|.#####....|가장 책 제목에 잘 맞는 방식은 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이 '왼쪽으로 세 번 가고 회전해'처럼 절차를 말하게 하지 않고, 최종 목표만 말하게 하는 것이 핵심이다.
게임 엔진은 여전히 한 번에 하나의 행동만 받는다. 그래서 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 게임 에이전트를 만들 때 매우 중요한 패턴이다. 모델에게 모든 것을 시키는 대신, 모델이 잘하는 판단과 코드가 잘하는 실행을 나눈다.
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을 의사결정자, 코드를 검산자'로 쓰는 구조라고 설명하면 좋다.
게임 에이전트에서 LLM 환각은 곧 잘못된 조작이다. 그래서 프롬프트는 자유로운 설명보다 제한된 출력 형식을 강하게 요구해야 한다.
테트리스는 1980년대 게임이지만, LLM에게는 작지 않은 추론 문제다. 화면에는 숫자 계산, 공간 인식, 단기 계획, 위험 회피가 동시에 들어 있다. 그래서 이 장은 단순한 게임 자동화가 아니라, LLM을 시각적 의사결정 엔진으로 사용하는 예제가 된다.
./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으로 풀 때 가장 안정적인 기본형이다.
댓글 0
아직 댓글이 없습니다.