Learning
레슨 7 / 8·4개 토픽

실전: 텍스트 어드벤처 게임

프로젝트 개요

지금까지 배운 테이블, 메타테이블, 코루틴, 문자열 패턴을 모두 활용하여 텍스트 기반 어드벤처 게임을 만들어 봅니다. 플레이어가 방을 이동하고, 아이템을 수집하며, 간단한 퍼즐을 풀 수 있는 게임입니다.

게임 상태와 아이템 시스템

테이블로 게임 월드를 구성하고, 메타테이블로 아이템의 공통 동작을 정의합니다. 각 방(room)에는 설명, 연결된 방, 놓인 아이템 정보가 있습니다.

lua
-- 아이템 클래스 (메타테이블 활용)
local Item = {}
Item.__index = Item

function Item.new(name, description, usable)
    return setmetatable({
        name = name,
        description = description,
        usable = usable or false,
    }, Item)
end

function Item:describe()
    return self.name .. " — " .. self.description
end

function Item:__tostring()
    return "[" .. self.name .. "]"
end

-- 아이템 생성
local key = Item.new("열쇠", "녹슨 황동 열쇠", true)
local note = Item.new("쪽지", "희미한 글씨가 적힌 쪽지", true)
local sword = Item.new("검", "빛나는 단검", true)

-- 방(room) 정의
local rooms = {
    entrance = {
        name = "입구",
        description = "어두운 동굴 입구입니다. 북쪽으로 통로가 보입니다.",
        exits = { north = "hallway" },
        items = { note },
    },
    hallway = {
        name = "복도",
        description = "길고 좁은 복도입니다. 횃불이 벽에 걸려 있습니다.",
        exits = { south = "entrance", east = "treasure", west = "armory" },
        items = { key },
    },
    armory = {
        name = "무기고",
        description = "먼지 쌓인 무기고입니다.",
        exits = { east = "hallway" },
        items = { sword },
    },
    treasure = {
        name = "보물방",
        description = "잠긴 문이 있습니다. 열쇠가 필요합니다.",
        exits = { west = "hallway" },
        items = {},
        locked = true,
    },
}

플레이어와 게임 루프 (코루틴)

코루틴으로 게임 흐름을 관리합니다. 각 턴마다 yield로 사용자 입력을 기다리고, resume으로 게임을 진행합니다.

lua
-- 플레이어 상태
local player = {
    location = "entrance",
    inventory = {},
}

-- 인벤토리 관리
function player:has_item(name)
    for i, item in ipairs(self.inventory) do
        if item.name == name then return i end
    end
    return nil
end

function player:take_item(item_name)
    local room = rooms[self.location]
    for i, item in ipairs(room.items) do
        if item.name == item_name then
            table.insert(self.inventory, item)
            table.remove(room.items, i)
            return "'" .. item.name .. "'을(를) 획득했습니다."
        end
    end
    return "그런 아이템이 없습니다."
end

-- 게임 코루틴
local game_loop = coroutine.create(function()
    print("=== 텍스트 어드벤처 ===")
    print("명령어: 이동 [방향], 줍기 [아이템], 보기, 가방, 사용 [아이템]\n")

    while true do
        local room = rooms[player.location]
        print("[" .. room.name .. "] " .. room.description)

        if #room.items > 0 then
            io.write("아이템: ")
            for _, item in ipairs(room.items) do
                io.write(tostring(item) .. " ")
            end
            print()
        end

        -- 입력 대기 (yield)
        local input = coroutine.yield("입력 대기")
        -- input 처리는 다음 섹션에서
    end
end)

명령어 파서 (패턴 매칭)

string.match와 패턴을 사용하여 플레이어의 자연어 입력을 파싱합니다. "이동 북쪽", "줍기 열쇠" 같은 명령을 인식합니다.

lua
-- 방향 매핑
local dir_map = {
    ["북"] = "north", ["남"] = "south",
    ["동"] = "east",  ["서"] = "west",
    ["북쪽"] = "north", ["남쪽"] = "south",
    ["동쪽"] = "east",  ["서쪽"] = "west",
}

-- 명령어 파싱 함수
local function parse_command(input)
    -- 공백 정리
    input = string.gsub(input, "^%s+", "")
    input = string.gsub(input, "%s+$", "")

    -- "이동 [방향]"
    local dir = string.match(input, "^이동%s+(.+)$")
    if dir then
        local eng_dir = dir_map[dir]
        if eng_dir then
            local room = rooms[player.location]
            if room.exits[eng_dir] then
                local target = room.exits[eng_dir]
                local target_room = rooms[target]
                -- 잠긴 방 체크
                if target_room.locked then
                    if player:has_item("열쇠") then
                        target_room.locked = false
                        print("열쇠로 문을 열었습니다!")
                    else
                        print("문이 잠겨 있습니다.")
                        return
                    end
                end
                player.location = target
            else
                print("그 방향으로 갈 수 없습니다.")
            end
        else
            print("알 수 없는 방향입니다.")
        end
        return
    end

    -- "줍기 [아이템]"
    local item_name = string.match(input, "^줍기%s+(.+)$")
    if item_name then
        print(player:take_item(item_name))
        return
    end

    -- "가방" — 인벤토리 확인
    if string.match(input, "^가방$") then
        if #player.inventory == 0 then
            print("가방이 비어 있습니다.")
        else
            print("=== 가방 ===")
            for _, item in ipairs(player.inventory) do
                print("  " .. item:describe())
            end
        end
        return
    end

    print("알 수 없는 명령입니다.")
end

-- 게임 실행 예시 (실제로는 io.read()와 함께)
-- coroutine.resume(game_loop)
-- parse_command("줍기 쪽지")
-- coroutine.resume(game_loop, "이동 북쪽")
  • 메타테이블(Item)로 아이템 공통 동작 정의
  • 테이블로 게임 월드(방, 연결, 아이템) 구성
  • 코루틴으로 턴 기반 게임 루프 구현 (yield/resume)
  • 패턴 매칭으로 자연어 명령어 파싱
  • pcall로 예상치 못한 에러 안전하게 처리
💡

이 프로젝트를 확장하려면 NPC 대화(코루틴), 전투 시스템(메타테이블 상속), 세이브/로드(파일 I/O) 기능을 추가해 보세요.