making a text adventure game

Game development specific discussions.
Post Reply
BasicCoder2
Posts: 3917
Joined: Jan 01, 2009 7:03
Location: Australia

making a text adventure game

Post by BasicCoder2 »

Last weekend I spent some time on the internet looking for tutorials on the how to write a text based adventure game. There was a QBASIC example but I find the old gosub, goto code unreadable requiring hours to decipher. These days when I search for any coding examples they all seem to be written in Python! Other text adventure game tutorials used Unity, Inform 7 and so on.

I tried to write a FB version of this tutorial.
https://medium.com/@model_train/video-t ... a194aeab44

I even used MSPAINT to rough out some graphics to go with it, but when nearly completed decided I was going about it the wrong way.

It seems to me you need to first work out how to write a data base of the "world". Then you work out the routines to manipulate that data base. Lastly you can work on the parser to make it easier call the required routines.

It is in fact the parser that interests me most, the problem of converting a human language sentence into meaningful actions, but first you need a "world" (data base) to test it on. However my weekend effort was going to start simple with single word commands like quit, north, south, east, west, look, pick_up, put_down, eat, open, close and so on ... or verb noun commands like go north, move north, and so on...

The code below was as far as I got. It only uses the commands quit, north, south, east, west, and look. Unlike the internet example my locations are east or west of each other. So at the start the command "east" will move the player from the lake location to the forest path location.

Any comments welcome.

To use the graphics with the code you need to convert the locations.png to locations.bmp
Image

Code: Select all

' https://medium.com/@model_train/video-tutorial-create-a-text-adventure-game-in-30-minutes-in-the-browser-f4a194aeab44

screenres 960,500,32
'bload "locations.bmp"

dim shared as string sentence             'input sentence
dim shared as string words(0 to 20)       'list of words in sentence
dim shared as integer wordCount           'number of words in sentence

sub getWords()
    dim As Integer charCount
    dim as string word,char
    wordCount = 0
    charCount = 1
    word = ""
   
    while charCount < len(sentence)+1
        
        word = ""  'next word
    
        char = mid(sentence,charCount,1)  'get first character
        'skip any leading spaces
        while char = " " and charCount < len(sentence)+1
            charCount = charCount + 1
            char = mid(sentence,charCount,1)  'get next character
        wend
    
        'collect any word
        char = mid(sentence,charCount,1)
        while char <> " " and charCount < len(sentence)+1
            word=word+char
            charCount = charCount + 1
            char = mid(sentence,charCount,1)
        wend
    
        words(wordCount)=word  'add word to word list
        wordCount = wordCount + 1
        
    wend
end sub


type ITEM
    as string iName      'key, chest, apple, gem, ...
    as integer state
    as string states(1 to 2)
    as integer location  'where the item resides
end type

dim shared as ITEM items(1 to 2)
items(1).iName =  "TROLL"
items(1).states(1) = "alive"
items(1).states(2) = "asleep"
items(1).state = 1
items(1).location = 3  'outside cave

items(2).iName =  "apple"
items(2).states(1) = "not eaten"
items(2).states(2) = "eaten"
items(2).state = 1
items(2).location = 3  'outside cave

items(3).iName = "treasure"
items(3).states(1) = "open"
items(3).states(2) = "closed"
items(3).state = 1
items(3).location = 4  'in cave

type LOCATION
    as string description
    'direction holds the location number that taking that direction will lead to
    as integer direction(1 to 4)  'north, east, south, west POINTER INDEX to another location
    as integer itemPtr(1 to 4)        'ptr to items at this location 
end type

type AGENT
    as string  agentName   '
    as integer direction   'direction agent is looking
    as integer location    'location of agent
end type

dim shared as AGENT player
player.direction = 2  'east
player.location = 1   'at the lake



dim shared as LOCATION locations(1 to 4)

locations(3).itemPtr(1) = 1   'point to TROLL item
locations(3).itemPtr(2) = 2   'point to apple item
locations(4).itemPtr(1) = 3   'point to treasure item


