LLama Index를 활용한 더 지니어스 게임 AI로 제작 (게임 UI 반영)

배경 및 목적

지난 주까지 캐릭터간 대화 내용을 반영하였습니다.

지니어스 게임에서는 캐릭터가 '특정 공간'에 참여하여 서로간의 전략을 논의하는 장면이 매 게임마다 이루어집니다.

이번 주에는 사용자가 특정 방을 선택하면 방에 속해있는 캐릭터간의 대화를 확인할 수 있는 것을 LLama Index를 통해서 구현하였습니다.

참고 자료

기존과 동일합니다.

이번에는 Claude와 GPT를 적극적으로 활용하였습니다.

활용 툴

  1. Dall-E: Dall E를 통해서 캐릭터들의 이미지를 생성하였습니다.

  2. Claude: 지난 주에 두 캐릭터간의 대화를 게임 방식으로 변경하기 위해서는 기존 코드를 구조화해야 할 필요가 있었습니다. (클래스화) 이 부분에 있어서 코드 개발에 좀 더 안정적인 Claude 를 사용하였습니다.

  3. ChatGPT: Claude 요금제를 신청하지 않고 있어서, 호출 횟수가 다 끝나면 ChatGPT로 변경하였습니다.

  4. Python

    1. pygame

      1. 파이썬에서 제공하는 game module을 처음 알 수 있었습니다. 플라스크 웹 방식으로 구현하거나, .exe 파일로 만들어야 했는데 좋은 모듈을 찾아서 게임방식으로 구현합니다.

실행 과정

캐릭터의 이미지 생성

게더타운과 같은 방식으로 구현해보려고 했었습니다. 특히 API도 제공을 하고 있고 구조도 비슷하기 때문에 구현할 경우 실제와 유사하게 동작할 것이라고 예상하였습니다. 하지만 한정된 시간에 API 학습까지 함께 진행하기는 어려울 것 같아 API를 활용하지 않았습니다.

직접적으로 캐릭터를 그릴 필요가 생겼고, 다행히도 기존에 캐릭터 성격을 이미 만들어놨기 때문에, ChatGPT Dall-E를 통해서 쉽게 생성하였습니다.

캐릭터가 직접 움직이는 형식을 구현하면 좋겠지만, 캐릭터 얼굴만 그리도록 했습니다.

수치심은 참을 수 없다 | imgflip 밈 메이커로 제작

캐릭터 얼굴 이미지만 제작
1. 마인크래프트 스타일
2. 32x32 pixel size
3. 캐릭터 성격
    {
        "name": "Hyun-Ah",
        "age": 30,
        "role": "프로 마술사",
        "background": "세계 각지를 돌며 공연을 펼친 유명 마술사. TV 쇼에서도 활약 중이며, 사람의 시선을 교묘히 조작하는 데 능숙함.",
        "traits": [
            "유머러스하고 외향적임",
            "타인을 쉽게 매료시킴",
            "재치 있고 순발력이 뛰어남",
            "창의적이고 혁신적인 사고방식",
            "강한 직관력"
        ],
        "strengths": [
            "상대의 주의를 분산시키는 능력",
            "의도적으로 혼란을 유발하는 플레이",
            "눈속임과 기만 전술에 능함",
            "예측 불가능한 행동으로 상대를 혼란스럽게 함"
        ],
        "weaknesses": [
            "때로는 과도한 쇼맨십으로 진실성을 의심받음",
            "장기적인 계획보다는 즉흥적인 행동을 선호함"
        ],
        "favorite_quote": "불가능은 단지 환상일 뿐이다.",
        "hidden_talent": "뛰어난 픽포켓 기술 보유",
        "strategies": [
            {
              "name": "환영 카드",
              "description": "상대방을 혼란시키기 위해 거짓으로 제시하는 카드"
            },
            {
              "name": "목매듭 승점",
              "description": "예상치 못한 방법으로 획득한 승점"
            }
          ]        
    },

픽셀 아트에 담긴 소녀의 이미지

코드의 구조화

기존 코드는 주피터 노트북 방식으로 구현되어 있었습니다. 코드 로직보다는 구현에 더 집중을 하였습니다. 이러한 개발 방식은 UI가 반영된 모듈의 경우 정상적으로 동작하지 않을 경우가 많아 클래스로 변경하면서 코드에 대한 전반적인 리펙토링을 진행하였습니다.

