43년전 게임을 gpt5을 활용해 typescript로 다시 만들어보는 과정을 기록합니다. 글 마지막에 데모 링크가 있으니 확인해보세요.
1982년에 출시한 dunnet이라는 어마어마하게 오래된 어드벤처 게임이 있습니다. 우리에게 익숙한 그래픽 기반 게임은 아니고 터미널에서 타이핑하며 플레이하는 텍스트 기반 게임입니다.
텍스트 기반이라는게 무슨 뜻인지 모르시겠나요? 누군가 이미 브라우저에 이식한게 있어서 dunnet.js에서 플레이해볼 수 있습니다. help
로 도움말을 확인하고, east
로 동쪽으로 이동하고, inventory
로 인벤토리를 확인해보세요.
dunnet은 lisp라는 언어로 쓰여있습니다. 저는 이름만 들어본 언어인데요, 위키피디아에 따르면…
1958년에 초안이 작성된 이 언어는 현재 널리 사용되는 포트란에 이어 두 번째로 오래된 고급 프로그래밍 언어이다.
신기한게 중위표현식이 아닌 전위표현식을 사용해 1 + 2
를 아래와 같이 표기한다고 합니다:
(+ 1 2)
dunnet의 lisp 소스코드는 여기에서 볼 수 있습니다. 총 3350줄로 게임치고는 짧네요! 예전에 C언어로 테트리스를 만들어봤는데 1000줄은 훌쩍 넘더라고요.
lisp가 어떻게 생겼는지 궁금하시나요? 예를 들어 아래는 방을 설명하는 함수입니다:
;;; Give long description of room if haven't been there yet. Otherwise
;;; short. Also give long if we were called with negative room number.
(defun dun-describe-room (room)
(if (and (not (member (abs room) dun-light-rooms))
(not (member obj-lamp dun-inventory))
(not (member obj-lamp (nth dun-current-room dun-room-objects))))
(dun-mprincl "It is pitch dark. You are likely to be eaten by a grue.")
(dun-mprincl (cadr (nth (abs room) dun-rooms)))
(if (and (and (or (member room dun-visited)
(string= dun-mode "dun-superb")) (> room 0))
(not (string= dun-mode "long")))
nil
(dun-mprinc (car (nth (abs room) dun-rooms)))
(dun-mprinc "\n"))
(if (not (string= dun-mode "long"))
(if (not (member (abs room) dun-visited))
(setq dun-visited (append (list (abs room)) dun-visited))))
(dolist (xobjs (nth dun-current-room dun-room-objects))
(if (= xobjs obj-special)
(dun-special-object)
(if (>= xobjs 0)
(dun-mprincl (car (nth xobjs dun-objects)))
(if (not (and (= xobjs obj-bus) dun-inbus))
(progn
(dun-mprincl (car (nth (abs xobjs) dun-perm-objects)))))))
(if (and (= xobjs obj-jar) dun-jar)
(progn
(dun-mprincl "The jar contains:")
(dolist (x dun-jar)
(dun-mprinc " ")
(dun-mprincl (car (nth x dun-objects)))))))
(if (and (member obj-bus (nth dun-current-room dun-room-objects)) dun-inbus)
(dun-mprincl "You are on the bus."))))
괄호가 엄청나게 많네요! lisp의 특징을 말할 때 괄호가 자주 언급되는 것 같아요:
위 브라우저 이식본의 소스코드를 보면 알겠지만 원본 dunnet의 소스코드를 그대로 자바스크립트로 변환한게 아니라, 브라우저상에서 돌릴 수 있는 lisp 인터프리터를 통해 lisp 코드를 그대로 실행한다고 합니다.
meli._js is a minimum emacs lisp interpreter written in JS, which is supposed to be compiled by streamline.js.
이것도 굉장히 신기하지만 나는 게임 소스코드 구경도 하고 겸사겸사 한글화도 해보고싶어서 아얘 타입스크립트로 만들기로 했습니다.
하지만 원본 lisp 코드는 이해도 못하는데 어떻게 작업할 수 있을까요? AI의 도움을 받아봅시다!
제가 접근할 수 있는 하이엔드 모델인 o3 pro에게 부탁해봅시다.
아래 프롬프트를 사용했습니다. 재사용성을 위해 코드 구조도 클래스 형식으로 리팩터링해달라고 했어요.
아래는 emacs 게임 dunnet의 소스코드야. 이걸 typescript로 포팅해줘.
게임의 frontend는 필요없고 게임 엔진만 필요해. 예를 들면, 하나의 클래스가 있어서 상태를 내부적으로 관리하고 유저가 입력한 텍스트를 메서드를 통해 전달하는 식. 게임의 상태는 프로퍼티로 접근하는 등. 코드를 읽고 적합한 클래스 signature를 선택해줘. typescript 파일 하나로 출력해줘.
주석은 한글로 달아줘.
…
o3 pro는 6분정도 고민하더니 이상하게만치 짧은 ts 파일을 출력했습니다. 3000줄 넘는게 270줄로 줄어들었네요. 이유를 보니…
/*
Emacs 게임 “dunnet” 엔진의 TypeScript 포팅 (프런트엔드 제외)
- 하나의 클래스로 내부 상태 관리
- 사용자가 입력한 텍스트를 handleInput() 으로 전달
- 현재 상태(룸, 인벤토리 등)는 public 프로퍼티로 노출
* 원본 Elisp 소스가 방대한 관계로, 구조와 핵심 로직을 우선 포팅하고
나머지 세부 데이터/명령은 TODO 로 남겨 두었다.
→ 게임 룸·오브젝트 정의를 JSON 파일로 분리하거나, 아래
ROOM_DATA / OBJECT_DATA 에 확장해 가며 완성하면 된다.
*/
코드가 너무 길다고 자기마음대로 구현을 생략했습니다. 그래 뭐 방이 수십개가 되니 이런 반복작업은 내가 따로 4o에게 시키면 되지… 하면서 넘어갔는데 다른 문제도 있었습니다:
// 무게 한도 등은 간단히 무시
// 방에서 제거 → 인벤토리에 추가
room.objects = room.objects.filter(id => id !== objId);
무게 한도도 게임의 중요한 로직이 아닌가?라는 생각이 들어 생략하지 말라고 강하게 말하며 아래 프롬프트로 다시 요청습니다.
무게 한도는 왜 무시해? 동일한 모양의 객체가 반복돼서 처리하기 곤란한 경우는 제외하고 로직은 모두 유지해줘. 대신 생략한 경우는 뭘 생략했는지 주석으로 명시해줘
이러니 무게 로직만 추가한 ts 파일을 출력해서 포기했습니다.
운명같이 다음날에 gpt5가 출시했습니다. gpt5는 다르지 않을까?? 하는 생각에 다시 요청해보았습니다.
어제 깨달은 내용을 바탕으로 프롬프트에 아래 내용을 반영했습니다:
수정된 프롬프트입니다:
아래는 emacs 게임 dunnet의 소스코드야. 이걸 typescript로 포팅해줘. 코드 구조를 바꾸지는 말고 가능한 기존 구조를 유지한 채 포팅해줘. 다만 방의 정보를 나타내는 객체 등 반복작업이면서 방대하면 몇 개만 예시로 남기고 주석으로 명시해줘. 원본 코드의 어떤 값인지, 어디에 어떻게 넣으면 되는지 지시를 남겨. 이외에는 게임의 로직을 모두 유지하도록 주의해. 정말정말 부득이하게 포팅이 불가능한 게임 기능이 있다면 꼭 주석으로 명시해. 출력은 영어로
…
15분동안이나 기다리니 코드가 출력되었습니다. o3 pro보다 오래 고민했네요! 게다가 로직 생략 없이 2753줄짜기 코드를 출력해줬습니다!
프롬프트가 좀 더 구체적인 영향도 있겠지만 소요 시간과 결과물을 생각하면 5pro가 더 일을 열심히 하는듯한 느낌이 들었네요.
게다가 이번엔 아래처럼 어떤 부분이 불가피하게 생략됐는지도 구체적으로 알려줬습니다:
- I/O & UI: Emacs buffer operations (insert, minibuffer, keymaps, sleep-for, etc.) aren’t meaningful in TypeScript. I implemented handleInput() and mprinc/mprincl equivalents that buffer output strings. Blocking prompts like dun-read-line are handled via an awaiter (promptAwait) so the engine can pause and resume on the next handleInput() call. Logic and messages remain the same.
- Save/Restore: Emacs used ROT13’d elisp files and eval. In TS, I exposed saveGame() / restoreGame(json) that operate on JSON strings (portable and safer). I left explicit comments marking this as an UNPORTABLE change in the sense of APIs, not game logic.
- Files & Score Log: Writing to /usr/local/dunnet.score is not portable in a browser or sandbox, so I comment-noted it and didn’t implement the file side-effect. Scoring logic stays intact.
- Batch mode: Not applicable; host can call handleInput in a loop.
하지만 그대로 복붙해서 실행해보니 정의되지 않은 함수를 사용해 에러가 발생했습니다. 이걸 피드백하니 빠진 함수를 추가해줬고, 이후 게임 초반 부분이 잘 동작함을 확인했습니다.
가능성을 보았으니 욕심을 낼 차례겠지요. gpt와 티키타카하기에는 대상 ts파일의 길이가 너무 길어서 (2700줄) 토큰 낭비가 심해보였습니다. 따라서 프롬프트에 아래 내용을 추가해 아이템 종류나 방의 종류같은 정적데이터들은 JSON으로 뽑아달라고 했습니다. 이후에는 좀 더 짧아진 ts 파일을 토대로 빠르게 대화할 수 있겠지요.
코드 구조를 바꾸지는 말고 가능한 기존 구조를 유지한 채 포팅해줘. 다만 방의 목록처럼 동일한 구조가 계속 반복되며 방대한 데이터는 별도의 JSON 파일들을 만들고 이를 import해서 사용해.
이번에도 15분정도 고민하고 html 파일 하나, ts 파일 하나, JSON 파일 15개를 반환했습니다. 놀랍네요!
…요청하신 대로 방·오브젝트·던전 맵 등 반복되는 대규모 데이터는 모두 별도 JSON으로 분리했습니다.
프로젝트 폴더(dunnet_port/) — 아래 링크로 파일을 받으세요.
- dunnet.ts
- index.html
분리된 데이터(JSON):
- rooms.json
- dungeonMap.json
- lightRooms.json
- objnames.json
- objects.json
- roomObjects.json
- roomSilents.json
- objectLbs.json
- objectPts.json
- objfiles.json
- permObjects.json
- physobjDesc.json
- permobjDesc.json
- diggables.json
- endgameQuestions.json
단순 브라우저 이식본은 게시물 초반에 소개했던 dunnet.js와 다를게 없기에 한글화를 시도했습니다. 게임 소스코드에 대한 이해는 저보다 gpt가 월등할거기에 솔직하게 물어봤습니다.
Q. 혹시 한글화를 하려면 어쩐 json 파일을 번역해야돼? 당연히? ts 파일도 건드려야되는거지? 일단 한글화하지는 말고 대답만해줘.
A. 요점만 딱 정리해 드릴게요. 결론: “방·오브젝트 같은 콘텐츠 텍스트”만 한글화한다면 JSON만 번역해도 어느 정도 됩니다. 하지만 **UI/시스템 메시지, 도움말, 질문/정답, 명령어(동사/명사)까지 ‘완전 한글화’**하려면 TS 파일도 수정이 필요합니다.
‘요점만 딱 정리해 드릴게요.‘가 왜그렇게 킹받는지… 너 정말 핵심을 찔렀어!
하지만 여기서 문제가 발생했습니다. 냅다 영어 문장을 한글로 번역하면 플레이에 지장이 생길 수 있기 때문입니다. 예를 들어 게임 극초반에 아래와 같은 다이얼로그를 볼 수 있습니다:
> 당신은 컴퓨터실에 있습니다. 대부분의 장비는 치워진 듯합니다. 하지만 당신 앞에는 VAX 11/780이 있고, 캐비닛 하나가 활짝 열려 있습니다. 기계 앞의 표지판에는 이렇게 쓰여 있습니다: 이 VAX의 이름은 ‘pokey’입니다.
여기서 유저가 할 일은 아래 명령어로 인벤토리의 CPU를 캐비닛에 넣는 것입니다.
> put cpu in cabinet
하지만 한국인 유저가 다이얼로그로부터 ‘cabinet’이란 단어를 떠올려 명령어를 입력하는건 쉽지 않습니다. 캐비닛이야 캐비닛-cabinet으로 발음이 비슷하니 그나마 낫지만 다른 단어는? 이를 통해 단순 문자열 번역으로는 불충분하고 한글화 전략에 대한 고민도 필요함을 깨달았습니다.
또다른 이슈로, 명령어의 한글 직역은 게임의 테마와 맞지 않습니다. 'look'
, 'get cpu'
같은 명령어를 굳이 한글로 번역해서 '보기'
, 'CPU 얻기'
로 바꾸는건 의미없습니다. 개발하면서 터미널에 한글 칠 일은 잘 없잖아요? 게임의 분위기를 흐린다고 판단했습니다.
여기서는 생략했지만 위 내용들은 gpt 토큰과 시간을 엄청 쓰면서 깨달았습니다. gpt에게 요청하고, 15분 기다리고, 결과를 실행하고 이상함을 깨닫길 반복했지요. gpt에게 요청하기 전에 좀 더 생각해서 토큰을 아껴야겠다고 느꼈습니다.
아무튼 더더욱 자세해진 새로운 프롬프트로 gpt에게 다시 요청했습니다.
아래는 emacs에 내장된 text adventure 게임 dunnet의 소스코드야. 이걸 포팅하는게 목표야.
[출력 파일]
- 메인 typescript 파일 하나
- 데모 목적의 아주 단순한 html 파일 하나
- 스타일 신경쓸거없이 최소 기능만 아주 단순하게
- ts를 js처럼 스크립트에 넣어서 사용
- ts파일을 어떻게 html에 번들링할지는 내가 할테니 신경쓰지 말고 그냥 js처럼 취급해.
[주의점]
- 게임은 원본과 동일하게 동작해야돼. 게임의 로직을 모두 유지하도록 주의해. 원본 코드 로직의 수정은 최소화해.
- 다만 쉘 게임을 브라우저로 옮기는거다보니 부득이하게 포팅이 불가능하거나 브라우저 환경에 맞게 수정이 필요한 게임 기능이 있다면 수정해도 돼.
- 또한 브라우저에서는 단순한 input으로만 상호작용하니 이것과 연관없는 emacs 전용 기능은 제거해도 돼.
- 수정사항은 코드에 주석으로 남겨줘.
- 영어를 모르는 한국인 플레이어도 플레이할 수 있도록 한글로 번역해줘.
- 다만 모든 텍스트 인풋은 영어로 들어온다고 가정해도 좋아. 굳이 한글 인풋을 지원하지마.
- 유저가 읽는건 한국어지만 쓰는건 영어니까 필요한 경우 ‘삽(shovel)‘처럼 한국어 텍스트 단어 다음에 괄호 열고 영어 단어를 쓰는 식으로 유저가 특정 대상에 관련있는 영어 단어가 무엇인지 알 수 있게 해줘.
쓸만한 ts 파일이 나왔고 여기서부터는 직접 코드를 이해하며 수정하기로 했습니다.
AI의 도움을 받아 내가 전혀 이해하지 못한 코드를 기반으로 작업하는 독특한 경험이었습니다. 지금까지는 제가 아는 작업을 더 빠르고 편리하게 하는 보조 용도로 사용했는데 이번에는 제가 할 수 없는 영역을 맡겨봤습니다.
다만 동시에 제가 AI의 결과물을 평가할 수 없으니 답답했습니다. lisp -> typescript 부분은 ai가 해준걸 온전히 신뢰해야했는데 그러기 쉽지 않았네요. 나중에 버그가 생기면 버그 원인을 좁혀나가는 과정이 필요할텐데 코드의 구성을 모르니 불가능해보였습니다. 이런 디버깅도 ai에게 맡기는게 바이브 코딩이려나요?
그리고 코드를 부분부분 쪼개야 AI에게 시키고 피드백하는 텀이 짧아질텐데 코드의 구성을 모르니 이게 불가능했습니다. 다만 gpt5 pro를 사용하기 위해 chatgpt 앱을 사용했는데 cursor나 claude code를 사용했다면 따로 쪼갤 필요도 없고 작업 환경도 좋지 않았을까하는 생각이 듭니다.
사이드 프로젝트다보니 ai한테 시키고 저는 딴짓하게됐는데… 모델이 응답하는 시간동안 할 수 있는 작업을 고민해봐야겠습니다. ai가 생각중인걸 마치 제가 일하고 있는 것처럼 착각하게되네요.
시간과 비용이 소모되는 모델을 사용할거면 프롬프트를 그만큼 자세히 작성합시다. 프롬프트를 깎을 때는 저비용 모델을 활용할 수 있겠네요.
/craft/dunnet에서 한글화된 브라우저 이식본을 확인해보세요.
(주의) 모든 기능이 동작하는지 확인하지 않았습니다!
댓글을 남기려면 로그인이 필요합니다.