Text Adventure

Hi guys…

Following on from thread on text input, this is where I’m up to with a ‘very’ basic adventure game. Atm it just has a working map and location descriptions. I want to introduce objects next and this is my query.

It seems logical to put them in the room tables, but I also want to give them attributes e.g. certain objects can’t be taken, some are initially hidden until discovered and so on… I’m not sure I can do this within the room table.

So next thought was an objects table… objects={lamp=3, key=9… etc} where the number is the initial location, but again I can’t have more than the one attribute can I?

So then I considered objects={lamp,key,rope…} objRooms={3,9,5…} objHidden={0,0,1…} This latter choice is harder to keep track of but seems more flexible.

I’m sure there is an ideal way of dealing with this, so before I get completely lost, any ideas?

Thanks!



--# Main
-- Adventure 2.1

function setup()
displayMode(FULLSCREEN)
font("Courier")
textWrapWidth(WIDTH-50)
strTab={"WELCOME to Islands of Adventure v2.1, an interactive adventure game. This is a basic framework with a simple working map, but you can only type in simple direction commands, n,s,e,w,u,d.\
\
FOREST\
You are deep inside a dark forest. The undergrowth makes it very difficult to wander off a path that leads to the North.\
There appears to be something hidden in the bushes to your left."}
buffer=""
showKeyboard()
textMode(CORNER)
str=0 

map()

end

function draw()
background(30,30,0,255)
fill(255)
text(">",30,400)
if buffer~="" then
text(buffer,45,400)
end
lineSize=0 
for z=#strTab,1,-1 do
if (z+1)%2==0 then
fill(255,255,0)
text(strTab[z],30,465+lineSize) 
else
fill(255)
text("> "..strTab[z],30,465+lineSize)
end
w,h=textSize(strTab[z])
lineSize=lineSize+h
end
end

function touched() 
showKeyboard()
end

function keyboard(key)
if key == RETURN then
table.insert(strTab,buffer)
buffer=""
input=strTab[2*(str+1)]

parse(input)

elseif key==BACKSPACE then 
buffer=string.sub(buffer,1,string.len(buffer)-1) 
else
buffer=buffer..key
end 
end


--# Map
function map()

room={}
myLoc=1 


room[1]={n=4,s=0,e=2,w=0,u=0,d=0,name="FOREST",desc="#1 You are lost in a dark, dense forest."}
room[2]={n=5,s=0,e=3,w=1,u=10,d=0,name="CLEARING",desc="#2 You are standing in a clearing. There is a large oak tree in the middle."}
room[3]={n=6,s=0,e=0,w=2,u=0,d=0,name="CAVE",desc="#3 You are in a cold damp cave."}
room[4]={n=7,s=1,e=5,w=0,u=0,d=0,name="BEACH",desc="#4 You are on a warm sandy beach."}
room[5]={n=8,s=2,e=6,w=4,u=0,d=0,name="HUT",desc="#5 You are stood inside a small beach hut."}
room[6]={n=9,s=3,e=0,w=5,u=0,d=0,name="WATERFALL",desc="#6 You are standing under a waterfall. There is a small opening behind you."}
room[7]={n=0,s=4,e=8,w=0,u=0,d=0,name="CLIFF",desc="#7 You are standing at the base of a steep cliff."}
room[8]={n=0,s=5,e=9,w=7,u=0,d=0,name="SEA",desc="#8 You are splashing around in the sea."}
room[9]={n=0,s=6,e=0,w=8,u=0,d=0,name="BOAT",desc="#9 You are in a small rowing boat."}
room[10]={n=0,s=0,e=0,w=0,u=0,d=2,name="TREE TOP",desc="#10 Wow... a wonderful view of the entire island. It's not very big."}

end
--# Parser
function parse(input)

print(strTab[2*(str+1)])

