Announcement

Collapse

Forum Guidelines

This forum is for finished source code that is working properly. If you have questions about this or any other source code, please post it in one of the Discussion Forums, not here.
See more
See less

Text Adventure Demo using Finite State Machine

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

  • Text Adventure Demo using Finite State Machine

    This example really pushes the envelope and does things that are frowned on within certain circles.
    It is posted here to show some of the Power available with the new PowerObjects. It would be of interest to anyone creating text based adventure games and those who want to learn how to implement FMS's(see below) with PowerBasic 5/9.
    Note to [email protected]. I have permission from the author to use his code. I will forward the email if requested.
    I am not seasoned OOP programmer by any means but this code seems stable.

    James


    Back ground:
    I started investigating FSM's (Finite State Machines) as they are used in RPG game development and came across a number of interesting articles. The one that really caught my eye was an exert from a book written by Mat Buckland (url in the code). By using his methods outlined you eliminate the monster if / then | select / case normally used in these games. He writes of course in C++ and it took some magic to finally come up with a way to implement in PBCC 5.
    '******************************************************************************
    Problem:
    There is no documented way to pass an OBJECTPTR to a Function/Sub/Method AS A BYVAL DWORD.
    ******************************************************************************
    Magic Part #1
    Example:
    This does not work:
    Code:
    METHOD M1(BYVAL MyobjPtr AS DWORD) 
       LOCAL oMyObj AS iMyObj
       oMyObj = MyObjPtr
    You would need to this:
    Code:
    METHOD M1(BYVAL Myobj AS iMyObj ) 
       LOCAL oMyObj AS iMyObj
       oMyObj = MyObj
    But what if we really need to pass a DWORD
    Here is how this is done but be very careful
    Code:
    METHOD M1(BYVAL MyobjPtr AS DWORD) 
       LOCAL oMyObj AS iMyObj
       POKE DWORD,VARPTR(oMyObj),MyObjPtr)
       IF ISNOTHING(oMyObj) THEN EXIT METHOD
       oMyObj.AddRef
    Explanation:

    The MyObjPtr is an iMyObj Object created outside the METHOD
    There is no assignment for oMyObj (we poke it). When the METHOD Ends
    it is noted by the compiler that the oMyObj variable contains a valid object reference
    so it Calls the COM Release Method to deallocate all the internal stuff.
    woops we just NOTHING'ed our original passed in OBJECT so off to GPF land.

    To counteract this we use the (PB undocumented - although well known COM call) AddRef
    oMyObj.AddRef

    Magic Part #2.
    I needed a way to fool the compiler into thinking it was dealing with just one type of
    Class/Interface while in reality there were as many different class/interface as there
    were locations.
    Solution:
    I use what is called in C++ as a Virtual Class. It has no functionality and is used strictly
    as a base class shown below
    Code:
    CLASS cState
        INTERFACE iState : INHERIT IUNKNOWN
            METHOD Enter(BYVAL Agent AS tAgent)
            END METHOD
            METHOD Execute(BYVAL Agent AS tAgent)
            END METHOD
            METHOD EXIT(BYVAL Agent AS tAgent)
            END METHOD
        END INTERFACE
    END CLASS
    All Location classes inherit from this class. They then flesh out the three methods Enter,

    Execute, Exit.

    We have two Control states Called oNewState, oCurrentState also derived from cState/iState.
    We then use the same method describe abouve to POKE the OBJPTR's of the locations to oNewState

    Code:
    'SED_PBCC
    '******************************************************************************
    '               A Finite State Machine example written in PBCC 5.0
    '                                James C. Fuller
    '                                August 11,2008
    '                             jcf[email protected]
    '
    '******************************************************************************
    '==============================================================================
    'Based on WestWorld c++ example by  Mat Buckland 2002 ([email protected])
    '------------------------------------------------------------------------------
    '##############################################################################
    'If you are unfamilar with FSM's here are a few links:
    '******************************************************************************
    ' This one is where I got the code my example is based.
    ' http://www.ai-junkie.com/architecture/state_driven/tut_state1.html
    '******************************************************************************
    '------------------------------------------------------------------------------
    ' http://aigamedev.com/questions/fsm-implementation
    '------------------------------------------------------------------------------
    'http://klabs.org/DEI/References/design_guidelines/nasa_guidelines/fsm/finite_state_machines.htm
    '------------------------------------------------------------------------------
    'http://www.rwc.uc.edu/koehler/comath/34.html
    '------------------------------------------------------------------------------
    'http://centipedia.com/articles/Finite_state_automaton
    '------------------------------------------------------------------------------
    'http://www.aiwisdom.com/bytopic_statemachines.html
    '##############################################################################
    'I'll try to explain how this works.
    'I use a completely Virtual Base Class (cState/iState) that is inherited by all
    'the State Classes so I can fool the Update Method into thinking it's dealing
    'with a single class. We don't have Pointers that can be used and instantiated so
    ' this approach seems to work fine?
    '##############################################################################
    'Each location is a class composed of three state classes(Enter,Execute,Exit)
    ' all inherited from cState/iState
    '******************************************************************************
    'Note TRACE is enabled specifically to check  for AddRef problems. The areas where
    'it is used here should always print 2. The origianl and the one we add. When the
    'local scope ends the compiler reduces the count by one; exactly what we want.
    
    '******************************************************************************
    #COMPILE EXE
    #DIM ALL
    %shack = 1
    %goldmine = 2
    %bank = 3
    %saloon = 4
    $AgentNames="Miner Bob;Elsa"
    MACRO FUNCTION GetAgentName(Id)
    END MACRO = PARSE$($AgentNames,";",Id)
    MACRO MyTrace(MyStr)
    	TRACE ON
    	TRACE PRINT MyStr
    	TRACE OFF
    END MACRO
    %AGENT_MINERBOB = 1
    TYPE tAgent
    	AgentObj	AS DWORD			'Holds OBJPTR(ME)
    	AgentType	AS LONG
    END TYPE
    '==============================================================================
    CLASS cBaseGameEntity
        INSTANCE m_id,m_NextValidId AS LONG
        INTERFACE iBaseGameEntiy :INHERIT IUNKNOWN
            PROPERTY GET m_id() AS LONG
                PROPERTY = m_id
            END PROPERTY
            PROPERTY SET m_id(BYVAL Param AS LONG)
                 m_id = Param
            END PROPERTY
        END INTERFACE
    END CLASS
    '==============================================================================
    CLASS cMiner
        INSTANCE Location,GoldCarried,MoneyInBank,Thirst,Fatigue AS LONG
        INSTANCE ComfortLevel,MaxNuggets,ThirstLevel,TirednessThreshold AS LONG
        INSTANCE oCurrentState AS iState
        INSTANCE oNewState AS iState
    	INSTANCE oGoHomeAndSleepTilRested AS iGoHomeAndSleepTilRested
    	INSTANCE Agent AS tAgent
    '------------------------------------------------------------------------------
        CLASS METHOD CREATE
            ComfortLevel = 5
            MaxNuggets = 3
            ThirstLevel = 5
            TirednessThreshold = 5
            Location = %shack
    		Agent.AgentType = %AGENT_MINERBOB
        END METHOD
    '------------------------------------------------------------------------------
        INTERFACE iMiner : INHERIT cBaseGameEntity, iBaseGameEntiy
    		PROPERTY GET Agent() AS tAgent
    			PROPERTY = Agent
    		END PROPERTY
            PROPERTY GET Location() AS LONG
                PROPERTY = Location
            END PROPERTY
    '..............................................................................
            PROPERTY GET GoldCarried() AS LONG
                PROPERTY = GoldCarried
            END PROPERTY
            PROPERTY SET GoldCarried(BYVAL Param AS LONG)
                GoldCarried = Param
            END PROPERTY
            PROPERTY GET ComfortLevel() AS LONG
                PROPERTY = ComfortLevel
            END PROPERTY
            PROPERTY SET oNewState(BYVAL dwNewState AS DWORD)
    		    POKE DWORD,VARPTR(oNewState),dwNewState
                oNewState.AddRef
            END PROPERTY
    '..............................................................................
            METHOD ChangeLocation(BYVAL NewLocation AS LONG)
                Location = NewLocation
            END METHOD
    '------------------------------------------------------------------------------
    'This is where the states are changed
    '------------------------------------------------------------------------------
            METHOD UpDate()AS LONG
    			LOCAL RefC AS LONG ,TrStr AS STRING
                Thirst +=1
    'This will be the first time UpDate is Called
    ' Create the Start Location -> oGoHomeAndSleepTilRested
    			IF Agent.AgentObj = 0 THEN		'First Time
    				Agent.AgentObj = OBJPTR(ME)
    				oGoHomeAndSleepTilRested = CLASS "cGoHomeAndSleepTilRested"
    'We can't just do an  assignment because they are not the same interface but
    'we can do a little trick that works fine
    				POKE DWORD,VARPTR(oNewState),OBJPTR(oGoHomeAndSleepTilRested)
    'This is undocumented but works fine. just make sure either out of scope or = NOTHIMG's
    'match the AddRef. For debugging you can check the Count which is returned from AddRef.
    				RefC = oNewState.AddRef
    				TrStr = "oNewState RefC in UpDate = " + FORMAT$(Refc)
    			END IF
    			IF ISOBJECT(oNewState) THEN
    				'No oCurrentState if First Update
    				IF ISOBJECT(oCurrentState) THEN
    					oCurrentState.Exit(Agent)
    					oCurrentState = NOTHING
    				END IF
    				oCurrentState = oNewState
    				oNewState = NOTHING
    				oCurrentState.Enter(Agent)
    
    			END IF
    
                IF ISOBJECT(oCurrentState) THEN
                    oCurrentState.Execute(Agent)
                END IF
    
            END METHOD
    '------------------------------------------------------------------------------
            METHOD AddToGoldCarried(BYVAL Amount AS LONG)
                GoldCarried += Amount
            END METHOD
    '------------------------------------------------------------------------------
            METHOD IncreaseFatigue()
                Fatigue += 1
            END METHOD
    '------------------------------------------------------------------------------
            METHOD DecreaseFatigue()
                Fatigue -= 1
            END METHOD
    '------------------------------------------------------------------------------
            METHOD PocketsFull() AS LONG
                METHOD = (GoldCarried >= MaxNuggets)
            END METHOD
    '------------------------------------------------------------------------------
            METHOD Thirsty() AS LONG
                METHOD = (Thirst >= ThirstLevel)
            END METHOD
    '------------------------------------------------------------------------------
            METHOD Wealth() AS LONG
                METHOD = MoneyInBank
            END METHOD
    '------------------------------------------------------------------------------
            METHOD SetWealth(BYVAL Param AS LONG)
                MoneyInBank = Param
            END METHOD
    '------------------------------------------------------------------------------
            METHOD AddToWealth(BYVAL Param AS LONG)
                MoneyInBank += Param
                IF MoneyInBank < 0 THEN
                    MoneyInBank = 0
                END IF
            END METHOD
    '------------------------------------------------------------------------------
            METHOD BuyAndDrinkAWhiskey()
                Thirst = 0
                MoneyInBank -= 2
            END METHOD
    '------------------------------------------------------------------------------
            METHOD Fatigued() AS LONG
                METHOD = (Fatigue > TirednessThreshold)
            END METHOD
    '------------------------------------------------------------------------------
        END INTERFACE
    END CLASS
    '=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*
    'Virtual State Class
    'This is the Class/Interface that is the base for all Locations and in C++
    'terms in is a Virtual Class with no functionality on it's own.
    '=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*
    CLASS cState
        INTERFACE iState : INHERIT IUNKNOWN
            METHOD Enter(BYVAL Agent AS tAgent)
            END METHOD
            METHOD Execute(BYVAL Agent AS tAgent)
            END METHOD
            METHOD EXIT(BYVAL Agent AS tAgent)
            END METHOD
        END INTERFACE
    END CLASS
    '=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*
    CLASS cEnterMineAndDigForNugget
    	INTERFACE iEnterMineAndDigForNugget : INHERIT cState,iState
    
    '------------------------------------------------------------------------------
            OVERRIDE METHOD Enter(BYVAL Agent AS tAgent)
    			LOCAL oMiner AS iMiner
    			LOCAL RefC AS LONG ,TrStr AS STRING
    			IF Agent.AgentType = %AGENT_MINERBOB THEN
    				POKE DWORD,VARPTR(oMiner),Agent.AgentObj
    				IF ISNOTHING(oMiner) THEN
    					EXIT METHOD
    				END IF
    				RefC = oMiner.AddRef
    				TrStr = "oMiner RefC in cEnterMineAndDigForNugget Enter = " + FORMAT$(RefC)
    				MyTrace(TrStr)
    				IF oMiner.Location <> %goldmine THEN
    					PRINT GetAgentName(1); ": Walking to the Glodmine"
    					oMiner.ChangeLocation(%goldmine)
    				END IF
    			'ELSEIF	Agent.AgentType = Something Else
    			END IF
            END METHOD
    '------------------------------------------------------------------------------
            OVERRIDE METHOD Execute(BYVAL Agent AS tAgent)
    			LOCAL Id AS LONG
    			LOCAL oMiner AS iMiner
    			LOCAL oVisitBankAndDepositGold AS iVisitBankAndDepositGold
    			LOCAL oQuenchThirst AS iQuenchThirst
    			LOCAL RefC AS LONG ,TrStr AS STRING
    			IF Agent.AgentType = %AGENT_MINERBOB THEN
    				POKE DWORD,VARPTR(oMiner),Agent.AgentObj
    				IF ISNOTHING(oMiner) THEN
    					EXIT METHOD
    				END IF
    				ReFc=oMiner.AddRef
    				TrStr = "oMiner RefC in cEnterMineAndDigForNugget Execute = " + FORMAT$(RefC)
    				MyTrace(TrStr)
    
    				Id = oMiner.m_id
    				oMiner.AddToGoldCarried(1)
    				oMiner.IncreaseFatigue()
    				PRINT GetAgentName(Id); ": Picking Up a Nugget"
    				IF oMiner.PocketsFull() THEN
    					oVisitBankAndDepositGold = CLASS "cVisitBankAndDepositGold"
    					IF ISNOTHING(oVisitBankAndDepositGold) THEN
    						EXIT METHOD
    					END IF
    					oVisitBankAndDepositGold.AddRef
    					oMiner.oNewState = OBJPTR(oVisitBankAndDepositGold)
    					PRINT GetAgentName(Id); ": Pockets Full"
    				END IF
    
    				IF oMiner.Thirsty THEN
    					oQuenchThirst = CLASS "cQuenchThirst"
    					oQuenchThirst.Addref
    					oMiner.oNewState = (OBJPTR(oQuenchThirst))
    				END IF
    			END IF
            END METHOD
    '------------------------------------------------------------------------------
            OVERRIDE METHOD Exit(BYVAL Agent AS tAgent)
    			LOCAL oMiner AS iMiner
    			LOCAL Id AS LONG
    			LOCAL RefC AS LONG,TrStr AS STRING
    			IF Agent.AgentType = %AGENT_MINERBOB THEN
    				POKE DWORD,VARPTR(oMiner),Agent.AgentObj
    				Id = oMiner.m_id
    				RefC=oMiner.AddRef
    				TrStr = "oMiner RefC in cEnterMineAndDigForNugget Exit = " + FORMAT$(RefC)
    				MyTrace(TrStr)
    
    				PRINT GetAgentName(Id); ": Ah'm leavin' the goldmine with mah pockets full o' sweet gold"
    			END IF
            END METHOD
        END INTERFACE
    END CLASS
    '******************************************************************************
    CLASS cVisitBankAndDepositGold
        INSTANCE oGoHomeAndSleepTilRested AS iGoHomeAndSleepTilRested
        INSTANCE oEnterMineAndDigForNugget AS iEnterMineAndDigForNugget
    	INSTANCE Id AS LONG
        INTERFACE iVisitBankAndDepositGold : INHERIT cState,iState
            OVERRIDE METHOD Enter(BYVAL Agent AS tAgent)
    			LOCAL oMiner AS iMiner
    			IF Agent.AgentType = %AGENT_MINERBOB THEN
    				POKE DWORD,VARPTR(oMiner),Agent.AgentObj
    				oMiner.AddRef
    				IF oMiner.Location <> %Bank THEN
    					PRINT GetAgentName(1); ": Walking to the Bank"
    					oMiner.ChangeLocation(%Bank)
    				END IF
    			END IF
            END METHOD
    '------------------------------------------------------------------------------
            OVERRIDE METHOD Execute(BYVAL Agent AS tAgent)
    			LOCAL oMiner AS iMiner
                IF Agent.AgentType = %AGENT_MINERBOB THEN
    				POKE DWORD,VARPTR(oMiner),Agent.AgentObj
    				oMiner.AddRef
    				oMiner.AddToWealth(oMiner.GoldCarried)
    				oMiner.GoldCarried = 0
    				PRINT GetAgentName(1); ": Depositing gold. Total savings now: ";oMiner.Wealth
    				IF oMiner.Wealth >= oMiner.ComfortLevel THEN
    					PRINT GetAgentName(1); ": WooHoo! Rich enough for now. Back home to mah li'lle lady"
    					oGoHomeAndSleepTilRested = CLASS "cGoHomeAndSleepTilRested"
    					oMiner.oNewState = OBJPTR(oGoHomeAndSleepTilRested)
    				ELSE
    					oEnterMineAndDigForNugget = CLASS "cEnterMineAndDigForNugget"
    					oMiner.oNewState = OBJPTR(oEnterMineAndDigForNugget)
    				END IF
    			END IF
            END METHOD
    '------------------------------------------------------------------------------
            OVERRIDE METHOD Exit(BYVAL Agent AS tAgent)
                PRINT GetAgentName(1); ": Leaving The Bank"
            END METHOD
    '------------------------------------------------------------------------------
        END INTERFACE
    END CLASS
    '=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*
    CLASS cGoHomeAndSleepTilRested
    	INSTANCE oEnterMineAndDigForNugget AS iEnterMineAndDigForNugget
    	INSTANCE Id AS LONG
        INTERFACE iGoHomeAndSleepTilRested : INHERIT cState,iState
            OVERRIDE METHOD Enter(BYVAL Agent AS tAgent)
    			LOCAL oMiner AS iMiner
                IF Agent.AgentType = %AGENT_MINERBOB THEN
    				POKE DWORD,VARPTR(oMiner),Agent.AgentObj
    				oMiner.AddRef
    				IF oMiner.Location <> %Shack THEN
    					PRINT GetAgentName(1); ": Walking to the Shack"
    					oMiner.ChangeLocation(%Shack)
    				END IF
    			END IF
            END METHOD
    '------------------------------------------------------------------------------
            OVERRIDE METHOD Execute(BYVAL Agent AS tAgent)
    			LOCAL oMiner AS iMiner
                IF Agent.AgentType = %AGENT_MINERBOB THEN
    				POKE DWORD,VARPTR(oMiner),Agent.AgentObj
    				oMiner.AddRef
    				IF ISFALSE(oMiner.Fatigued) THEN
    					PRINT GetAgentName(1); ": What a Gol darn fantastic nap! Time to find more gold"
    					oEnterMineAndDigForNugget = CLASS "cEnterMineAndDigForNugget"
    					oMiner.oNewState = OBJPTR(oEnterMineAndDigForNugget)
    				ELSE
    					oMiner.DecreaseFatigue
    					PRINT GetAgentName(1); ": ZZZZ....."
    				END IF
    			END IF
            END METHOD
    '------------------------------------------------------------------------------
            OVERRIDE METHOD Exit(BYVAL Agent AS tAgent)
                PRINT GetAgentName(1); ": Leaving the House"
            END METHOD
    '------------------------------------------------------------------------------
        END INTERFACE
    END CLASS
    '=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*
    CLASS cQuenchThirst
    	INSTANCE oEnterMineAndDigForNugget AS iEnterMineAndDigForNugget
        INTERFACE iQuenchThirst : INHERIT cState,iState
            OVERRIDE METHOD Enter(BYVAL Agent AS tAgent)
    			LOCAL oMiner AS iMiner
                IF Agent.AgentType = %AGENT_MINERBOB THEN
    				POKE DWORD,VARPTR(oMiner),Agent.AgentObj
    				oMiner.AddRef
    				IF oMiner.Location <> %Saloon THEN
    					oMiner.ChangeLocation(%Saloon)
    					PRINT GetAgentName(1); ": Boy, ah sure is thusty! Walking to the saloon"
    				END IF
    			END IF
            END METHOD
    '------------------------------------------------------------------------------
            OVERRIDE METHOD Execute(BYVAL Agent AS tAgent)
    			LOCAL oMiner AS iMiner
                IF Agent.AgentType = %AGENT_MINERBOB THEN
    				POKE DWORD,VARPTR(oMiner),Agent.AgentObj
    				oMiner.AddRef
    				IF oMiner.Thirsty THEN
    					oMiner.BuyAndDrinkAWhiskey
    					PRINT GetAgentName(1); ": That's mighty fine sippin liquer"
    					oEnterMineAndDigForNugget = CLASS "cEnterMineAndDigForNugget"
    					oMiner.oNewState =OBJPTR(oEnterMineAndDigForNugget)
    				ELSE
    					PRINT "Error"
    					PRINT "Error"
    					PRINT "Error"
    				END IF
    			END IF
            END METHOD
    '------------------------------------------------------------------------------
            OVERRIDE METHOD Exit(BYVAL Agent AS tAgent)
                PRINT GetAgentName(1); ": Leaving the Saloon"
            END METHOD
        END INTERFACE
    END CLASS
    '=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*
    FUNCTION PBMAIN () AS LONG
        LOCAL oMiner AS iMiner
        LOCAL i AS LONG
    
    TRACE NEW "WW15_TRACE.TXT"
        COLOR 11
        oMiner = CLASS "cMiner"
    	oMiner.m_Id = 1
        FOR i = 1 TO 20
            IF oMiner.UpDate() < 0 THEN
                PRINT "Bad UpDate"
                EXIT FOR
            END IF
            Sleep 1000
        NEXT i
    
        PRINT "Any Key To Exit"
    
        WAITKEY$
    
    TRACE CLOSE
    
    END FUNCTION
    Attached Files
Working...
X