순수 규칙 게임을 인공지능과 연결하기 위한 관찰, 행동, 통신 설계

고전 게임이 좋은 이유

스페이스 인베이더류 게임은 규칙이 단순하다. 플레이어는 좌우로 움직이고, 총알을 쏘고, 적의 총알을 피한다. 적은 천천히 내려오고, 벙커는 총알에 맞아 부서진다. 이 단순함은 학습 예제에서 큰 장점이다. 복잡한 3D 물리나 스토리 없이도 관찰, 행동, 보상, 정책이라는 인공지능의 핵심 요소를 모두 설명할 수 있기 때문이다.

이 프로젝트의 게임 엔진은 Qt/C++로 작성되어 있고, 기본적으로 사람 조작만으로도 돌아간다. 같은 규칙을 폰에서 두는 순서는 「안드로이드 스튜디오로 인베이더 만드는 방법」과 「Xcode로 인베이더 만드는 방법」에 있다. 다시 말해 게임 본체는 AI가 아니다. AI는 게임 밖에서 들어온다. 이 구분을 분명히 해야 한다. 게임은 세계이고, 휴리스틱과 VLM은 그 세계를 바라보며 행동을 고르는 플레이어다.

프로젝트 구성

파일역할
src/gameengine.h/.cpp게임 규칙, 플레이어, 에일리언, 총알, 벙커, 충돌, 승패를 담당한다.
src/aiplayer.cpp총알 회피, 적 정렬, 발사 규칙으로 만든 로컬 휴리스틱 플레이어다.
src/mainwindow.cpp사람, 로컬 AI, VLM 모드를 선택하고 매 틱 행동을 게임에 넣는다.
src/tcpvlmclient.cpp게임 화면 PNG와 14x11 격자를 외부 VLM 서버로 보내고 액션을 받는다.
invader_vlm_server.pyTCP 서버로 INV3 추론 요청과 INVL 로그 요청을 처리한다.
dataset_invader.pyJSONL 로그를 읽어 이미지와 액션 라벨 학습 데이터로 바꾼다.
download_vlm_model.pyQwen3-VL 2B 모델을 vlm_models 아래에 내려받는다.

게임 상태와 행동

인공지능을 붙이려면 먼저 게임을 기계가 읽을 수 있는 형태로 정해야 한다. 이 프로젝트는 화면 전체를 PNG로 보낼 수도 있고, 동시에 14행 11열의 작은 격자 상태도 보낸다. 격자는 픽셀보다 정보가 거칠지만, 학습과 디버깅에는 훨씬 쉽다.

인베이더 화면을 0~4 숫자로 채운 14행 11열 격자와 셀 값 범례, 왼쪽·오른쪽·발사·대기 네 가지 행동 키
그림. 게임 화면은 14×11 숫자 격자로, 조작은 행동 번호 0~3으로 줄여서 모델에 건넨다
요소값
격자 크기14 x 11
Cell 0빈칸
Cell 1에일리언 또는 UFO
Cell 2플레이어
Cell 3플레이어 총알
Cell 4적 총알
행동 0왼쪽 이동
행동 1오른쪽 이동
행동 2발사
행동 3대기
관찰 = 화면 PNG + 14x11 격자
행동 = 0:left, 1:right, 2:fire, 3:none
게임 루프 = tick(action)

휴리스틱이란 무엇인가

휴리스틱은 학습된 모델이 아니라 사람이 직접 만든 경험적 규칙이다. 정답을 수학적으로 보장하지는 않지만, 문제를 빨리 풀기 위해 쓸 만한 판단 기준을 적어 둔 것이다. 인베이더에서는 '적 총알이 가까우면 피한다', '적과 x좌표를 맞춘다', '위에 적이 있으면 쏜다' 같은 규칙이 휴리스틱이다.

휴리스틱의 장점은 이해하기 쉽고 빠르다는 점이다. 단점은 새로운 상황에 약하다는 점이다. 예를 들어 적 총알이 두 방향에서 동시에 오거나, 벙커가 애매하게 막고 있거나, UFO를 노릴지 일반 적을 노릴지 선택해야 하는 상황에서는 규칙이 금방 복잡해진다.

로컬 AI의 휴리스틱

aiplayer.cpp의 로컬 AI는 매우 짧고 좋은 예제다. 먼저 플레이어 근처로 내려오는 적 총알을 찾는다. 위험하면 왼쪽이나 오른쪽 중 더 안전한 쪽으로 이동한다. 위험이 없으면 아래쪽에 있고 x좌표가 가까운 에일리언을 목표로 잡고 그쪽으로 움직인다. 목표 x좌표에 거의 맞으면, 위에 적이 있고 플레이어 총알 수가 한도보다 적을 때 발사한다.