if input~="n" and input~="s" and input~="e" and input~="w" and input~="u" and input~="d" then
table.insert(strTab, "Sorry, but I don't understand what you mean? For now just stick with basic directions.")

elseif room[myLoc][input]==0 then
table.insert(strTab, "You can't go in that direction")

else myLoc=(room[myLoc][input]) 
table.insert(strTab,room[myLoc].name.."\
"..room[myLoc].desc)
end

str=str+1

end

@David Nice going. Since your input is one character, skip having to press return. Just accept n,s,e,w,u,d.

EDIT: Try this.

function keyboard(key)
    parse(key)
end

What if each object had a table? For example

objects = {
    lamp = {id = 3, hidden = 0, room = 7}, 
    key = {id = 9, hidden = 1, room = 3}
}

Thanks @dave1707. I’m intending to use standard verb/noun parsing but nice that I can do this for now.

And @JakAttak - that’s great… I had no idea you could nest tables 3 times like this.
I can retrieve specific information such as objects.lamp.room = 7, but suppose I enter room 7 and want to know which objects are there? I can’t use objects.room or objects.room=7. Is there a way, without referencing each individual object?

@David Sorted like this, I think the only way is looping through all objects. You can consider nesting the tables differently, like:

room = {1, 2, 3, 4}
room[1].objects = {
                   lamp = {id = 3, hidden = 0}, 
                   key = {id = 9, hidden = 1}
                  }

This would make the number of iterations significantly smaller in a larger game.

There are a few ways to do this. Another is like this, which uses constants to avoid the need to name all the table elements and is more compact

--constants
HIDDEN,SHOWN=0,1 --visibility
--object list
LAMP,KEY,CHEST,SWORD=0,1,2,3
Etc.......

--room properties
room[1]={
               exits={0,6,0,8,0,0}, 
               name="Boat",
               desc=" you are in a rowing boat"
               objects={{LAMP,HIDDEN}, {KEY,SHOWN}}
                }

If you want to make complex objects with more properties, or NPCs, you could create classes for them so each individual can be made different.

But what I might suggest is looking at some of the code people have written for this over the years, to get ideas, eg (found in 2 minutes on Google)
https://github.com/gvx/adventure/
http://shawndumas.github.io/adventure.lua/
http://www.anotherivan.com/lua-scripting/
http://secretgeek.net/text_adventure_samples.asp

It’s a lot of fun seeing how it’s been done before!