locations(1).description = "NEXT TO A LAKE"
locations(2).description = "ON FOREST PATH"
locations(3).description = "OUTSIDE CAVE"
locations(4).description = "INSIDE CAVE"

'next to lake = location 1
locations(1).direction(1)= 0  'north blocked
locations(1).direction(2)= 2  'east to forest path
locations(1).direction(3)= 0  'south blocked
locations(1).direction(4)= 0  'west blocked

'on forest path = location 2
locations(2).direction(1)= 0
locations(2).direction(2)= 3  'east to outside cave
locations(2).direction(3)= 0
locations(2).direction(4)= 1  'west to next to lake


'outside cave = location 3
locations(3).direction(1)= 0
locations(3).direction(2)= 4  'east to inside cave
locations(3).direction(3)= 0
locations(3).direction(4)= 2  'west to forest path

'inside cave = location 4
locations(4).direction(1)= 0
locations(4).direction(2)= 0
locations(4).direction(3)= 0
locations(4).direction(4)= 3  'west to outside cave


dim as string action
do

    screenlock
    cls
    bload "locations.bmp"
    locate 280\8,1
    print
    color rgb(100,100,255)
    print "  YOU ARE ";locations(player.location).description
    color rgb(255,255,255)
    print
    print "    Enter one of these single word commands"
    print
    print "    +========+===================================+"
    print "    |COMMAND |   action taken                    |"
    print "    +========+===================================|"
    print "    | quit   | exits the program                 |"
    print "    +--------+-----------------------------------|"
    print "    | north  | moves player to another location  |"
    print "    +--------+-----------------------------------|"
    print "    | east   | moves player to another location  |"
    print "    +--------+-----------------------------------|"
    print "    | south  | moves player to another location  |"
    print "    +--------+-----------------------------------|"
    print "    | west   | moves player to another location  |"
    print "    +--------+-----------------------------------|"
    print "    | look   | print location description        |"
    print "    +--------+-----------------------------------+"
    print
    line ( (player.location-1)*240+10,0)-( (player.location-1)*240+10+217,180),rgb(255,0,0),b
    line ( (player.location-1)*240+10-1,0)-( (player.location-1)*240+10+217-1,180),rgb(255,0,0),b
    screenunlock

    print
    input "Type command:";sentence
    print
    cls

    getWords()

    action = words(0)  'only using first word from sentence
        
    'change direction
    if action ="north" then
        player.direction = 1
    end if
    if action ="east" then
        player.direction = 2
    end if
    if action ="south" then
        player.direction = 3
    end if
    if action ="west" then
        player.direction = 4
    end if

    'if move possible then change player's location
    if locations(player.location).direction(player.direction) <> 0 then  'can move
        player.location = locations(player.location).direction(player.direction) 'change location
    else
        color rgb(255,0,0)
        print
        print " DIRECTION BLOCKED"
    end if
    
    if action = "look" then
        print
        print " items at player's location"
        color rgb(100,255,100)
        for j as integer = 1 to 4
            if locations(player.location).itemPtr(j) <> 0 then
                print items(locations(player.location).itemPtr(j)).iName
            end if
        next j
        color rgb(255,255,255)
        print
    end if

    sleep 2
    
loop until action = "quit"
Image
Last edited by BasicCoder2 on Feb 06, 2021 20:58, edited 1 time in total.
grindstone
Posts: 862
Joined: May 05, 2015 5:35
Location: Germany

Re: making a text adventure game

Post by grindstone »

In my opinion it would make things easier if every location as well as the player (agent) had its own item array (and the SUBs to manage it):

Code: Select all

ScreenRes 960,500,32

Type ITEM
	As String iName      'key, chest, apple, gem, ...
	As Integer state
	As String states(1 To 2)
	As Integer location  'where the item resides
End Type

Dim Shared As ITEM items(1 To 2)
items(1).iName =  "TROLL"
items(1).states(1) = "alive"
items(1).states(2) = "asleep"
items(1).state = 1
items(1).location = 3  'outside cave

items(2).iName =  "apple"
items(2).states(1) = "not eaten"
items(2).states(2) = "eaten"
items(2).state = 1
items(2).location = 3  'outside cave