적 총알이 가까운가, 목표 적과 x가 다른가, 위에 적이 있고 총알 여유가 있는가를 차례로 묻고 각각 회피, 이동, 발사, 대기로 이어지는 순서도
그림. 로컬 AI는 회피 → 정렬 → 발사 → 대기 순서로 묻고, 처음 '예'가 나온 규칙을 따른다
규칙의미
bulletThreatNearX플레이어 위쪽 가까운 x좌표에 적 총알이 내려오면 위험으로 판단한다.
bestAlienX아래쪽에 있고 플레이어 x좌표와 가까운 적을 우선 목표로 잡는다.
alienAboveX현재 x좌표 위에 적이 있으면 발사할 가치가 있다고 본다.
countPlayerBullets동시에 쏠 수 있는 플레이어 총알 수 제한을 지킨다.
suggestAction회피 -> 정렬 -> 발사 -> 대기 순서로 행동을 고른다.
if enemy bullet is near:
    move to safer side
else if target alien is left:
    move left
else if target alien is right:
    move right
else if alien is above and bullet limit allows:
    fire
else:
    none

휴리스틱의 교육적 가치

이 장에서 휴리스틱은 단순한 임시방편이 아니다. 휴리스틱은 첫 번째 선생님이다. VLM을 학습시키려면 처음에는 누군가의 행동 데이터가 필요하다. 사람의 플레이를 모아도 되고, 로컬 휴리스틱의 플레이를 모아도 된다. 로컬 휴리스틱은 완벽하지 않지만, 모델이 '무엇을 보면 무엇을 해야 하는가'를 처음 익히는 데 충분한 출발점이 된다.

관점휴리스틱학습 모델
만드는 방법사람이 규칙을 직접 적는다.데이터에서 패턴을 배운다.
장점빠르고 설명 가능하다.복잡한 상황을 더 유연하게 처리할 수 있다.
단점상황이 늘면 규칙이 복잡해진다.데이터가 부족하면 엉뚱한 행동을 한다.
역할초기 교사, 기준선, 폴백최종 정책 또는 보조 판단자

브리지가 필요한 이유

게임은 C++ Qt 프로그램이고, VLM은 Python 서버에서 돈다. 둘은 언어도 다르고 실행 환경도 다르다. 브리지는 이 둘 사이의 번역기다. 게임 쪽은 현재 화면과 격자 상태를 보낸다. AI 쪽은 0, 1, 2, 3 중 하나의 행동만 돌려준다. 이 단순한 계약이 있어야 게임을 고치지 않고도 여러 AI 모델을 갈아 끼울 수 있다.

Qt 게임과 Python VLM 서버 사이에 PING/PONG, INV3/RPLY, INVL/LACK 프레임이 오가는 시퀀스 다이어그램
그림. 연결 확인, 추론 요청, 로그 전송을 짧은 TCP 프레임으로 주고받는다
Qt Game
  -> packGrid()
  -> grab screen as PNG
  -> TCP INV3 frame
  -> Python VLM server
  -> action digit 0..3
  -> GameEngine.tick(action)
프로토콜용도
PING서버 연결 확인. 응답은 PONG.
INV3PNG와 격자를 보내고 VLM 액션을 받는 추론 요청.
RPLY서버가 돌려주는 액션 응답. 성공 시 action 바이트 포함.
INVL사람 또는 로컬 AI의 한 틱 행동을 서버에 기록하는 로그 요청.
LACK로그를 받았다는 확인 응답.

VLM 서버의 역할

invader_vlm_server.py는 외부 플레이어다. INV3 요청을 받으면 PNG와 격자를 읽고, Qwen3-VL 같은 비전 언어 모델에게 '이 화면에서 어떤 행동을 할 것인가'를 묻는다. 모델은 반드시 한 글자 0, 1, 2, 3으로 답해야 한다. 파싱이 실패하면 재시도하고, 그래도 실패하면 mock 또는 로컬 폴백 행동으로 안전하게 빠질 수 있다.

이 설계에서 중요한 점은 VLM이 게임 규칙을 직접 실행하지 않는다는 것이다. VLM은 행동만 고른다. 충돌, 점수, 총알 이동, 적 이동은 여전히 GameEngine이 책임진다. 이렇게 해야 AI가 이상한 답을 해도 게임 세계의 규칙은 깨지지 않는다.

python invader_vlm_server.py --host 127.0.0.1 --port 9300 --device mps

# 모델 없이 통신만 확인할 때
python invader_vlm_server.py --port 9300 --mock

로그가 학습 데이터가 되는 과정

MainWindow는 사람, 로컬 AI, VLM의 행동을 모두 로그로 남길 수 있다. 각 로그에는 게임 이름, 시간, 액션, 액터, 모드, 상태, 순번, 14x11 보드가 들어간다. 이 로그는 그대로 행동 복제 학습의 재료가 된다. dataset_invader.py는 JSONL 로그를 읽고, 보드를 작은 이미지로 렌더링한 뒤, action 라벨과 함께 train.jsonl을 만든다.

사람, 로컬 AI, VLM의 행동이 MainWindow에서 JSONL 로그로 쌓이고 dataset_invader.py를 거쳐 행동 복제용 학습 데이터가 되는 흐름과 로그 필드 목록
그림. 누가 플레이하든 한 틱은 로그 한 줄이 되고, 로그는 행동 복제 데이터셋이 된다
python dataset_invader.py \
  --jsonl logs/invader_train.jsonl \
  --out-dir datasets/invader_sft \
  --save-images
