갈루아의 반서재


Getting Started with CLISP (16)

텍스트 게임 엔진 만들기 Building a text game engine


텍스트 기반의 게임 엔진을 만들어보자. 본 프로그램에서는 다음의 몇 가지를 다룰 수 있어야 한다. 

1. 둘러보기

2. 다른 장소로 이동하기

3. 물건 집기

4. 집은 물체에 대한 행동


먼저 1. 주변을 둘러보는 것과 관련해서, 어떤 위치에서든 서로 다른 3가지를 "볼" 수 있어야 한다. 

1. 기본 경관

2. 다른 위치로 가는 하나 이상의 경로

3. 집어서 조작할 수 있는 물건


Describing the Scenery with an Association List


본 예제에서는 3개의 위치만 포함하고 있다. 먼저 위치를 나타내는 최상위 변수 nodes 를 선언해보자. 

1
2
3
4
[32]> (defparameter *nodes* '((living-room (you are in the living-room. a wizard is snoring loudly on the couch.))
                              (garden (you are in a beautiful garden.there is a well in front of you.))
                              (attic (you are in the attic. there is a giant welding torch in the corner.))))
*NODES*
cs

변수에는 3군데 위치에 대한 리스트와 설명이 포함되어 있다. 위의 예에서 키는 장소의 이름(living-room, garden, or attic), 이고 데이터는 해당 장소에 대한 설명이다. 위와 같은 구조는 흔히 association list 또는 alist 라고 부른다. 

이 *nodes* 라는 변수는 실제 장소에 대한 설명을 담고는 있지만, 실제로는 텍스트 문자열을 포함하고 있지는 않다. 물론 Common Lisp는 문자열 데이터타입을 가지고 있기 때문에, 따옴표를 통해 위치에 대한 설명 부분을 다음과 같이 처리할 수도 있다. 

 "You are in a beautiful garden. There is a well in front of you." 

하지만 여기서는 심볼과 리스트를 통해서 처리하기로 한다. 그러면 왜 문자열을 사용하지 않는가? 앞서 언급했듯이 텍스트 조작은 실제로 기본적인 컴퓨팅의 컨셉이 아니기 때문이다. 실제 리스프에서 가장 손쉬운 처리방식이 심볼과 리스트에 의한 것이기 때문에, 숙련된 리스퍼들은 보통 여기에 초점을 맞춘다. 


Describing the Location


앞서 alist 를 생성했다. 이제 위치를 기술할 명령어를 생성해보자. 이를 위해서 key를 이용해 해당 리스트에서 적절한 아이템을 찾는 assoc 함수가 필요하다. 

1
2
3
4
Break 32 [33]> (assoc 'garden *nodes*)
(GARDEN (YOU ARE IN A BEAUTIFUL GARDEN. THERE IS A WELL IN FRONT OF YOU.))
Break 32 [33]>
cs

그러면 assoc 를 이용해 손쉽게 describe-location 함수를 만들 수 있다.

1
2
3
4
Break 33 [34]> (defun describe-location (location nodes) (cadr (assoc location nodes)))
DESCRIBE-LOCATION
Break 33 [34]>
 
cs

이 함수를 사용하기 위해, 위치를 *nodes* 리스트로 전달한다.

1
2
3
4
Break 34 [35]> (describe-location 'living-room *nodes*)
(YOU ARE IN THE LIVING-ROOM. A WIZARD IS SNORING LOUDLY ON THE COUCH.)
Break 34 [35]>
cs

그러면 왜 describe-location 함수에서 직접 *nodes* 변수를 참조하지 않는가? 그 이유는 이 함수의 경우  functional programming 스타일로 작성되었기 대문이다. 이 방식에서는 함수 자체에 선언된 인수나 변수만을 참조하고, 값 - 이 경우에 위치에 대한 설명 - 을 반환하는 것 외에 다른 일은 수행하지 않는다. 


Describing the Paths


각각의 장소에 대한 기본 설명을 만들었고, 이제 다른 장소로 이동하는 경로에 대한 설명을 만들어야 한다. 이제 플레이어가 다른 장소로 이동하는 경로를 담고 있는 2번째 변수인 *edges* 를 만들어보자. 

1
2
3
4
5
Break 37 [38]> (defparameter *edges* '((living-room (garden west door)
                                                    (attic upstairs ladder))
                                       (garden (living-room east door))
                                       (attic (living-room downstairs ladder))))