코드 구조

전체 코드를 구현한 후 GPT에 코드 구조를 제작하도록 요청하였습니다.

👍👍👍👍👍 GPT는 항상 기대 이상의 효과를 만들어냅니다.!!

소셜 네트워크의 구조를 보여주는 다이어그램

Class Definitions:

  • GameData:

    • __init__: 게임 데이터 로딩 및 저장 여부 확인.

    • get_main_match_description: 쿼리 엔진을 통해 첫 번째 메인 매치 설명을 반환.

    • load_strategies: 게임 전략을 JSON 파일에서 로드.

    • load_player_characters: 게임 캐릭터 정보를 JSON 파일에서 로드.

  • AIAgent:

    • __init__: Nest_asyncio와 LLM 에이전트를 초기화.

    • gameplayers: 게임 참가자 캐릭터를 무작위로 선택.

    • select_game_members: 무작위 캐릭터 선택 함수 호출.

    • get_member_info: 선택된 캐릭터의 게임 스타일을 AI로부터 받아옴.

    • ai_generate_dialogue: AI가 게임 시나리오에 맞는 대화를 생성.

  • Room:

    • __init__: 방 객체 초기화, 방 위치, 캐릭터, 대화 관련 기능 초기화.

    • add_characters: 랜덤한 좌표에 캐릭터를 추가.

    • save_dialogue_to_json: 생성된 대화를 JSON 파일로 저장.

    • draw: 방과 캐릭터를 화면에 렌더링.

    • generate_dialogue: AI 에이전트가 대화를 생성하도록 요청.

  • Game:

    • __init__: 게임 초기화, 화면 설정, 캐릭터 로드, 방 생성.

    • load_character_image, load_character_images: 캐릭터 이미지를 로드.

    • create_rooms: 방을 생성하고 캐릭터 배치.

    • run: 게임 루프 실행.

    • handle_mouse_click: 방 클릭 이벤트 처리.

    • draw: 화면에 방, 캐릭터, 대화 상자 그리기.

    • draw_dialogue_box: 대화 상자와 내용을 화면에 그리기.

개발 → 테스트 → 디버깅 → 개발 →

이제는 원하는 항목이 구현될 때까지 GPT와 씨름을 계속합니다. ಥ_ಥ

주요 개발 영역

  1. 게임 데이터의 벡터화

게임 데이터가 주기적으로 변경될 것이 아니기 때문에 매번 새롭게 생성하는 것보다는 storagecontext를 활용한 기존 벡터를 활용한 방식이 용이해보였습니다.

class GameData:
    def __init__(self):
        if not os.path.exists("./game_data/storage"):
            self.documents = SimpleDirectoryReader("./game_data/game_scenario").load_data()
            self.index = VectorStoreIndex.from_documents(self.documents)
            self.index.storage_context.persist()    
        else:
            self.storage_context = StorageContext.from_defaults(persist_dir="./game_data/storage")
            self.index = load_index_from_storage(self.storage_context)        
        self.query_engine = self.index.as_query_engine()
  1. 보다 다양한 캐릭터의 성격을 반영

기존에 AI가 만들어주는 캐릭터간의 대화 내용을 보면 너무 일반적인 톤으로 대화하였습니다. 이러한 방식은 실제 게임과는 비슷한 느낌을 제공하지 않기 때문에 보다 다양한 성격과 대화 방식을 제공해주기 위해 대화 내용을 만드는 프롬프트를 개선하였습니다.

A. 프롬프트의 변경 1차

캐릭터 게임 플레이에 나이, 성별, 성격을 반영하여 게임 플레이 방식을 고려하라고 함.

  • 기존 프롬프트:

    what's the {game_member}'s game playing style? Explain it in Korean language
  • 변경 후 프롬프트:

    {member}의 게임 플레이 스타일은 어떠한가요? 나이, 성별, 성격에 비추어 게임 플레이 방식을 한글로 설명하세요.

B. 프롬프트의 변경 2차