items(3).iName = "treasure"
items(3).states(1) = "open"
items(3).states(2) = "closed"
items(3).state = 1
items(3).location = 4  'in cave

Type LOCATION
	As String description
	'direction holds the location number that taking that direction will lead to
	As Integer direction(1 To 4)  'north, east, south, west POINTER INDEX to another location
	As ITEM itemList(Any)
	Declare Sub addItem(it As ITEM)
	Declare Sub delItem(i As Integer)
	Declare Sub printInventory()
End Type

Sub LOCATION.addItem(itm As ITEM)
	ReDim Preserve this.itemList(UBound(this.itemList) + 1) 'extend item list
	itemList(UBound(this.itemList)) = itm 'write item in item list
End Sub

Sub LOCATION.delItem(i As Integer)
	If i > UBound(this.itemList) Then Return
	For x As Integer = i To UBound(this.itemList) - 1
		itemList(x) = itemList(x + 1) 'shift items >i one place up
	Next
	ReDim Preserve this.itemList(UBound(this.itemList) - 1) 'delete highest index
End Sub

Sub LOCATION.printInventory()
	For x As Integer = LBound(this.itemList) To UBound(this.itemList)
		Print x; " "; this.itemList(x).iName
	Next
End Sub

Type AGENT
	As String  agentName   '
	As Integer direction   'direction agent is looking
	As Integer location    'location of agent
	As ITEM itemList(Any)
	Declare Sub addItem(it As ITEM)
	Declare Sub delItem(i As Integer)
	Declare Sub printInventory()
End Type

Sub AGENT.addItem(itm As ITEM)
	ReDim Preserve this.itemList(UBound(this.itemList) + 1) 'extend item list
	itemList(UBound(this.itemList)) = itm 'write item in item list
End Sub

Sub AGENT.delItem(i As Integer)
	If i > UBound(this.itemList) Then Return
	For x As Integer = i To UBound(this.itemList) - 1
		itemList(x) = itemList(x + 1) 'shift items >i one place up
	Next
	ReDim Preserve this.itemList(UBound(this.itemList) - 1) 'delete highest index
End Sub

Sub AGENT.printInventory()
	For x As Integer = LBound(this.itemList) To UBound(this.itemList)
		Print x; " "; this.itemList(x).iName
	Next
End Sub


'#####################
Dim As LOCATION l1
Dim As AGENT a1

Locate 1,1

'put the items in the location's item list
l1.addItem(items(1))
l1.addItem(items(2))
l1.addItem(items(3))

? "LOCATION:"
l1.printInventory
?
? "AGENT:"
a1.printInventory
?
? "-----------------------"
?
? "The AGENT takes the apple"
?
a1.addItem(l1.itemList(1)) 'put the apple to the agent
l1.delItem(1) 'delete the apple form the location

? "LOCATION:"
l1.printInventory
?
? "AGENT:"
a1.printInventory
?

?
? "OK"
Sleep

'#####################

BasicCoder2
Posts: 3917
Joined: Jan 01, 2009 7:03
Location: Australia

Re: making a text adventure game

Post by BasicCoder2 »

@grindstone,

Thanks for the suggestions. I had also forgotten how to use redim which I had intended to use as well so your example came in handy.
grindstone
Posts: 862
Joined: May 05, 2015 5:35
Location: Germany

Re: making a text adventure game

Post by grindstone »

Another 3 suggestions:

1) A list of actions that can be done with the items:

Code: Select all

Type ITEM
	As String iName      'key, chest, apple, gem, ...
	As Integer state
	As String states(1 To 2)
	As String action(Any)
	As Integer location  'where the item resides
End Type

Dim As ITEM i1

ReDim i1.action(1 To 6)
i1.action(1) = "take"
i1.action(2) = "drop"
i1.action(3) = "throw"
i1.action(4) = "eat"
i1.action(5) = "look"
i1.action(6) = "examine"
2) Accessing the objects by a name instead of an index would increase the readability and helps to keep the overview later on:

Code: Select all

Enum
	troll = 1
	apple
	treasure