*EDGES*
cs

심볼 시스템을 이용하여 주어진 edge 의 텍스트 설명을 만들어주는 함수인 describe-path 을 다음과 같이 만들어보자. 

1
2
3
Break 38 [39]> (defun describe-path (edge)`(there is a ,(caddr edge) going ,(cadr edge) from here.))
DESCRIBE-PATH
Break 38 [39]>
cs

describe-path 함수는 함수라기 보다는 데이터의 한 조각처럼 보인다. 어떻게 작동하는지 살펴보자. 

1
2
3
4
Break 39 [40]> (describe-path '(garden west door))
(THERE IS A DOOR GOING WEST FROM HERE.)
Break 39 [40]>
cs

이 함수는 기본적으로 삽입되는 정보를 가공하여 이를 일부 포함한 데이터를 반환한다. 그리고 Lisp의 이러한 기능을 quasiquoting 라고 부른다. 

Quasiquotation is a parameterized version of ordinary quotation where instead of specifying a value exactly some holes are left to be lled in later. A quasiquotation is a template.


How Quasiquoting Works

quasiquoting을 사용하기 위해서는 코드 모드에서 데이터 모드로 스위칭할때 따옴표 대신 역따옴표를 사용해야 한다. describe-path 함수는 그런 역따옴표를 포함하고 있다.

1
2
3
Break 38 [39]> (defun describe-path (edge)`(there is a ,(caddr edge) going ,(cadr edge) from here.))
DESCRIBE-PATH
Break 38 [39]>
cs


Describing Multiple Paths at Once

위치는 다수의 경로를 가질 수 있으므로,  모든 경로에 대한 설명을 생성하는 함수를 만들 필요가 있다. 