대화 내용을 만드는 프롬프트에 한국인 연령별로 가지고 있는 스테레오타입을 GPT를 통해서 구해서 이 내용을 반영하였습니다. 캐릭터 특성에 연령, 성별이 명시되어 있기 때문에 GPT가 이를 파악할 것이라고 생각하였습니다.

For example                
- 20대 남성
사회 초년생: 20대 남성은 대부분 대학을 졸업하고 막 사회에 진출한 신입사원으로 인식됩니다. 취업 준비나 군복무 경험을 바탕으로 성장을 시작하는 시기입니다.
자유를 중시: 대학 생활이나 사회생활 초기의 자유를 중요시하며, 여행, 취미생활, 자기 계발에 관심이 많습니다.
현실적 고민: 많은 20대 남성들이 경제적 부담, 취업난, 미래의 불안정성 등 현실적인 고민을 가지고 있습니다.
- 20대 여성
자기 계발: 20대 여성은 자기 계발을 중요시하며, 학업, 직업, 외모 관리에 많은 시간을 투자하는 시기로 인식됩니다.
독립과 자유: 여성들도 대학 생활에서 독립을 경험하고 자유로운 생활을 즐기며, 여행과 취미 활동에 대한 관심이 큽니다.
커리어 지향: 20대 여성 중 많은 이들이 자신의 커리어를 확립하기 위해 노력하며, 사회적 성취에 대한 열망이 큽니다.
- 30대 남성
안정적 직업: 30대 남성은 사회적으로 안정된 직장을 가진 가장으로 인식되며, 직업적 성공과 경제적 안정을 추구하는 이미지가 강합니다.
가정 중심: 결혼과 자녀 양육을 시작하는 시기인 만큼, 가정을 꾸리고 경제적 책임을 지는 가장으로서의 이미지가 있습니다.
사회적 성공 압박: 30대 남성은 커리어에서 성공을 이루어야 한다는 압박을 받기도 하며, 승진이나 자산 축적에 대한 욕구가 큽니다.
- 30대 여성
커리어와 가정의 균형: 30대 여성은 커리어와 가정 사이에서 균형을 잡으려는 경향이 있으며, 직장에서의 성취와 가정에서의 역할을 동시에 이루려는 이미지가 있습니다.
결혼과 육아: 30대 중반부터 결혼과 출산을 경험하는 여성들이 많으며, 이에 따라 일과 가정의 조화를 이뤄야 하는 부담이 있습니다.
자아 실현: 여전히 자기 계발에 대한 욕구가 강하며, 직업적으로도 더 많은 성장을 이루려는 경향이 있습니다.
- 40대 남성
성공의 상징: 40대 남성은 직업적으로 높은 위치에 올라 있으며, 경제적 안정성을 갖춘 성공적인 인물로 인식되곤 합니다.
가정의 기둥: 가정을 이끄는 역할이 강조되며, 자녀 교육과 가정의 경제적 안정에 대해 많은 책임을 느끼는 시기입니다.
건강 관리: 40대가 되면 자신의 건강에 대한 관심이 증가하고, 운동이나 건강관리에 시간을 투자하는 이미지도 있습니다.
- 40대 여성
가정 관리: 40대 여성은 자녀 양육에 많은 시간을 보내며, 가정의 중심 역할을 담당하는 이미지가 강합니다.
직업적 성취: 경력이 있는 40대 여성들은 직장에서 중간 관리자급으로 성장해 있으며, 직업적 성취를 이어가려는 이미지도 있습니다.
사회적 기여: 40대 여성들은 사회나 커뮤니티 활동에 참여하여 자원봉사, 사회 기여 활동을 활발히 하는 경향이 있습니다.

  1. 각각의 방을 꾸미기

A. 방 안에 캐릭터를 할당하기

12명의 캐릭터를 방1 ~ 방5, 로비에 2명씩 항상 랜덤으로 할당하게 됩니다. 그래서 게임을 플레이할 때마다 다른 방식의 대화가 발생하도록 합니다.

class Room:
    def add_characters(self, character_names):
        self.characters = []
        for name in character_names:
            x = random.randint(self.rect.left, self.rect.right - 32)
            y = random.randint(self.rect.top, self.rect.bottom - 32)
            self.characters.append((x, y, name, character_images[name]))  # 이름도 저장

B. 대화 내용을 json으로 저장