End Enum

Enum
	NEXT_TO_A_LAKE = 1
	ON_FOREST_PATH
	OUTSIDE_CAVE
	INSIDE_CAVE
End Enum

Dim Shared As ITEM items(troll To treasure)
items(troll).iName =  "TROLL"
items(troll).states(1) = "alive"
items(troll).states(2) = "asleep"
items(troll).state = 1
items(troll).location = OUTSIDE_CAVE  'outside cave

items(apple).iName =  "apple"
items(apple).states(1) = "not eaten"
items(apple).states(2) = "eaten"
items(apple).state = 1
items(apple).location = OUTSIDE_CAVE  'outside cave

items(treasure).iName = "treasure"
items(treasure).states(1) = "open"
items(treasure).states(2) = "closed"
items(treasure).state = 1
items(treasure).location = INSIDE_CAVE  'in cave
3) The troll is rather a person than an item, and thus he is able to act by himself (e.g if you throw the apple against his head he will kick you in the shin). This behaviour can be embedded in the TYPE:

Code: Select all

Type tTROLL
	As String iName
	As Integer location  'where the person resides
	Declare Sub act(<parameters>)
End Type

Sub tTROLL.act(<parameters>)
	'code how the troll will behave in reaction to the actions of the agent 
End Sub
BasicCoder2
Posts: 3917
Joined: Jan 01, 2009 7:03
Location: Australia

Re: making a text adventure game

Post by BasicCoder2 »

@grindstone

Thank you for the suggestions and accompanying code examples.
I had thought about what you have suggested.

We have the real world as an extended data base. Indeed our internal data base is derived from that real world. The presence of a troll, apple or treasure can be "seen" when the player is at a particular location. For example in one of my simulated world programs the agents could scan for objects. That scan would return the position of any object. In some text adventure games I think similar techniques have been used.

Same fact recorded two ways (or both).

A chess pieces on a chess board might be represented as an 8x8 array of locations with any item (chess piece). Or you could have a list of all the chess pieces with a location.

A Black King at location 3,3 (list of items and their locations)
vs.
At location 3,3 is a Black King (array of locations and their contents)

Humans seem to use some kind of one directional associative memory.
It is one directional as shown if you try and recite the alphabet backwards Z,Y,X....A as quickly as reciting it forward A,B,C ....Z
If you ask someone to describe what is behind them (in a well know place like a room in a house) they might have trouble listing everything. However if you ask where is the torch or green vase they might quickly respond "of course, it is on the bench behind me". Thus we are more likely to remember where things are rather than what things contain. Of course we may also use inference and say "the flour is probably in the pantry" without actually remembering where it is.


Another 3 suggestions:

1) A list of actions that can be done with the items:

Or actions that can be done with a class of object.
We can record something about an item by knowing what class of items it belongs to rather than recording the same thing for every object in that class.

An apple is a fruit.
Fruit can be eaten.
A banana is a fruit.
Therefore a banana can be eaten. (implied not recorded as an action possible on the banana)

A troll is an agent.
An agent will react if hit.
The troll was hit.
The troll reacts. (implied not recorded as a troll action)

2) Accessing the objects by a name instead of an index would increase the readability and helps to keep the overview later on:

Indeed. I will try and implement that in future. Readability makes life so much easier when reading other people's code or your own at a later date.

3) The troll is rather a person than an item, and thus he is able to act by himself (e.g if you throw the apple against his head he will kick you in the shin). This behaviour can be embedded in the TYPE:

My thoughts are that the player and any non player characters belong to a class of objects starting at the top with maybe the class of living objects. When there are only a few items (objects) you might just implement each object in its own type structure but when there are many objects?

Bill has eyes
Jane has eyes
Harry has eyes
Susan has eyes

vs.

People have eyes.
Bill is a person thus Bill probably has eyes.

It is better to infer or deduce something rather than try and record every possibility for each item.
grindstone
Posts: 862
Joined: May 05, 2015 5:35
Location: Germany

Re: making a text adventure game

Post by grindstone »

