PDA

View Full Version : FSM style Mainloop



Blumblebee
08-06-2010, 10:05 PM
This will be a fairly quick tutorial and I will try to keep it as simple as possible.

This tutorial will require a basic knowledge in the following area(s):

Pointers
conditional statements (if..then statements etc)
system/program flow


I. Intro

Well first of all I'm sure you're wondering "what is FSM"? FSM stands for Finite-state machine (http://en.wikipedia.org/wiki/Finite-State_Machine). Basically this type of mainloop mirrors that of RSBot's in the fact that the mainloop responds to "states" set by a previous function.

A FSM loop should look something like this:


const
{enum}
BANK = 1;
WALK = 2;
COOK = 3;
ANTIBAN = 4;

{enum: an enumeration of a set is an exact listing of all of its elements (perhaps with repetition).}

type
tStatus = record
ID: Integer;
Name: string;
end;

tPointerSet = record
isProc: Boolean;
proc: procedure;
func: function: Boolean;
name: string;
end;

var
state: tStatus;
Pointers: tPointerSet;
TileArray: array of tTile;
IDArray: array of Integer;

function getState(): Integer;
var
tempID: Integer;
i: Integer;
begin
if( (getAnimation() > -1) or (CharacterMoving()) )then
result := ANTIBAN;
if( result <> 0 )then
exit;

if( distanceFrom(TileArray[_BANK]) < 4 ) then
begin
tempID := getItemIDAt(1);
if state.ID = tempID then
result := WALK
else
result := BANK;
end;
if( result <> 0 )then
exit;

if( distanceFrom(TileArray[_COOK]) < 5 ) then
begin
tempID := getItemIDAt(1);
if( state.ID = tempID )then
result := COOK
else
result := WALK;
end;

if( result = 0 )then
begin
writeLn('getState is having issues. Checking for randoms.');
for i := 0 to 15 do
begin
if( FindNormalRandoms() )then
Exit;
wait(50);
end;
writeLn('not in a Random and state enum = 0. NextPlayer');
NextPlayer(False);
Exit;
end;
end;

procedure Loop();
begin
with Pointers do
begin
case getState() of
ANTIBAN:
begin
name := 'antiban';
isProc := true;
Proc := @Anti_Ban;
Func := nil;
end;
WALK:
begin
name := 'walk';
isProc := false;
Proc := nil;
Func := @createWalkLocation;
end;
BANK:
begin
name := 'bank';
isProc := false;
Proc := nil;
Func := @Bank_Loop;
end;
COOK:
begin
name := 'cook';
isProc := true;
Proc := @Cook_Loop;
Func := nil;
end;
end;
end;
end;

var
mtile: ttile;
p: tpoint;
begin
SMART_Signed := true;
SMART_Server := 81;

setUpSRL();
setUpReflectionEx(true);
DeclarePlayers();

TileArray := [tile(3269, randomRange(3169, 3166)), tile(3273, 3181)];
setArrayLength(IDArray, 1);
IDArray := [25730];

repeat
while not LoggedIn() do
LogInPlayer();
wait(randomRange(250, 500));
setAngle(true);

while Players[CurrentPlayer].Active do
begin
Loop();
with Pointers do
begin
writeLn('entering '+name+' cycle.');
if isProc then
Proc()
else
Func();
end;
end;
until AllPlayersInactive();
end.


Don't worry I will break it all down later. For now that is just a simple example.

II. Advantages of FSM

I have found in my 2 years of scripting recalibrating a script when it is lost is a difficult task. When a random mis-click or mis-hap occurs during the runtime of the script, it can alter the script and screw it up. This is because scripts normally flow with (for lack of a better phrase) a conditional flow. By this I mean,


if not Conditonal_Function then
begin
NextPlayer(False);
Exit;
end;

if not Conditonal_Function2 then
begin
NextPlayer(False);
Exit;
end;

// and so on and so fourth.


A FSM style loop allows for a recalibration of errors and ensures a longer runtime. When the script dynamically outputs functions/procedures in a response to it's environment the script should leave few area's to falter and should pick up on these issue's.

III. Warnings/Disadvantages

First of all there should be a simple warning cautioned that developing a well functioning FSM can become difficult/complicated. This is where you will require knowledge in Program Flow. I will now address the getState() function.


function getState(): Integer;
begin
if This_is_true then
result := CORRESPONDING_ENUM; // enums must begin at 1 not 0. or else the result could/would always = 0
//after this first state.
if result <> 0 then
Exit;

if This_is_true then
result := NEXT_ENUM;

// so on and so fourth.
end;


now addressing the flow problem here goes. So the issue lies here, what if two results are present on the screen, yet you can only have one result correct? So You must order the flow of the function to work according to the script. (That's a terrible explanation I know). Ummm, here's an example.


function getState(): Integer;
begin
if distanceFrom(tile(xxx, yyy) < 3 {<--- atTrees()} then
result := CORRESPONDING_ENUM; // enums must begin at 1 not 0. or else the result could/would always = 0
//after this first state.
if result <> 0 then
Exit;

if not invfull() atTrees() then
result := NEXT_ENUM;
end;


so let's say the character is on the specified tile that the function results with CORRESPONDING_ENUM. However, the script should have been chopping as that statement is also true because the player is by the tree's. So what happens is the player walks back to the bank instead of chopping like he should be.

This is where you are faced with one of two things. Either

A) You have to make each condition specific, meaning the first statement would become
if invfull() and distanceFrom(tile(xxx, yyy) < 3 then

or

B) You simply reconstruct the flow and put the Chop_Tree condition infront of the walking condition so the function returns that result first.

Either way works, I like to use a conjunction of both, as the more complicated you make each if..then statement the harder to understand the code becomes and tweaking turns into a bitch.

Sorry if that is confusing, I will try to readdress that later.

IV. Setting Pointers

Part A:

Pointers are simple tools that are often overlooked. I'll do a quick, simple breakdown of pointers, link some good tutorials on them and then continue with the tutorial.

A Pointer is simply a variable that can hold a function/procedure.

example


program new;

var
point: procedure;

procedure blah();
begin
writeLn('meow');
end;

begin
point := @blah;
point();
end.


same thing works with functions, however functions are a bit tricky. Say you have two functions, and you want one universal pointer. You have to keep in mind that these two functions must have the same parameters to used in conjunction with a single pointer.

example:

bad



program new;

var
point: function(): integer;

function blah(): Integer;
begin
result := 1;
end;

function blahblah(int: Integer): Integer;
begin
result := int;
end;

begin
if random(2) = 1 then
point := @blah
else
point := @blahblah;
point();
end.



good



program new;

var
point: function(): integer;
Globally_set_Var: integer;

function blah(): Integer;
begin
result := 1;
end;

function blahblah(): Integer;
begin
result := Globally_Set_Var;
end;

begin
Globally_Set_Var := 9;
if random(2) = 1 then
point := @blah
else
point := @blahblah;
writeLn(inttostr(point()));
end.



I hope that makes sense. Further explanation can be given upon request.

Part B:

Now Back to the mainloops. So we now have a setState() function created. Our conditionals set and results enumerated. Next step is to link these enums to specific functions/procedures. This is where pointers come into play.

first let's make a type to hold our pointer variables.

type
tPointerSet = record
isProc: Boolean; // This will be used later in the mainloop execution
proc: procedure;
func: function: Boolean;
name: string; // helps us keep track of the function being called
end;

next let's make an empty case statement that corresponds to the getState() function created.


procedure Loop();
begin
case getState() of
ANTIBAN: // empty
WALK: // empty
BANK: // empty
COOK: // empty
end;
end;


now we simply fill the Loop() procedure in with our pointers. It will look like so.


procedure Loop();
begin
with Pointers do
begin
case getState() of
ANTIBAN:
begin
name := 'antiban';
isProc := true;
Proc := @Anti_Ban;
Func := nil;
end;
WALK:
begin
name := 'walk';
isProc := false;
Proc := nil;
Func := @createWalkLocation;
end;
BANK:
begin
name := 'bank';
isProc := false;
Proc := nil;
Func := @Bank_Loop;
end;
COOK:
begin
name := 'cook';
isProc := true;
Proc := @Cook_Loop;
Func := nil;
end;
end;
end;
end;


so if you haven't already figured out when you wish to keep a pointer empty simply setting it to "nil" will do. The isProc variable will come into play later, bear with me for that. It is infact useful ;)

V. The MainLoop

This is the fun part. That code in an FSM Mainloop is infact quite simple (as you've seen from the example at the start of the tutorial).

here is the example, then I will go in detail about it (although it's probably not even needed).

repeat
while not LoggedIn() do
LogInPlayer();
wait(randomRange(250, 500));
setAngle(true);

while Players[CurrentPlayer].Active do
begin
Loop();
with Pointers do
begin
writeLn('entering '+name+' cycle.');
if isProc then
Proc()
else
Func();
end;
end;
until AllPlayersInactive();


the main gyst we're going to be looking at really is:

while Players[CurrentPlayer].Active do
begin
Loop();
with Pointers do
begin
writeLn('entering '+name+' cycle.');
if isProc then
Proc()
else
Func();
end;
end;


Now the isProc boolean was a failsafe to ensure that we did not call a procedure/function that has been set to nil. It ensures that the right variable is called upon.

Aside from that the loop is quite simple really. Loop() is called at the start of each succession and the proc/func is set in correspondence.

VI. Final Notes

A few final notes on my tutorial.

Regarding breaking the loop, I did not show any failsafes within the MainLoop itself. Inside my functions/procedures I simply call NextPlayer(false) or Players[CurrentPlayer].Active := false to break out of the 'while active do' loop.

If there are any questions/concerns feel free to PM me or ask here, I will be happy to help. Same goes if there are any corrections that need to be made, I wrote this quite fast and under a bit of pressure :p

I Hope you (the reader) found this, if nothing else interesting. Comments and suggestions are appreciated.

Some tutorials that you may find interesting:

Records and Psuedo-OOP (http://villavu.com/forum/showthread.php?t=43158)
Function and Procedure 'variables'. (http://villavu.com/forum/showthread.php?t=27218)
Will Edit with more later

Yush Dragon
08-13-2010, 12:03 AM
Lawl, nice tutorial, even i don't know anything about what u wrote there xD
im not that far in SRL :P

anyways, +rep

BraK
09-19-2010, 08:21 PM
I really want to understand this but I'm almost completely lost by the sections 3&4. Which Script do you use this in cause I might be able to beter understand seeing it work in a real scenario.

bugger0001
09-20-2010, 06:25 AM
Got quite a few good ideas how to build up my yet to come scripts. This not only makes it easier to keep track of your script, but it also makes it easier to get lots of scripts combined and well organized. Although, this is something you can't actually implement to every script. But to all 'mine/chop/etc & bank' scripts, it would be a good implementation.

TomTuff
09-20-2010, 06:56 AM
wow very clear cut tutorial, I'll definitely have to incorporate this in to my 2 current projects!

HyperSecret
09-20-2010, 10:28 AM
Those aren't actually considered pointers are they? I thought we didn't have access to pointers...

BraK
09-22-2010, 11:38 AM
Been reading up on Finite-state Machine(FSM) and Virtual Finite state Machine(VFSM) and I have to say it's definitely a concept that's worth learning for programming. Your link to wiki is broken it's Here (http://en.wikipedia.org/wiki/Finite-state_machine). Which is funny because the only difference is the s in state and the m in machine are lower case. Wiki might be case sensitive.

So do you have to set a different global var for each function/procedure with different parameters. Please explain it a little more.

E: What I mean is:

Function A(a:integer):integer;
Function Getcolor(x , y :integer);
Findcolorstoloerance(T :TPA; Color, xs, ys, xe, ye, tol : integer): boolean

I'd have to globally set variables for each of these functions. I know they aren't a great example but they get my point across. But these below could be set on the same variable.

Writeln(x: string);
Debug(x: string);

Also how would I write those up in a pointer. Would it be like.

Writeln('sdf')();

E2: Scratch what I said. I think I was lost in my last edit, but further explaining would be nice. Could you please include a small working code of how it should be set up so we can experiment in simba/scar with it?

Nava2
09-22-2010, 01:12 PM
When calling a procedural or functional pointer, the parenthesis are always needed. If you have parameters, just stick them where they normally go. Also, you call it using the var name, and when you are getting reference to the function as a pointer you prefix it with @.

BraK
09-22-2010, 01:55 PM
Hmm so really you'd only want to use it on the main transitioning functions.

const
DepBank = 1;

procedure GetState();
begin
if Bankscreen and inv_of_bows then
Result := Depbank;
end;

and so on correct. So basically At the end of a transition, Getstate() determines where it's at and what it needs to do next if it is lost or malfunctions. Correct?


E: I'm confusing myself and spitting out crap I just realized.

Nava2
09-22-2010, 02:13 PM
Hmm so really you'd only want to use it on the main transitioning functions.

const
DepBank = 1;

procedure GetState();
begin
if Bankscreen and inv_of_bows then
Result := Depbank;
end;

and so on correct. So basically At the end of a transition, Getstate() determines where it's at and what it needs to do next if it is lost or malfunctions. Correct?


E: I'm confusing myself and spitting out crap I just realized.

No, no. You are correct actually! At least how I do it.

I have callibration functions which are used everytime a player moves on. I had it skip the callibration if it successfully completed the current section, or callibrated to the next section. I don't remember which I did now.

Zyt3x
09-22-2010, 02:32 PM
Hmm... I have used this style of mainlooping without knowing that a name existed for it :O
Nice tutorial! Time to do some reading :)

Blumblebee
09-28-2010, 07:55 PM
Hmm so really you'd only want to use it on the main transitioning functions.

const
DepBank = 1;

procedure GetState();
begin
if Bankscreen and inv_of_bows then
Result := Depbank;
end;

and so on correct. So basically At the end of a transition, Getstate() determines where it's at and what it needs to do next if it is lost or malfunctions. Correct?


E: I'm confusing myself and spitting out crap I just realized.

You can use GetState to calibrate if you want, but I actually use it to do every task. Example would be my Essence Miner, where it responds to the enviroment it's in, which should in theory make it fool-proof.

BraK
09-28-2010, 07:59 PM
Thanks for the reply and I'll have to look into your Essence Miner I haven't seen it yet. Love the code for your AK cooker and Fletch N String. Between you and Narcle I get Good Ideas of how to mod my scripts all the time.

wthomas
03-20-2013, 03:32 PM
Free bump, This is a good way to structure a script that can recover from any mishaps. I know its 3yr old bump, but in my opinion more scripts should be written like this, and not like:

pickInvFlax;
wait(1000);
walkBank;
wait(1000);
bankItems;
wait(1000)
walktoFlax;
wait(1000)
until not LoggedIn;


scripts like this ^^ will get users banned.

King
03-20-2013, 04:23 PM
Free bump, This is a good way to structure a script that can recover from any mishaps. I know its 3yr old bump, but in my opinion more scripts should be written like this, and not like:

pickInvFlax;
wait(1000);
walkBank;
wait(1000);
bankItems;
wait(1000)
walktoFlax;
wait(1000)
until not LoggedIn;


scripts like this ^^ will get users banned.

Thankyou for the awesome grave dig, Im writing my next script like this(:

StickToTheScript
03-20-2013, 04:33 PM
Very interesting... I will be sure to use it in the future. Thanks!

Kevin
03-20-2013, 04:41 PM
Free bump, This is a good way to structure a script that can recover from any mishaps. I know its 3yr old bump, but in my opinion more scripts should be written like this, and not like:

pickInvFlax;
wait(1000);
walkBank;
wait(1000);
bankItems;
wait(1000)
walktoFlax;
wait(1000)
until not LoggedIn;


scripts like this ^^ will get users banned.

I'm in no way saying FSM style programming is always bad, but I am offering an alternative for mishap recovery. Ignoring basic standards like randomization, let's look at your example script and add a 'stuck' failsafe. Obviously, it would be a little more fleshed out, but I tend to have my scripts follow logic more akin to this:

var
stuck: integer;

begin
whateverSetup;
repeat
stuck:= 0;
if(not pickInvFlax)then
Inc(stuck);
wait(1000);
if(not walkBank)then
Inc(stuck);
wait(1000);
if(not bankItems)then
Inc(stuck);
wait(1000)
if(not walktoFlax)then
Inc(stuck);
wait(1000)
if(stuck >= totalStepCount)then//In this case, the main loop has 4 steps
FailScript;//No single step could succeed, but if one could succeed, then a mishap was handled and the script follows it's normal pattern where it left off.
until not LoggedIn;
end;

wthomas
03-20-2013, 10:53 PM
I'm in no way saying FSM style programming is bad, but I am offering an alternative for mishap recovery. Ignoring basic standards like randomization, let's look at your example script and add a 'stuck' failsafe. Obviously, it would be a little more fleshed out, but I tend to have my scripts follow logic more akin to this:

var
stuck: integer;

begin
whateverSetup;
repeat
stuck:= 0;
if(not pickInvFlax)then
Inc(stuck);
wait(1000);
if(not walkBank)then
Inc(stuck);
wait(1000);
if(not bankItems)then
Inc(stuck);
wait(1000)
if(not walktoFlax)then
Inc(stuck);
wait(1000)
if(stuck >= totalStepCount)then//In this case, the main loop has 4 steps
FailScript;//No single step could succeed, but if one could succeed, then a mishap was handled and the script follows it's normal pattern where it left off.
until not LoggedIn;
end;



Your method will work with the way that you script your procedures, as soon as anything is hard coded into the script it'll still break. ie if the walktoBank procedure just looked like

MakeCompass('n');
mouse(MSCX,MSCY-50,5,5,mouse_left);
wait(2500);
mouse(MSCX,MSCY-50,5,5,mouse_left);
wait(2500);

The script would still break. The method you outline tries all of the possible steps in turn and executes them whereas the FSM decides which it should do and tries to execute that. Coded well they will both function exactly the same, but if the functions are coded without fail safes at the star then your method will still run about headless'ly before it realizes its lost. The main difference is the FSM would recognize its lost faster.

Kevin
03-20-2013, 11:08 PM
Your method will work with the way that you script your procedures, as soon as anything is hard coded into the script it'll still break. ie if the walktoBank procedure just looked like

MakeCompass('n');
mouse(MSCX,MSCY-50,5,5,mouse_left);
wait(2500);
mouse(MSCX,MSCY-50,5,5,mouse_left);
wait(2500);

The script would still break. The method you outline tries all of the possible steps in turn and executes them whereas the FSM decides which it should do and tries to execute that. Coded well they will both function exactly the same, but if the functions are coded without fail safes at the star then your method will still run about headless'ly before it realizes its lost. The main difference is the FSM would recognize its lost faster.

In general static movement/actions should be avoided with any script, and that is an important assumption. I easily agree with that, as well as the lack of failsafes causing my method to run about without an idea as to what's happening. FSM may recognize it is lost faster in a failure scenario/starting at any point in the run; I can agree with that as well. However, in a success scenario (which should occur the majority of the time in a well coded script), a script that does not need to make a state check after every action should run more efficiently.

stata
08-30-2013, 09:59 PM
Having taken several courses where most programs were structured as FSM's (we called them FSA's usually) and also being an ex-RSBot scriptwriter, can't stress enough how great structuring your main loop like this is. You can add all the necessary failsafes without this structure, but just thinking about and organizing your code in this way will make this far easier in the long run. Great tutorial, very thorough.

My only question is, is it necessary to have the loop outside of the main begin end block (not sure what this is called, kinda new to Pascalscript)? Couldn't the loop have just been inside the main begin end block, and then you could call the functions/procedures directly from the main loop. It just seems to be an unncessary extra layer of complexity (including a very odd dual-function-procedure-pointer-type type thingy). Were you trying to limit the scope of the code in the loop for some reason?

Flight
05-15-2014, 02:57 AM
Excuse my grave-dig but I'd like to bring this tutorial to light. This is a great way to structure your scripts and how they run, I'd strongly recommend this.

Zyt3x
05-15-2014, 06:07 AM
Excuse my grave-dig but I'd like to bring this tutorial to light. This is a great way to structure your scripts and how they run, I'd strongly recommend this.Gravedig excused. This is indeed a very good way to structure your scripts

Sk1nyNerd
05-15-2014, 06:33 AM
nice tut, i wrote a couple scripts this way in the past without knowing there was a name for this style lol. i found it quite useful to use when i had a script that did a fair amount of walking, i found this style easy to assess my location and what i needed to do next

Kevin
05-19-2014, 05:06 PM
Excuse my grave-dig but I'd like to bring this tutorial to light. This is a great way to structure your scripts and how they run, I'd strongly recommend this.


Gravedig excused. This is indeed a very good way to structure your scripts

Absolutely acknowledging the ease of failsafing a script using Finite State Machines, I disagree (and I think Brandon has also stated a dislike for FSM, although maybe I'm wrong and am alone on this). The amount of time spent determining 'What step am I in?' for every step of a loop can be astronomical, especially as scripts become more complex. You could still allow for the ease of failsafing noted in FSM, while following normal code progressions (thus reducing the complexity of step checking) in a simple manner like this:
var
stuck: integer;

begin
whateverSetup;
repeat
stuck:= 0;
if(not pickInvFlax)then
Inc(stuck);
wait(1000);
if(not walkBank)then
Inc(stuck);
wait(1000);
if(not bankItems)then
Inc(stuck);
wait(1000)
if(not walktoFlax)then
Inc(stuck);
wait(1000)
if(stuck >= totalStepCount)then//In this case, the main loop has 4 steps
FailScript;//No single step could succeed, but if one could succeed, then a mishap was handled and the script follows it's normal pattern where it left off.
until not LoggedIn;
end;

honeyhoney
06-28-2014, 02:57 PM
Thanks for this Blumblebee. :)

Not sure if you're still around, but anyone is welcome to answer...

I noticed you provided the ability to call both procedures and functions in the loop.
I'm assuming you included this as some parts of the loop (ie. banking and walking) will return a true/false dependant on it's success/failure.
Would it be acceptable to only call procedures in the loop but have checks in getState() to determine whether that procedure should be called?
(to clarify, if banking was a procedure and not a function, we would continuously call the banking procedure until the conditions to enter a different state were met (failsafes would obviously be integrated))