LLamaIndex로 나온 대화 내용은 다음에 활용하기 어렵도록 구어체 방식으로 구현되어 있습니다. 그렇기 때문에 json 형식으로 출력되도록 프롬프트에서 명시한 후 이를 json 형식으로 변환하고, 별도의 json 파일로 저장하였습니다.

이렇게 저장한 대화 내용을 추후에 음성 스크립트에 반영해보려고 합니다.

    def generate_dialogue(self, ai_agent, game_members_info, main_match_description):
        if len(self.characters) == 2:
            _, _, char1_name, _ = self.characters[0]  # 캐릭터 1의 이름
            _, _, char2_name, _ = self.characters[1]  # 캐릭터 2의 이름
            char_info = {char1_name: game_members_info[char1_name], char2_name: game_members_info[char2_name]}
            
            # 대화 생성
            dialogue_gen = ai_agent.ai_generate_dialogue(char_info, main_match_description)
            full_dialogue = ''.join(token for token in dialogue_gen)

            # 대화를 캐릭터별로 분리 (예시: "**캐릭터명**: 대화내용")
            dialogue_lines = full_dialogue.split("**")[1:]  # "**"로 시작하는 대화 부분만 추출
            dialogue_dict = {}

            for i in range(0, len(dialogue_lines), 2):
                    character_name = dialogue_lines[i].strip(":")  # 캐릭터 이름 추출
                    character_dialogue = dialogue_lines[i + 1].strip()  # 대화 내용 추출
                    
                    # 대화가 이미 존재하는 경우 리스트에 추가, 처음인 경우 리스트 생성
                    if character_name not in dialogue_dict:
                        dialogue_dict[character_name] = []
                    
                    dialogue_dict[character_name].append(character_dialogue)
                    # JSON 저장을 위한 형식 변환
            dialogue_data = {
                "room": self.name,
                "dialogue": [
                    {
                        "character": char_name,
                        "text": char_dialogue
                    }
                    for char_name, char_dialogue in dialogue_dict.items()
                ]
            }

            # 대화 저장 (방 이름과 대화 내용을 저장)
            self.save_dialogue_to_json(dialogue_data, self.name)


            # 현재 대화 내용 설정
            self.dialogue = full_dialogue  
  1. 게임 인터페이스 구현하기

A. 키보드로의 이동에서 마우스로 이동

개인 캐릭터를 키보드로 이동하여 각 방으로 이동했을 때 방에서 진행하는 대화 내용을 파악하도록 구현하고 싶었습니다. 키보드로 구현 시 키보드를 오랫동안 눌렀을 때에 대한 반응, 사각형 공간을 이동했을 때의 고려를 해야할 필요가 있었습니다. 구현하기 쉽도록 하기 위해 사용자가 방을 클릭하면, 해당 방에서 이벤트가 발생하는 방식으로 변경하였습니다.

class Game:
    def run(self):
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    self.handle_mouse_click(event.pos)
                elif event.type == pygame.MOUSEWHEEL:
                    # 마우스 휠 이벤트로 스크롤 오프셋 조절
                    self.scroll_offset += event.y * 10  # 마우스 휠 값에 따라 스크롤 이동 속도 조절
                    self.scroll_offset = max(self.scroll_offset, 0)  # 최상단으로 스크롤 제한
                    

            self.draw()
            pygame.display.flip()

        pygame.quit()

B. 스크립트 내용이 길 경우 대화 내용이 보이지 않음.

대화 내용이 길 경우 스크립트가 보이지 않아 스크롤 기능을 추가하였습니다.

결과 및 인사이트

UI에 대한 센스가 없어서 아쉬운 점이 컸습니다. 현재까지는 게임 플레이에 대해서 구현하지 못했지만, 각각의 방에서 전략을 세운 캐릭터들이 본인의 전략에 맞게 게임을 실행하는 것까지 구현해보고 싶습니다.

이에 따라서 어떤 결과가 나올지도 궁금합니다.

이번 주에는 llama index에 대해 새롭게 구현한 부분을 많이 사용하지 않아 좀 아쉽습니다.

다음 단계로 나가는데 있어서 llama index의 사례를 반영할 것이 있으면 좀 시도해보겠습니다

3

👉 이 게시글도 읽어보세요