BasicCoder2 wrote: Bill has eyes
Jane has eyes
Harry has eyes
Susan has eyes

vs.

People have eyes.
Bill is a person thus Bill probably has eyes.
Sounds like a clear case for inheritance. Alas, I never worked with inheritance, so I can't give any tips.

I don't know if it is handy for this adventure, but I consider it worth to be kept in mind: A TYPE can contain a (dynamic) array of itself. It took me some time to figure out how to make it accessible from both inside and outside the TYPE, but it works:

Code: Select all

Type ITEM
	As String iName      'key, chest, apple, gem, ...
	As Integer state
	As String states(1 To 2)
	As Integer location  'where the item resides
	Static As ITEM items(Any)
End Type

'static members must be DIMmed outside the TYPE
ReDim As ITEM ITEM.items(1 To 3)
...
BasicCoder2
Posts: 3917
Joined: Jan 01, 2009 7:03
Location: Australia

Re: making a text adventure game

Post by BasicCoder2 »

grindstone wrote:Sounds like a clear case for inheritance.
Yes something like that. But I am getting ahead of myself here. I just see it as a mental challenge to think about how it might be done. In the meantime I am thinking about how to code a simple text based game using FreeBasic and writing experimental code like the piece below that uses functions to add locations and its connections and items using subroutines. There is also a routine, printLocations() to describe the locations.

Code: Select all

screenres 900,500,32
color rgb(0,0,0),rgb(255,255,255):cls

Enum
   troll = 0
   apple
   treasure
End Enum

Enum
   NEXT_TO_A_LAKE = 0
   ON_FOREST_PATH
   OUTSIDE_CAVE
   INSIDE_CAVE
End Enum

Type ITEM
   As String iName      'key, chest, apple, gem, ...
   As Integer state
   As String states(1 To 2)
   As String action(Any)
   As Integer location  'where the item resides
End Type

Type LOCATION
   As String description
   as integer connection(any)  'location id that this path connects to
   as ITEM items(any)
End Type

dim shared as LOCATION locs(any)

Sub addLocation(tag as string)
   ReDim Preserve locs(UBound(locs) + 1) 'extend item list
   locs(UBound(locs)).description = tag
End Sub

Sub addConnection(fromLoc as integer, toLoc as integer)
   ReDim Preserve (locs(fromLoc).connection)(UBound(locs(fromLoc).connection) + 1)
   locs(fromLoc).connection(UBound(locs(fromLoc).connection)) = toLoc
End Sub

Sub addItems(atLoc as integer, item as string)
   ReDim Preserve (locs(atLoc).items)(UBound(locs(atLoc).items) + 1)
   locs(atLoc).items(UBound(locs(atLoc).items)).iName = item
End Sub

Sub printLocations()
   For x As Integer = LBound(locs) To UBound(locs)
      Print locs(x).description
      'print items at location
      For y As Integer = LBound(locs(x).items) To UBound(locs(x).items)
          print "where you will find ";locs(x).items(y).iName
      next y      
      'print connections
      For y As Integer = LBound(locs(x).connection) To UBound(locs(x).connection)
          print "it connects to ";locs(locs(x).connection(y)).description
      next y
      print
   Next
End Sub

'add locations to data base
addLocation("next to lake")
addLocation("on forest path")
addLocation("outside cave")
addLocation("inside cave")

'add location's connections
addConnection(NEXT_TO_A_LAKE,ON_FOREST_PATH)
addConnection(ON_FOREST_PATH,OUTSIDE_CAVE)
addConnection(OUTSIDE_CAVE,INSIDE_CAVE)
addConnection(ON_FOREST_PATH,NEXT_TO_A_LAKE)
addConnection(OUTSIDE_CAVE,ON_FOREST_PATH)
addConnection(INSIDE_CAVE,OUTSIDE_CAVE)

'add some items to locations
addItems(NEXT_TO_A_LAKE,"player")
addItems(OUTSIDE_CAVE,"troll")
addItems(OUTSIDE_CAVE,"apple")
addItems(INSIDE_CAVE,"treasure")

printLocations()

print


sleep

Post Reply