Text adventures & state machines (a newb approaches!)

General discussion about LÖVE, Lua, game development, puns, and unicorns.
User avatar
a neobum
Prole
Posts: 5
Joined: Fri Mar 30, 2012 6:46 pm

Text adventures & state machines (a newb approaches!)

Post by a neobum »

Hi, guys!

I should start by saying that until a week ago I'd never written a line of code in my life. But my head is bursting with game ideas and after some searching I stumbled across Lua and Löve and it seemed the best of possible places to start. I quickly fell in löve (I'm sorry but these damn puns just write themselves!) and, after following a few tutorials and doing some reading on the wiki, I started trying to put together something of my own. Here's what I'm after:

The long-distance goal is a game that incorporates dialogue trees and so I figured the best way of learning about these would be crafting together their essential skeleton: a text-based adventure. I soon realized that this requires the implementation of state machines and here's where I've gotten stuck. I read chapter 6.3 on Proper Tail Calls in Roberto Ierusalimschy book and thought my problems solved with the return command but I was wrong. The solution seems banally simple but I just can't figure out the right terminology.

So, would somebody please be kind enough to explain to me (or redirect me to a place where this is explained, should I have missed that during my forum searches) how I go about putting together the following:

(it should perhaps be underlined that the practical content of the below is completely arbitrary; I'm only after figuring out the mechanics of how exactly one implements it)
  • on startup there should be a greeting prompting you to press space
  • having pressed space there should pop up a new prompt, telling you to press either 1 or 2
  • if you press 1 you should get output1
  • if you press 2 you should get output2
Simple enough? I thought so and wrote ten million variations of this:

Code: Select all

function love.load()
	love.graphics.setFont(12)
	greeting = "Hello Wörld (Press SPACE to proceed.)"
	
	options1 =	""
	options2 =	"Are you impressed? (Press ONE for Yes, TWO for No.)"
	output0 =	""
	output1 =	"I know, right? It's amazing what a neobum can do!"
	output2 =	"Why would you sit there and lie like that to yourself?"
end

function love.update(dt)
end

function love.keyreleased(key)
	if	key == " " then
		options1 = options2
	end
	
	if		key == "1" then
			output0 = output1
	elseif	key == "2" then
			output0 = output2
	end
	
	if	key == "return" then
		love.load()
	end

end

function love.draw()
	love.graphics.setColor(0, 255, 255, 255)
	love.graphics.print(greeting, 50, 50)
	love.graphics.print(options1, 60, 80)
	love.graphics.print(output0, 70, 110)
end
In case it's not glaringly obvious, I'll elaborate on what's wrong with the above:

The problem is that all three keys (spacebar, 1 and 2 respectively) are constantly listened for and will give their respective outputs regardless of when they are pressed. I need the spacebar to be a prerequisite for the other two options and once one of them is selected the other should no longer be available to press. As I say, I know this needs to be done by setting various states and calling various functions. Instinctively it's so straight forward. In theory I can see the whole thing set up in front of me but I just can't figure out how to phrase the damned thing!

Somebody please help me! I've been running around in circles for days and it's long since stopped being funny (well, all right, it is a little bit funny). If this is all comprehensively covered in another thread then I apologize for blundering in here and wasting your time. If it isn't... how about we comprehensively cover it here?
User avatar
Robin
The Omniscient
Posts: 6506
Joined: Fri Feb 20, 2009 4:29 pm
Location: The Netherlands
Contact:

Re: Text adventures & state machines (a newb approaches!)

Post by Robin »

Okay. Where you stand, you don't have a state machine at all. So this is what we're going to do:

We write a piece of code for each state:

Code: Select all

state_something = {}

function state_something.update(dt)
end

function state_something.keypressed(key)
end

function state_something.draw()
end
(add more callbacks as needed)
Each function only contains code for that state.

Code: Select all

function love.load()
    state = whatever_start_state
end

function love.update(dt)
    state.update(dt)
end

function love.draw()
   state.draw()
end

--repeat for all needed callbacks
When you want to change the state, you do state = next_state_name.

Did that help you or do you want me to elaborate?
Help us help you: attach a .love.
User avatar
a neobum
Prole
Posts: 5
Joined: Fri Mar 30, 2012 6:46 pm

Post by a neobum »

I realize full well that I didn't have a state machine but I didn't even have an inkling as to how I should make one. Since my original post I had an acquaintance write me up the program I described but I've had no end of trouble trying to reverse-engineer it. At first glance your post seems to have given me exactly what I was looking for. Thanks a bunch.

I'll do some tinkering and see what I came up with. I'll probably come back here and ask a bunch of stupid questions about what different terms mean but, for now, let's see if I can't make a state machine of my own.
User avatar
Inny
Party member
Posts: 652
Joined: Fri Jan 30, 2009 3:41 am
Location: New York

Re: Text adventures & state machines (a newb approaches!)

Post by Inny »

Warning: This post may blow your mind. Robin's answer is probably better than mine.

I mentioned in the "Coroutine Are Awesome" thread that you can implement the text adventure from Programming In Lua in love using coroutines. Actually, if you overwrite love.run, you don't even need coroutines. I'll show both forms in minimal example:

Code: Select all

-- Instead of io.read, we go with our own function that works with love.
function my_read()
  local textline = ""
  while true do
    local e = love.event.wait()
    -- do some keyboard handling to take in input
    update_screen()
  end
  return textline
end

function update_screen()
  love.graphics.clear()
  -- draw all of the on screen stuff
  love.graphics.present()
end

function room1 ()
  local move = my_read() -- this is the line that changed.
  if move == "south" then return room3()
  elseif move == "east" then return room2()
  else print("invalid move")
     return room1()   -- stay in the same room
  end
end
    
--[[ the rest of the text adventure part is clipped for brevity, but you get the idea ]]
    
function love.run()
  room1()
end
That way of doing things isn't very flexible however, because lets say you decide to add a visible screen and want to animate things, or have various game modes, etc., or decide that it shouldn't be a text adventure at all, it should be a text-based adventure game like Space Quest, then you may have a harder time dealing with this.

The coroutine alternative looks like so:

Code: Select all

-- use that same text adventure code from above, with io.read replaced with my_read

main = coroutine.wrap( room1 )

function my_read()
  while true do
    local lastline = coroutine.yield(true)
    if lastline then return lastline end
  end
end

function love.update(dt)
  -- update any animations with the dt var
end

function love.draw()
  -- draw all of the text
end

currentline = ""

function love.keypressed( k, u )
  -- do some text input stuff here
  if k == "backspace" then
    currentline = currentline:sub(0, #currentline-1)
  elseif k == "return" then
    main( currentline )
  elseif u >= 32 and u < 127 then
    currentline = currentline + string.char(u)
  end
end
The benefit of doing it this style is you can build a game like it's 1988 and all you have is Turbo Pascal on a ibm cheapo, but still have love's modern event loop running underneath.

If any of this confused you, then go with Robin's solution.
User avatar
trubblegum
Party member
Posts: 192
Joined: Wed Feb 22, 2012 10:40 pm

Re: Text adventures & state machines (a newb approaches!)

Post by trubblegum »

Hell, that confused me .. I think from now on, I might always feel a little bit bad if I don't call setPixel() from a coroutine .. but I'm not sure :cry:

PS : if you're new to coding, you might like to know that you should put your states in something called a "table", and that each state can contain a table of references to other states

Code: Select all

states = {}
states.new = function(text)
  local state = {}
  state.text = text
  state.options = {}
  return state
end

love.load = function()
  states.room1 = states.new('a room')
  states.room2 = states.new('another room')
  
  states.room1.options[1] = states.room2
  states.room2.options[1] = states.room1
  
  states.current = states.room1
end

love.keypressed = function(key, code)
  if states.current.options[key] then
    states.current = states.current.options[key]
  end
end

love.draw = function()
  love.graphics.print('You find yourself in ' .. states.current.text)
  for i, option in ipairs(states.current.options) do
    love.graphics.print(i .. ' : go to ' .. option.text)
  end
end
It is the year 2000, and small bands of love.update(dt) struggle to survive in the harsh technological environment of the twenty-first century ...
coffee
Party member
Posts: 1206
Joined: Wed Nov 02, 2011 9:07 pm

Re: Text adventures & state machines (a newb approaches!)

Post by coffee »

A very basic hypertext solution/approach

Code: Select all

function love.load()
	rooms = {
		green	= { desc = "You are in a Green room", options = {"red", "An exit to a Red room", "blue", "An exit to a Blue Room" }},
		red		= { desc = "You are in a Red room", options = {"blue", "An exit to a Blue room", "green", "An exit to a Green Room"}},
		blue	= { desc = "You are in a Blue room", options = {"red", "An exit to a Red room", "green", "An exit to a Green Room","yellow","An exit to an Yellow Room" }},
		yellow	= { desc = "You are in an Yellow room", options = {"blue", "An exit to a Blue Room" }}
}
room = "red"
end

function love.update()
	function love.keypressed(key, unicode)
		for z=1,(#rooms[room].options)/2 do
			if key == tostring(z) then
				room = rooms[room].options[(z*2)-1]
			end	
		end
	end
end

function love.draw()
		love.graphics.print(rooms[room].desc..". You can go to:",0,20) 
		for i=1, (#rooms[room].options)/2 do
			love.graphics.print(i.." - "..rooms[room].options[i*2],0,20+i*20) 
		end
end

Sorry if any problem there unnoticed. Also obvious this could be optimized. It's clumsy. I skipped the "space" thing.
EDITED: Added an extra room because all rooms had always 2 exits. Now it shows the dynamic key menu working.
User avatar
trubblegum
Party member
Posts: 192
Joined: Wed Feb 22, 2012 10:40 pm

Re: Text adventures & state machines (a newb approaches!)

Post by trubblegum »

Please don't redefine love.keypressed every update.
User avatar
a neobum
Prole
Posts: 5
Joined: Fri Mar 30, 2012 6:46 pm

Post by a neobum »

Thanks a bunch, guys. You people are the best. But I'm beginning to fear that I might be a lost cause. I wonder what you guys would make of the following code:

Code: Select all

function love.load()
	love.graphics.setFont(12)
	
	StateManager = {}
	
	function StateManager:processKey(key)
		local returnState = self.state.actionList[key]
		if	(returnState ~= nil) then
			self.state = returnState
		end
	end
	
	function StateManager:show()
		return self.state.message
	end
	
	function StateManager.init(state)
		local stateManager = { state = state, processKey = StateManager.processKey, show = StateManager.show }
		return stateManager
	end
	
	
	
	State = {}
	
	function State.init(message, actionList)
		local state = { message = message, actionList = actionList }
		return state
	end
	
	state1 = State.init("Hello Wörld! \n\n (Press SPACE to proceed.)", nil)
	
	state2 = State.init("Are you impressed? \n\n\n1. YES \n\n2. NO", nil)
	
	state3 = State.init("Good. You should be. After all, neobums rock! \n\n (Press ENTER to restart)", nil)
	
	state4 = State.init("Saw through it did you? Yeah, Drav wrote this shit and I just copied it. \nI don't even know what half this code does! \n\n (Press ENTER to restart)", nil)
	
	
	state1.actionList = { [" "] = state2 }
	
	state2.actionList = { ["1"] = state3, ["2"] = state4 }
	
	state3.actionList = { ["return"] = state1 }
	state4.actionList = { ["return"] = state1 }
	
	StateManager = StateManager.init(state1)

	end

function love.keyreleased(key)
	StateManager:processKey(key)
end

function love.draw()
	love.graphics.setColor(0, 255, 255, 255)
	love.graphics.print(StateManager:show(), 100, 100)
end
This is the thing my friend wrote and I've been desperately trying to wrap my head around what exactly is happening here but I just can't. I realize I could just use it (or any of your codes) to make a simple, little game but the point of this thread is that I want to learn how to do this stuff from scratch. If the above code is unnecessarily convoluted let's just pretend I never posted it but would somebody care to actually walk me through each and every step that needs to happen to achieve a program that does what I specified in my original post?
User avatar
kraftman
Party member
Posts: 277
Joined: Sat May 14, 2011 10:18 am

Re: Text adventures & state machines (a newb approaches!)

Post by kraftman »

If it helps, it took me a while to get my head around that code, it does look a little convoluted.
The State table doesn't really need to be there, it just adds the message and the options into a table for you.
statemanager just handles the key presses and returns the text.
User avatar
a neobum
Prole
Posts: 5
Joined: Fri Mar 30, 2012 6:46 pm

Post by a neobum »

That much I gathered and I was told that it's far from a smooth implementation. But I was hoping somebody would walk me through the process; function by function and explain what's doing what to what, in what order, why things are where they are and so on. Yes, folks, I'm really that thick.
Post Reply

Who is online

Users browsing this forum: No registered users and 250 guests