데이터 원천장점주의점
사람 플레이자연스럽고 창의적인 행동이 들어간다.실수와 습관도 함께 들어간다.
로컬 휴리스틱일관된 기준선 데이터를 빠르게 모을 수 있다.휴리스틱의 한계를 그대로 배운다.
VLM 플레이모델이 스스로 만든 행동을 다시 분석할 수 있다.오답이 반복 학습되지 않도록 필터링해야 한다.

브리지를 만들 때의 설계 원칙

  • 게임 엔진은 항상 결정적 규칙을 유지하고, AI는 행동 선택만 담당한다.
  • 관찰은 화면 PNG와 구조화된 격자를 함께 보낸다.
  • 행동 공간은 작고 명확하게 둔다. 이 프로젝트는 4개 행동만 사용한다.
  • AI 응답은 반드시 검증한다. 응답이 허용 범위를 벗어나면 게임의 대기 액션 3으로 바꾸거나 로컬 휴리스틱으로 대체한다.
  • 응답이 늦을 때 게임이 무너지는 것을 막기 위해 타임아웃과 취소 버튼을 둔다.
  • 서버 실패 시 로컬 휴리스틱으로 대체해 게임이 계속 진행되도록 한다.
  • 모든 행동을 JSONL로 남겨 나중에 데이터셋으로 변환할 수 있게 한다.

AI 연결 이후의 발전 단계

이 프로젝트는 세 단계로 발전시킬 수 있다. 첫 단계는 휴리스틱 자동 플레이어다. 두 번째 단계는 VLM 서버가 화면을 보고 행동을 고르는 단계다. 세 번째 단계는 사람과 휴리스틱의 로그를 모아 VLM을 미세조정하거나 작은 정책망을 학습하는 단계다. 이때 게임은 그대로 두고 브리지와 모델만 바꾸면 된다.

휴리스틱, 브리지, 로깅, 데이터셋, 학습, 검증 여섯 단계가 계단처럼 올라가는 그림
그림. 게임은 그대로 두고 브리지와 모델만 바꿔 가며 단계적으로 올라간다
단계설명
1. 휴리스틱총알 회피, 적 정렬, 발사 규칙으로 자동 플레이 기준선을 만든다.
2. 브리지Qt 게임과 Python VLM 서버를 TCP 프로토콜로 연결한다.
3. 로깅사람과 휴리스틱의 행동을 JSONL로 저장한다.
4. 데이터셋JSONL을 이미지와 액션 라벨로 변환한다.
5. 학습VLM 또는 작은 시각 정책 모델이 행동을 모방하게 학습한다.
6. 검증점수, 생존 시간, 웨이브 클리어 수, 위험 회피율을 비교한다.

평가 기준

평가 항목질문
생존 시간모델이 얼마나 오래 버티는가?
점수적을 얼마나 많이 맞히는가?
웨이브 진행한 웨이브를 클리어하고 다음 웨이브로 넘어가는가?
총알 회피적 총알이 가까울 때 피하는 행동을 하는가?
발사 효율적이 위에 있을 때 쏘고, 의미 없는 발사를 줄이는가?
응답 안정성VLM 응답이 0~3 형식을 잘 지키는가?
폴백 빈도서버 실패나 파싱 실패로 휴리스틱 대체가 얼마나 자주 일어나는가?

핵심 메시지

이 장의 주인공은 거대한 모델이 아니다. 주인공은 연결 방식이다. 인공지능이 없는 고전 게임도 관찰과 행동의 인터페이스를 만들면 학습 환경이 된다. 휴리스틱은 첫 번째 교사이고, 브리지는 게임과 모델 사이의 언어다. 이 둘을 만들 수 있으면 어떤 오래된 프로그램도 AI 실험장으로 바꿀 수 있다.

실습 순서

  • Qt 프로그램을 실행해 사람 모드로 게임 규칙을 먼저 확인한다.
  • 로컬 AI 모드로 바꾸고 휴리스틱이 어떻게 움직이는지 관찰한다.
  • mock 서버를 실행해 TCP 브리지가 정상 동작하는지 확인한다.
  • VLM 서버를 실행하고 VLM 모드에서 액션이 돌아오는지 확인한다.
  • 사람 또는 로컬 AI의 플레이를 JSONL로 저장한다.
  • dataset_invader.py로 train.jsonl과 보드 이미지를 만든다.
  • 학습된 모델과 휴리스틱의 점수, 생존 시간, 발사 효율을 비교한다.

연습 문제

  • bulletThreatNearX의 거리 기준을 14에서 20으로 늘리면 생존 시간이 어떻게 바뀌는지 실험해 보자.
  • UFO가 나타났을 때 UFO를 우선 목표로 잡는 휴리스틱을 추가해 보자.
  • VLM 응답이 잘못된 형식일 때 재시도 프롬프트를 어떻게 바꾸면 좋을지 설계해 보자.
  • 14x11 격자 대신 더 촘촘한 28x22 격자를 쓰면 어떤 장단점이 생기는지 정리해 보자.
  • 휴리스틱 로그와 사람 로그를 섞어 학습할 때 어떤 비율이 좋을지 실험 계획을 세워 보자.