1
2
3
[41]> (defun describe-paths (location edges) 
(apply #'append (mapcar #'describe-path (cdr (assoc location edges)))))
DESCRIBE-PATHS
cs

이 함수는 리스프에 익숙하지 않은 사람이라면 아주 이국적으로 보일 다수의 명령어를 포함하고 있다. 대부분의 프로그래밍 언어의 경우, 엣지에 대해 for-next loop 문을 보통 사용하여 각각의 경로에 대한 정보를 임시 변수를 이용해 저장할 것이다. 하지만 리스프는 다르다.

1
2
[42]> (describe-paths 'living-room *edges*)
(THERE IS A DOOR GOING WEST FROM HERE. THERE IS A LADDER GOING UPSTAIRS FROM HERE.)
cs

describe-paths 함수는 다음의 순서로 동작한다.

1. 관련있는 엣지를 찾는다

2. 그 엣지를 설명으로 변환시킨다.

3. 그 설명에  합류한다.


그럼 어떻게 동작하는지 살펴보자.

describe-paths 함수의 첫 번째 부분은 꽤나 명확하다. living room과 관련있는 경로와 엣지를 찾기 위해 다시 assoc 를 사용하여 엣지 리스트의 장소를 찾는다. cdr로 첫 번째 아이템을 제거하면 다음과 같다. 

1
2
3
4
5
6
Break 42 [43]> (assoc 'living-room *edges*)
(LIVING-ROOM (GARDEN WEST DOOR) (ATTIC UPSTAIRS LADDER))
Break 42 [43]> (cdr (assoc 'living-room *edges*))
((GARDEN WEST DOOR) (ATTIC UPSTAIRS LADDER))
Break 42 [43]>
 
cs

엣지를 설명부분으로 변환해보자.

1
2
3
Break 43 [44]> (mapcar #'describe-path '((GARDEN WEST DOOR) (ATTIC UPSTAIRS LADDER)))
((THERE IS A DOOR GOING WEST FROM HERE.) (THERE IS A LADDER GOING UPSTAIRS FROM HERE.))
Break 43 [44]>
cs

mapcar 함수는 리스퍼들이 자주 쓰는 함수 중의 하나로, 다른 함수와 리스트를 인수로 갖고, 리스트의 각각의 아이템에 대해 다른 함수가 적용된다. 아래의 예를 보자. 

1
2
3
Break 45 [46]> (mapcar #'sqrt '(1 2 3 4 5))
(1 1.4142135 1.7320508 2 2.236068)
Break 45 [46]>
cs

이 예는 (1 2 3 4 5) 라는 리스트에 sqrt (square root) 함수를 전달한다. 그 결과, 리스트의 각 멤버에 대해 sqrt 를 적용한 새로운 리스트를 반환한다. 또 다른 예를 보자. 

1
2
Break 46 [47]> (mapcar #'car '((foo bar) (baz qux)))
(FOO BAZ)
cs

이번에는 소스 리스트내에 작은 리스트 2개가 포함되어 있는 경우다. car 함수는 리스트의 첫 번째 아이템만 반환한다. 그러므로 첫 번째 리스트인 foo 와 baz 를 반환한다. 

이 대목에서 mapcar 는 왜 #' 심볼을 앞에 붙이는지 궁금할 것이다. 이 심볼은 함수를 나타내는 축약형태이다. 다음을 줄여서 표현한 것이다. 

1
2
Break 47 [48]> (mapcar (function car) '((foo bar) (baz qux)))
(FOO BAZ)
cs

function 이라는 이름은 프로그램 내의 다른 아이템의 이름과 충돌날 우려가 있기 때문에 위의 경우처럼 함수를 인수로 전달하는 경우에는 주로 #' 이 활용되는 것이다. 다음의 예를 보자.

1
2
3
[51]> (let ((car "Honda Civic"))
      (mapcar #'car '((foo bar) (baz qux))))
(FOO BAZ)
cs

이번 예에서는, car 심볼은 서로 다른 2개의 의미를 가질 수 있다. 리스트의 아이템을 다루는 기본 함수인 car 를 의미할 수도 있고, 지역 변수 car 를 의미할 수도 있는 것이다. 하지만 우리는 car 앞에 #' 을 붙임으로써 혼란스러울 것이 없다. 

describe-paths 함수를 다시 살펴보자. 

1
2
3
Break 53 [54]> (defun describe-paths (location edges)
(apply #'append (mapcar #'describe-path (cdr (assoc location edges)))))
DESCRIBE-PATHS
cs

append 와 describe-path 함수가 어떻게 값으로 apply와 mapcar에 전달되었는지 살펴보자. Common Lisp 는 함수 이름을 변수 이름과 분리하여 추적한다. 리스프는 변수를 위한 것과 함수를 위한 것들 등 다수의 네임스페이스를 가지고 있다. 

스키마는 함수와 변수를 위해 단일 네임스페이스를 가진다. 예를 들면, 스키마에서는 1에서 5까지 각각의 제곱근을 구하기 위해 (map sqrt '(1 2 3 4 5)) 를 사용하면 된다(map은 mapcar의 스키마 버전이다).

이러한 디자인 때문에, 스키마에서는 동일한 코드 블록에서 변수와 독립된 함수 사용이 불가능하다. 보기에 따라 이러한 디자인은 스키마의 커다란 장점 중 하나가 된다. 네임스페이스의 수에 관련된 차이점때문에 커먼 리스프가 리스프-2로 불리는데 비해, 스키마는 때로는 리스프-1 으로 불린다. 


Joining the Descriptions

다수의 리스트를 하나의 리스트로 결합해주는 append 함수를 통해서 설명 리스트와 경로 리스트를 하나의 단일 설명 리스트로 결합할 수 있다. 

1
2
3
4
5
 
Break 54 [55]> (append '(many had) '(a) '(little lamb))
(MANY HAD A LITTLE LAMB)
Break 54 [55]>
 
cs

append 함수를 이용해 하나로 결합시킬 수는 있지만, 여기서 문제는 얼마나 많은 경로가 주어질지 알 수 없다는 것이다. 여기서 이 문제를 해결하기 위해 apply 함수를 이용할 수 있다. 

1
2
3
4
5
 
Break 55 [56]> (apply #'append '((many had)(a)(little lamb)))
(MANY HAD A LITTLE LAMB)
Break 55 [56]>
 
cs


1
2
3
4
5
6
 
Break 57 [58]> (apply #'append '((THERE IS A DOOR GOING WEST FROM HERE.)
                                  (THERE IS A LADDER GOING UPSTAIRS FROM HERE.)))
(THERE IS A DOOR GOING WEST FROM HERE. THERE IS A LADDER GOING UPSTAIRS FROM HERE.)
Break 57 [58]>
 
cs

그럼 describe-paths 함수의 각 부분을 살펴보자. 

1
2
3
4
5
6
 
Break 56 [57]> (defun describe-paths (location edges)
                      (apply #'append (mapcar #'describe-path (cdr (assoc location edges)))))
DESCRIBE-PATHS
Break 56 [57]>
 
cs


describe-paths 함수는 2개의 파라메터를 가지는데, 하나는 현재 플레이어의 위치이고 나머지 하나는 게임맵용 edges/paths 리스트이다. 

1) 엣지 리스트로부터 정확한 위치를 assoc 를 통해 조회한다. assoc 는 리스트로부터 키와 값을 반환해주기 때문에, cdr 를 통해 이 중에서 값만 불러올 수 있는 것이다. 

2) 다음으로, mapcar 를 통해서 앞서 찾은 각각의 엣지에 대응하여  describe-path 함수를 매핑시킨다.

3) 마지막으로 리스트에 append 를 적용하여 모든 경로를 하나의 리스트로 결합시킨다. 


Describing Objects at a Specific Location


마지막으로 남은 부분이다. 플레이어가 주워서 사용할 수 있도록 주어진 장소의 오브젝트에 대한 묘사가 필요하다. 먼저 오브젝트 리스트부터 만들어보자. 

1
2
3
4
5
 
Break 63 [64]> (defparameter *objects* '(whiskey bucket frog chain))
*OBJECTS*
Break 63 [64]>
 
cs

각각의 오브젝트의 위치를 추적하는 2번째 변수인 *object-locations* 을 생성할 수 있다.

1
2
3
4
5
6
Break 64 [65]> (defparameter *object-locations* '((whiskey living-room)
(bucket living-room)
(chain garden)
(frog garden)))
*OBJECT-LOCATIONS*
 
cs

그리고 해당 위치에서 보이는 오브젝트의 목록을 보여주는 함수를 만든다.

1
2
3
4
5
Break 66 [67]> (defun objects-at (loc objs obj-locs)
 (labels ((at-loc-p (obj)
 (eq (cadr (assoc obj obj-locs)) loc)))
 (remove-if-not #'at-loc-p objs)))
OBJECTS-AT
cs

objects-at 함수는 labels 명령을 이용하여 at-loc-p 라는 새로운 함수를 선언했다(labels 함수는 로컬 함수를 정의함에 주의). at-loc-p 함수는 전역으로 사용되지 않기 때문에, objects-at 함수내에 바로 포함시켰다.

at-loc-p 함수는 해당 오브젝트의 심볼을 받아서 해당 오브젝트가 그 위치에 존재하는지 유무에 따라 t 또는 nil 을 반환한다. 

obj-locs 리스트내의 오브젝트를 조회한다. 그리고 eq 를 통해 찾은 위치가 질의한 위치와 같은지 확인한다. 그럼 여기서 왜 함수 이름을 at-loc-p 라고 지었는가? 함수가 nil 또는 t 값을 반환할 때 함수 이름의 긑에 p 를 붙이는 것이 커먼 리스프의 관례다. 예를 들면, oddp 5 처럼 5가 홀수인지 체크할 수 있다. 그런 T/F 함수는 소위 predicates 라고 불리고, 그래서 p 를 끝에 붙이는 것이다. 

remove-if-not 함수는 전달받은 함수(이 경우에는 at-loc-p 가 되겠다)가 true 를 반환하지 않는 모든 것을 제거한다. 즉, at-loc-p 가 true 인 오브젝트의 리스트만 필터링해서 반환하는 셈이다. 실제 어떻게 작동하는지 살펴보자.

1
2
3
4
 
Break 67 [68]> (objects-at 'living-room *objects* *object-locations*)
(WHISKEY BUCKET)
 
cs


Describing Visible Objects

이제 주어진 위치에서 보이는 사물을 기술하는 함수를 작성할 수 있게 되었다.

1
2
3
4
5
Break 68 [69]> (defun describe-objects (loc objs obj-loc)
 (labels ((describe-obj (obj)
`(you see a ,obj on the floor.)))
 (apply #'append (mapcar #'describe-obj (objects-at loc objs obj-loc)))))
DESCRIBE-OBJECTS
cs

이 리스트에서 describe-objects 는 먼저  describe-obj 함수를 생성한다. 이 함수는 quasiquoting 을 활용하여 주어진 물체가 플로어 위에 있다는 것을 서술하게 한다. 이 함수의 주요 부분은 현재 위치의 사물을 찾아 사물 리스트에서 매핑시키고, 최종적으로 해당 리스트에 설명을 덧붙이는 것이다. 그럼 describe-objects 를 실행해보자.

1
2
3
4
5
 
Break 69 [70]> (describe-objects 'living-room *objects* *object-locations*)
(YOU SEE A WHISKEY ON THE FLOOR. YOU SEE A BUCKET ON THE FLOOR.)
Break 69 [70]>
 
cs


Describing It All


look 이라는 간단한 명령어로 모든 설명 함수를 하나로 엮어보자. 왜냐하면 실제로 게임에서 주위를 둘러보게 될 것이므로 look 은 플레이어의 현재 위치를 알아야할 필요가 있다. 따라서 플레이어의 현재 위치를 추적하는 변수가 필요하다. 그 변수를 다음과 같이 *location* 이라고 부르자. 

1
2
3
4
 
Break 70 [71]> (defparameter *location* 'living-room)
*LOCATION*
 
cs

즉, *location* 이라는 변수는 living-room 으로 초기화된다. 

1
2
3
4
5
6
7
 
Break 71 [72]> (defun look ()
 (append (describe-location *location* *nodes*)
 (describe-paths *location* *edges*)
 (describe-objects *location* *objects* *object-locations*)))
LOOK
 
cs

look 함수는 전역 변수 이름 (*location*, *nodes* 등등)을 사용하고 있으므로, 플레이어는 값을 전달할 필요가 없다. 하지만 이것은 또 역시 look 함수가 기능적 프로그래밍 스타일이 아님을 의미하기도 한다(기능적 프로그래밍 스타일에서는 함수 그 자체에 선언된 파라메터나 변수만을 참조하기 때문이다). 

진행에 따라 플레이어의 위치가 바뀌게 됨으로, look은 서로 다른 시간에 서로 다른 일을 수행하게 된다. 다른 말로 하면, 여러분이 보고 있는 물체는 여러분의 위치에 따라 바뀌게 된다. 실제 쳐다볼 때 무슨 일이 일어나는지 살펴보자. 

1
2
3
4
5
6
 
Break 72 [73]> (look)
(YOU ARE IN THE LIVING-ROOM. A WIZARD IS SNORING LOUDLY ON THE COUCH. THERE IS A DOOR GOING WEST FROM HERE. THERE IS A LADDER GOING
 UPSTAIRS FROM HERE. YOU SEE A WHISKEY ON THE FLOOR. YOU SEE A BUCKET ON THE FLOOR.)
Break 72 [73]>
 
cs


Walking Around in Our World


이제 walk 함수를 만들어보자.

1
2
3
4
5
6
7
8
9
Break 73 [74]> (defun walk (direction)
 (let ((next (find direction
 (cdr (assoc *location* *edges*))
 :key #'cadr)))
 (if next
 (progn (setf *location* (car next))
 (look))
 '(you cannot go that way.))))
WALK
cs

먼저 이 함수는 현재 위치를 활용하여 *edges* 테이블에서 걸을 수 있는 경로를 찾는다. find 함수를 이용해 적절한 방향이 표시된 경로를 찾아낸다(find 는 리스트를 검색하여 해당되는 아이템을 찾아낸 뒤 반환한다). find 를 키워드 파라메터로 전달하여, 서쪽, 위, 아래 등 방향은 각각의 경로의 cadr 에 있으므로, 해당 리스트의 모든 경로의 cadr 에 대해 방향을 매칭하도록 find 함수에 지시해야 한다. 

커먼 리스프에서는 find 와 같은 다수의 함수들이 특수한 파라메터를 함수 호출 말미에 전달함으로써 접근할 수 있는 내장 기능들을 가지고 있다. 예를 들면 다음의 코드는 cadr 위치에 y 심볼을 가지고 있는 리스트의 첫 번째 아이템을 찾아준다. 

1
2
3
4
5
 
Break 74 [75]> (find 'y '((5 x) (3 y) (7 z)) :key #'cadr)
(3 Y)
Break 74 [75]>
 
cs

키워드 파라메터는 2부분은 구성된다. 첫번째는 콜론으로 시작하는 이름(위의 코드에서는 :key)이다. 두 번째는 값으로 위의 경우에는 #'cadr 이 이에 해당한다. 

일단 올바른 경로를 갖게되면, 다음 변수에 결과를 저장한다. if 표현식은 그리고 next 가 값을 가지고 있는지 체크한다( next 변수는 nil이 아니다). 만약 next 가 값을 가진다면, 유효한 방향이라는 의미이므로 플레이어의 위치를 조정하게 된다. 

look을 호출하면 새로운 위치의 설명을 불러내 값으로 반환한다. 만약 플레이어가 유효하지 않은 경로를 선택하면 look 은 새로운 설명대신에 경고를 생성해낼 것이다. walk 함수가 어떻게 작동하는지 살펴보자.

1
2
3
4
5
6
 
Break 75 [76]> (walk 'west)
(YOU ARE IN A BEAUTIFUL GARDEN. THERE IS A WELL IN FRONT OF YOU. THERE IS A DOOR GOING EAST FROM HERE. YOU SEE A FROG ON THE FLOOR.
 YOU SEE A CHAIN ON THE FLOOR.)
Break 75 [76]>
 
cs

방향 이름은 데이터모드로 쓰여져야하기 때문에, 방향 앞에 따옴표가 있음을 알 수 있다.


Picking Up Objects


물체를 집기 위해 물체의 위치를 추적하기 위해 *object-locations* 변수를 수정해야 한다.

1
2
3
4
5
6
7
Break 76 [77]> (defun pickup (object)
 (cond ((member object
 (objects-at *location* *objects* *object-locations*))
 (push (list object 'body) *object-locations*)
`(you are now carrying the ,object))
(t '(you cannot get that.))))
PICKUP
cs

pickup 함수는 해당 물체가 현재 위치의 floor에 있는지 확인하기 위해 member 함수를 사용한다. objects-at 명령을 사용해 현재 위치의 물체 리스트를 생성한다. 만약 물체가 현재 위치에 있다면, 아이템과 새로운 위치로 구성된 리스트인 *object-locations* 에 새로운 아이템을 집어넣는 push 명령을 사용한다. push 명령은 단순히 리스트 변수 목록의 앞에 새로운 아이템을 집어넣는 역할을 한다. 예를 들어, 다음의 코드는 리스트 1 2 3 에 숫자 7을 집어넣는 것이다. 

1
2
3
4
5
6
7
8
9
 
Break 78 [79]> (defparameter *foo* '(1 2 3))
*FOO*
Break 78 [79]> (push 7 *foo*)
(7 1 2 3)
Break 78 [79]> *foo*
(7 1 2 3)
Break 78 [79]>
 
cs

앞선 코드는 (setf *foo* (cons 7 *foo*)) 으로 대체할 수 있다. 하지만 이보다는 push 를 사용하는 편이 훨씬 간편하다. 

새로운 위치를 *object-locations* 에 넣는 것은 약간 이상해보일 수 있다. 해당 물체의 예전 위치를 삭제하지 않고, 새로운 것을 집어넣는다. 즉, *object-locations* 은 해당 물체에 대해 다수의 목록을 갖게 되는 셈이며, 이는 물체에 대해 2개의 저장된 위치를 리스팅한다. 

다행히 주어진 위치(objects-at 명령내에 있는)에서 물체를 찾는데 사용한 assoc 명령은 해당 리스트에서 찾은 아이템의 첫 번째를 반환한다. 따라서 push 명령을 사용하는 것은 assoc 명령으로 하여금 해당 주어진 키의 목록의 값이 대체된 것처럼 만든다. push 와 assoc 명령을 이런 식으로 동시에 사용하는 것은 실제로는 예전 값을 저장하고 있지만, 마치 alist 의 값이 변화하고 있는 것처럼 보이게 한다. 

예전 값은 단순히 새로운 값에 의해 막혀져 있을 뿐이다. 그런 이유로 모든 이전 값의 히스토리를 저장하는 것이다. push/assoc 은 리스퍼들이 흔히 쓰는 테크닉이다. 그러면 living room 으로 돌아와 물체를 집어보자. 

1
2
3
4
5
6
 
Break 79 [80]> (walk 'east)
(YOU ARE IN THE LIVING-ROOM. A WIZARD IS SNORING LOUDLY ON THE COUCH. THERE IS A DOOR GOING WEST FROM HERE. THERE IS A LADDER GOING
 UPSTAIRS FROM HERE. YOU SEE A WHISKEY ON THE FLOOR. YOU SEE A BUCKET ON THE FLOOR.)
Break 79 [80]>
 
cs


1
2
3
4
5
 
Break 80 [81]> (pickup 'whiskey)
(YOU ARE NOW CARRYING THE WHISKEY)
Break 80 [81]>
 
cs


Checking Our Inventory


마지막으로 현재 플레이어가 가지고 이동중인 물체를 조회하는 함수를 만들어보자. 

1
2
3
4
 
Break 81 [82]> (defun inventory ()
                (cons 'items- (objects-at 'body *objects* *object-locations*)))
INVENTORY
cs

위의 재고 함수는 요청받은 위치에 존재하는 물체의 목록을 objects-at 함수를 통해서 가져오고 있다. 여기서 어떤 위치를 조회하는 것인가? 플레이어가 물체를 집으면, 우리는 그 물체의 위치를 'body 로 변경했음을 알 수 있다. 지금 이 위치가 조회시 사용되는 위치인 것이다. 그러면 재고 함수를 실행해보자. 

1
2
3
4
5
 
Break 82 [83]> (inventory)
(ITEMS- WHISKEY)
Break 82 [83]>
 
cs

보는 바와 같이 방금 집은  whiskey bottle 이라는 1개의 아이템을 나르고 있음을 알 수 있다. 여기까지 해서 기본 엔진이 완성이 되었다.