Thanks @Kjell - I’m looking at that now but can’t work out how to retrieve info…
tried print(room[1].objects.key.id)
print(room[1].[“objects”].[“key”].[“id”]
and a some others but no luck so far…

Thanks @Ignatz… the links are great - no idea why I never thought to google ‘text adventure lua’ before! But a whole bunch of useful information out there.

@David It looks like I made a mistake in the example I gave you. When defining the room table, I put numbers in it, but I should’ve defined every entry as a table. This is the correct code:

room = { {}, {} } -- Define empty table for every room, could also be done in a loop
room[1].objects = {
                   lamp = {id = 3, hidden = 0}, 
                   key = {id = 9, hidden = 1}
                  }

I’d go for classes here.

Room = class()

function Room:init()
    self.objects = {}
end

function Room:placeObject(o)
    table.insert(self.objects,o)
    o:setRoom(self)
end

function Room:removeObject(o)
    local l
    for k,v in ipairs(self.objects) do
        if v == o then
            l = k
            break
        end
    end
    if l then
        table.remove(self.objects,l)
    end
end

Object = class()

function Object:init(t)
    t = t or {}
    self.room = t.room
    self.hidden = t.hidden
end

function Object:setRoom(r)
    if self.room then
        self.room:removeObject(self)
    end
    self.room = r
end

This way, every object has a pointer to its containing room and every room has a table of its objects. When moving an object from one room to another then you call the method room:placeObject(object) and that takes care of removing it from the old room, placing it in the new room, and updating the object’s pointer.

Thanks @Kjell… that works now!

And thank you @Andrew_Stacey… I’m hastily reading @Ignatz eBook chapters on Classes to follow what you’ve done! If I did use this method, am I able to easily add other attributes to the objects? As well as room# and hidden# there may need to be other flags eg lamp running out after 10 turns.

@David Yes, adding other attributes is straightforward. As is updating them if they change.

Hi guys… I’ve been looking at classes and this is all I’ve managed to come up with so far…

--# Main
-- Classes

function setup()
    KEY = Object("Key",1,0)
    LAMP = Object("Lamp",3,1)
    ROPE = Object("Rope",6,0)
    
print(KEY.room)
print(LAMP.name)
print(ROPE.hidden) 

end

function draw()
    background(30,30,30)    
end

--# Objects
Object = class()

function Object:init(name, room, hidden)    
    self.name = name
    self.room = room
    self.hidden = hidden
end

I can very easily produce a database of objects with attributes shown, and access the info. But I’m struggling to access all the objects that may be in a particular room, e.g. print(Object.room[1]). There may be several too.
@Andrew_Stacey I’ve tried to follow what you’ve done, but I can’t find a tutorial that takes me much beyond where I am.
Thanks :wink:

@David Why not create a “room” class. That way you can create a table of objects that are in each room. You can create as many rooms as you want with as many objects as you want in each room.

Yes that’s how it is in @Andrew_Stacey’s example as well

@David Here’s an example of what I was saying.


--# Main
-- Classes

function setup()
    rm1=Room("Dungeon",{{"Key",1,0},{"Lamp",3,1,6,6,6},{"Rope",6,0}})
    print(rm1.name)
    print(rm1.objects[1][1],rm1.objects[1][2],rm1.objects[1][3])
    print(rm1.objects[2][1],rm1.objects[2][2],rm1.objects[2][3])
    print(rm1.objects[3][1],rm1.objects[3][2],rm1.objects[3][3])
    print(rm1.nbr)
    for a,b in pairs(rm1.objects) do
        for c,d in pairs(b) do
            print(d)
        end
    end
end

function draw()
    background(30,30,30)    
end

Room=class()

function Room:init(name,objects)
    self.name=name
    self.objects=objects  
    self.nbr=#objects  
end

Thanks guys… this is making more sense now. But if you’re including a table within the room class, you don’t need to define it first like this… from @Ignatz eBook?

function setup() 
--define objects 
objects={} --table to hold type and location for each object 
object[1]={type=Ball,x=100,y=100,direction=0} 
object[2]={type=Balloon,x=150,y=75,direction=-50) 
--and we can define more balls, or balloons, or mines, the same way 
end

Also, @Andrew_Stacey had Room and Object classes. Your code @dave1707 (ty!!) seems to do everything I’d want so is there any advantage to having an objects class too? I’m still confused as to how they would work together.

I normally pick things up really quickly, but it’s taken me a while to get my head around this… but getting there. Ty!

One of the reasons I was focusing on an object class rather than room class, was I figured it would be very easy to scan a list of objects looking for the room# and hidden# attributes, and list them for any room you are in.

Whereas adding them to the existing room data means that because there will be some rooms with no objects and some with maybe 5 or 6, I also then need to keep track of how many objects are in each room in the table so I can list them… and then update that if one is taken or dropped.

@David I changed my code above to show how easy it is to get the info of the objects. I added self.nbr which tells you how many objects there are. I added “for” loops in setup to show you how to list the object information. I also added more information to the Lamp object just to show how easy you can add more information if you want. There isn’t any reason to have an object class if you use the room class.

@dave1707 That’s brilliant! ty so much. I actually completely understand all that - I had no idea you could #objects so that makes it all straightforward now. Onwards and upwards :wink:

@David You could also do “self.nbr=#self.objects” for the number of objects. I don’t know if it makes a difference or not which one is used, but I like the #self.objects. I think #self.objects keeps it cleaner.