"When I was a boy", there were computer magazines full of programs you could type into your computer.
BASIC was the usual language- pretty limited, but then, so were computers!
Even though the language was basic, some of the programming was far from trivial. And a lot of fun games were created. And a lot of fun was had typing them in, and learning a lot during the debugging process.
This is one of the longest, and to be honest, perhaps most tedious, of my tutorials.
Partly because it covers two separate topics.
Yes, by the end, you are well on the way to a converted BASIC program, but, more importantly, and even if you don't want the BASIC program(!), I commend this to you: It trys to convey my grasp of how thinking of the computer as a "state machine" can help you write bigger, better programs.
I'm new to the "state machine" perspective. But following the twists and turns of what I do understand may start you on the journey towards having this useful trick in you armory.
Just as this tutorial is "about converting a BASIC program", and about using the "state machine" model, I have another tutorial which you may benefit from, even if you aren't an Arduino programmer. An Arduino program creating a "state machine".
It is easy to find listings of those old programs. If you want to try to convert one to Lazarus, I hope this guide will help.
Three magnificent books, often available via Amazon or Abebooks... I'd commend Abebooks to you...
What to Do After You Hit Return... Or P.C.C.'S First Book of Computer Games Published by People's Computer Company (1977)
Gloriously "70's". Large... 14" x 10".
Amazing value... $8!
... and Ahl's "Basic Computer Games". There were two volumes, available separately.
BASIC Computer Games: Microcomputer Edition David H. Ahl, Published by Workman Pub Co (1978) ISBN 10: 0894800523 ISBN 13: 9780894800528
(The second volume was "More BASIC Computer Games")
If you offer the sourcecode for any game from them, free, I'd be glad to put link here. There are adaptations of some, not always with sourcecode, but free, on my SheepdogSoftware pages.
Modern programming languages and operating offer new opportunities, but they also make planning a program different. Once the flowchart was king. (And it still has a place!) And the languages were "flowchart friendly".
Suppose you wanted to create the following "game". (Called "Goldie's Game", by those who saw the BBC's wonderful documentary on life at Radley College, 1979.) (There was an excellent follow up, 30 years later.) (The programmes are on YouTube.)
It is up to you, the programmer, to decide the rules of your version of this. For today's discussion, I'm going to say that the rules in this version are that you can't go on to the next part of the problem until you get the current part right. Your "score" would be how quickly you complete the task. (Of course, the program would change the numbers in the problem each time you went through it.)
A flowchart for that might look like the following, although they were usually draw down a page, not across it. (I turned it for better on-screen viewing.)
A program to do that, written in BASIC, leaving out the stuff to change the numbers in the problems, might look something like...
050 REM Demo BASIC program 100 PRINT "What is 5+4?" 110 INPUT Ans 120 IF Ans>< 9 THEN GOTO 100 200 PRINT "... plus 2?" 210 INPUT Ans 220 IF Ans>< 11 THEN GOTO 200 300 PRINT "... minus 5?" 310 INPUT Ans 320 IF Ans>< 6 THEN GOTO 300 400 PRINT "Good! Do another? ('N' for "no")" 410 INPUT Ans$ 420 IF Ans>< "N" THEN GOTO 100 430 PRINT "Goodbye"
When you "ran" the program, it started with the line with the lowest line number. (Line numbers were very important to early BASICs, but by the end were less important... "labels" had been added to the language.)
Any line beginning "REM" was information for people reading the code.
PRINT put things on the screen.
INPUT would be followed by one or more variable names. Simple variables either held numbers or strings of characters. The latter had a $ at the end of the name, e.g. Ans$, in line 410 of the example. (If there were square brackets, e.g. LikeThis[x], the variable was an element of an array.)
Apart from array elements, variable names started with a letter, and were made up of letters and digits. A $ might appear at the end, to say "this is a string variable". Other than that, that was it. Names tended to be short.
If you see...
100 DIM X[3] 110 DIM P[2,2]
... you should interpret it as...
Set up arrays with the following elements...
X[0], X[1], X[2], X[3] ... and... P[0,0],P[1,0],P[2,0] P[1,1],P[1,1],P[2,1] P[2,2],P[1,2],P[2,2]
In each case, the "0" element may or may not have been present, depending on the BASIC you were using.
Something like...
120 IF Ans>< 9 THEN GOTO 100
... would look at what was in the variable "Ans". If it were NOT 9, the flow of code execution would jump to line 100 of the program. Otherwise, flow continued "down the page".
Not present in the current example, but also noteworthy....
120 FOR A=1 to 5 122 PRINT A 124 NEXT A 130 PRINT "Done"
Would have resulted in...
1 2 3 4 5 Done.
GOSUB 1000 sent the flow of code execution off to line 1000, from whence it continued, until it hit the word "RETURN", at which point it would jump back ("return"!) to the line after the GOSUB 1000 that had caused the GO to the SUBroutine. (When you go to a subroutine, the system remembers where you were when you went, and sends you back to the next line when "RETURN" is seen. Neat! Useful. In the current program, I don't think any subroutine is used from more than one place... but they CAN be. (And often are.)
There was a nifty thing called "DATA". It was used with READ... for things much fancier than what follows...
110 DATA 123,456, 100, 300, 200 120 FOR A=1 to 5 122 READ X 124 PRINT X 126 NEXT A 130 PRINT "Done"
That would have given rise to...
123 456 100 300 200 Done.
The Wumpus program, listing follows, seemed to have a way to create functions! I don't recall using this much in my BASIC days... but that was my weakness, not a good idea.
Line 170 says...
170 DEF FNA(X)=INT(20*RND(0)))+1
I believe that RND(0) will return 0 to 0.99999.
I believe that this "sets up" a function called "FNA". I believe that if, as at line 250 (I've changed it slightly), you say...
250 L[J]=FNA(27)
... then, at 250, the program will "look up" the function it knows, and FNA(27) will "boil down to", "be returned as" whatever INT(20*RND(0)))+1 evaluates to. In this instance, I believe the 27 in the calling statement was "thrown away". Had there been an X to the right of the "=" in the expression at 170, I believe the 27 would have been "plugged in" there, and affected what FNA(27) "boiled down to". // In any case, when 250 was called, there was a value in J. Let's say it was 1. And FNA(27) boiled down to something; let's say it was 16. After 250, in those circumstances, L[1] would hold 16.
All of which is very well, but I was supposed to be talking about how the execution proceeds in an BASIC program. Which line is done when. But it is as well to have got some of the "petty" things out of the way.
In the "good old", "simple old" days, flow control was easy. I hope that the little example above illustrates that.
However, there was an EVIL command that had perfectly good uses... but which was often used extremely ill-advisedly. "GOTO".
If you put GOTO 200 in a program, then when execution reached that line, execution jumped to line 200 without regard to ANYTHING. This led to some Really Bad programs.
As I say... it wasn't always a disaster when it was used. Wumpus has two... one at 620 which says "Go back, and do it all again" to a user who has finished a round of the game.
Back in the days of BASIC, having the computer just sit there until you answered a request for Input was no big deal. It wasn't doing anything else.
Today's multi-process operating systems tolerate such things poorly. If they are done crudely. But there are ways to make the program work just fine, even if you are slow to answer, from the small example this started from, "What is 5+4?"
Here's how it is done, with Lazarus...
We put a timer object in our program, and from time to time... say 5 times a second or so?... we look to se if a button has been pressed, etc, since we last looked.
If it has, we consider what we should do.
We start the application up in one STATE. We do all we can without using something that has to be waited for, e.g. an answer from the human as to the sum of 5 and 4, and then we settle down, waiting.
Once the human has supplied an answer, we evaluate that answer, and move on to a new state, if necessary.
Within the timer event handler, we'll have many blocks of code which will boil down to something like the following... which is NOT compilable code...
if we are in "state 0"... we need to... ---- check whether an answer has been supplied ---- if it has, we need to... ------------see if answer to first was right. ----------- if wrong, we move on to state 1 ----------- if right, we move on to state 2 if we are in "state 1"... we need to... ---- tell user answer was wrong, and ---- move on to state 0 if we are in "state 2"... we need to... ---- put the next question on ---- move on to state 0 AND SO ON...
When you examine the code... it is coming... it may all be more clear.
Note in particular that when I say "move on to (a different state)", the way I do that is to change the value in the bStateWeAreIn variable. Nothing happens immediately, because of that, but the next time we enter the timer's event handler, we do a different bit from what is available there.
Here we have the core of a small program that is a start towards the "state based" answer that is our objective.
It is the core central to an app with a memo (where user prompts will appear), an edit box (where user input will go) and a timer, to keep things ticking over.)
So far, it just flips back and forth from state "0" to state "1". A label on the form... not normally present... tells you the current state.
(The application doesn't actually "do" "anything" (else) yet!)
procedure Tlt3n_convert_old_basic_f1.FormCreate(Sender: TObject); begin lt3n_convert_old_basic_f1.caption:= kPrgmName+' vers:'+kVers; meShowUser.lines.clear; Timer1.interval:=500; Panel1.caption:=''; MakeState(0);//Initialize bState, display end; procedure Tlt3n_convert_old_basic_f1.Timer1Timer(Sender: TObject); begin case bState of 0:begin MakeState(1); end; 1:begin MakeState(0); end; end;//case end;//Timer1Timer procedure Tlt3n_convert_old_basic_f1. MakeState(bNewState:byte); begin bState:=bNewState; laCurrentStateID.caption:=inttostr(bState); end;//MakeState
Make sure you're clear about what the above does. Then we will move on.
Next, we will add boInputReceived, and change the Timer event handler.
Now the application will stay in one state or the other (... we have two so far. More will be typical...) UNTIL some data, any data is entered into the edit box, and the enter key is pressed, or something else triggers the edit box's OnEditingDone event.
Here's the new code, with that development in it. I've removed from here, but not from the actual code, some things that haven't changed since the previous listing. Notably some "housekeeping" in FormCreate, and the details of what happens during MakeState()
procedure Tlt3n_convert_old_basic_f1.FormCreate(Sender: TObject); begin MakeState(0);//Initialize bState, display eUserInput.text:=''; boInputReceived:=false; end; procedure Tlt3n_convert_old_basic_f1.eUserInputEditingDone(Sender: TObject); begin boInputReceived:=true; end; procedure Tlt3n_convert_old_basic_f1.Timer1Timer(Sender: TObject); begin if boInputReceived then begin boInputReceived:=false;//restore to "waiting" case bState of 0:begin MakeState(1); end; 1:begin MakeState(0); end; end;//case end;//if boInputReceived end;//Timer1Timer
What states will Goldie's Game entail?
For the sake of completeness, I will first mention: What happens during the FormCreate handler might be seen as the application passing through a state. We were in no state when the application started running. Things happened during FormCreate, and then we "changed state" into whatever state dictated by the code in FormCreate.
Before we begin the "blow by blow", let me remind you: This will not be "finished". (You can finish it!) In particular, it will give the same problem over and over! (The final answer is 6!)
What is at the heart of "being in a state"?
The variable bState. If bState holds 0, we are in "state 0".
FormCreate will normally leave an application in state 0. It is just logical.
State 0: Start problem
We enter it from: The FormCreate handler
We go from it to: State 1. While in state 0, we put a "Welcome" message up, and we post the first part of the problem, e.g. "What is 5+4?".
We set bTermsDone to 2, because when we leave, the user will have done 2 terms of the problem.
We leave the state when some input has been seen.
State 1: Process input. We enter it from: State 0 the first time, and subsequently from state 2
We go from it to: 2 if a right answer has been given, or we stay in 1, or we go on to state 3 if enough terms have been presented and answered correctly
State 2: Present next term. We enter it from: State 1
We go from it to: State 1
State 3: Wrap up, after last part of question done. We enter it from: state 1
We go nowhere from it.. it contains the end of the application.
You can download the full sourcecode of the little arithmetic test, if you wish. Try adapting it to time how long it takes the user to complete the problem. Try adapting it to present more than one problem to the user.
It is "driven" by the code that happens every time the Timer counts down to zero...
begin //main block of Timer1Timer //boInputReceived:=false;//restore to "waiting" case bState of 0:begin DoState0Things; end; 1:begin DoState1Things; end; 2:begin DoState2Things; end; 3:begin DoState3Things; end; end;//case end;//Timer1Timer
(Core to the whole design is that the value in bState sometimes changes inside one of the "DoThings" subroutines, of course.)
Here are the details of the DoThings subroutines...
procedure DoState0Things; //State0: Start problem begin meShowUser.lines.add('Welcome!'); meShowUser.lines.add('-------'); meShowUser.lines.add(''); meShowUser.lines.add('What is 5+4?'); bAns:=9; bTermsDone:=2; MakeState(1); end;//DoState0Things procedure DoState1Things; //State1 ProcessInput begin if boInputReceived then begin boInputReceived:=false; case bTermsDone of 2:begin if (extUserEntered=bAns) then begin sNextOpIs:='.. add '; bNextTerm:=2; bAns:=11; MakeState(2); bTermsDone:=3; end;//if extUserEntered=bAns end;//Case 2 3:begin if (extUserEntered=bAns) then begin sNextOpIs:='.. subtact '; bNextTerm:=5; bAns:=6; MakeState(2); bTermsDone:=4; end;//if extUserEntered=bAns end;//Case 3 4:begin if (extUserEntered=bAns) then begin MakeState(3); end;//if extUserEntered=bAns end;//Case 4 end;//case bTerms of... end;//If boInputReceived end;//DoState1Things procedure DoState2Things; //State2: Present next term begin meShowUser.lines.add('Yes! Now...'); meShowUser.lines.add(sNextOpIs+ inttostr(bNextTerm)+'?'); MakeState(1); end;//DoState2Things procedure DoState3Things; //State3: Wrap up begin meShowUser.lines.add('Yes again...'); meShowUser.lines.add('...and you are done!'); Timer1.enabled:=false; end;//DoState3Things
What is MakeState()? I'm glad you asked...
begin bState:=bNewState; laCurrentStateID.caption:=inttostr(bState); laTermsDone.caption:=inttostr(bTermsDone); end;//MakeState
That's all well and good. And it is good, by the way. Thinking of an application as a state machine is very useful.
But this page was supposed to be about converting an old BASIC program to Lazarus.
We're going to make a state machine to "hold" the Lazarus program.
The program we'll take as an example appeared in 1978 in the wonderful "What to do after you hit Return", the book I mentioned earlier.
Wumpus is a cave adventure. There's a Wumpus in the cave. No one has seen a Wumpus and lived to tell the tale, so they are assumed to be dangerous.
You wander from chamber to chamber withing a cave system. From time to time you are told "There are bats nearby", or "There is a Wumpus smell".
Either means a hazard in an adjacent cave chamber. Not the one you just came from, obviously. One of the ones in front of you.
If you stumble into a chamber with bats, they will snatch you up, whisk you to some other chamber. If you enter one with the Wumpus you die.
Object of the game?
The Wumpus is big, and for some reason you want it dead. It is enough to fire one of your magic arrows into the chamber the Wumpus is in... you will hit it, and kill it. "Magic" arrows? They were early cruise missiles. If chamber 3 connects to 4 connects to 5, and the Wumpus is in 5, you can fire an arrows from three, with it programmed to go to 4 then to 5. Be careful. If you accidentally send the arrow to the chamber you are in, you will kill yourself.
Furthermore, while normally the Wumpus is a placid creature, stays put, he hates the sound of bowstrings. If you shoot at him and MISS, he will move. If he moves into the chamber you are in, he will eat you.
And there's another hazard, which you'll find out about in due course.
So... wander about. Build a map ("Chamber 1 connects to chambers 3, 6 and 8. Chamber 3 connects to... Etc.) Find the Wumpus by smell. Kill it.... Easy! (Easier than writing the program, anyway.)
We don't have to write the program! It's been written. But we have to convert it.
Start with the listing. Below.
(I should mention at this point that I have not FINISHED the conversion of Wumpus to Lazarus... ran out of time! Will try to come back to it. But I've made a good start, and the sourcecode of that is available to you as a free download. And I sketched all that remains. You do it, if you don't want to wait for me!
Draw a diagram of the times that execution can go someplace other than just onward to the next line. Also below, although the diagram has at least one slight flaw. Diagram below is a starting point.
Build state diagram. At first, "fake" many parts of the program. Instead of using the user's input as it will be used eventually, use it to simulate the bits of the program you haven't got to yet, so you can build it bit by bit. To be more specific...
I've started the Lazarus Wumpus with the code for "see how "shoot arrow" went". At this stage, you may have won, or lost, or merely have used up an arrow without hitting the Wumpus. After this stage, what was called "F" in the original, which I've renamed "siF_inished", holds 1, -1 or 0, respectively. "siF_inished" will hold a code for whether we have finished, and if so, how did things turn out? In this text I may use either name... F or siF_inished... because all of the variable names in the original consist of only one letter, or a letter and a digit, e.g. A1. "siF_inished" must be the Lazarus code equivalent of "F", so either should make sense to you. (When I mentioned "A1", I MEANT A1, not A[1]. The latter would be an element of the ARRAY A[]. "A1" is just an "ordinary" variable, with a slightly "long" name. If A1 in the BASIC version was holding air left, as a byte, in the Lazarus version, I'd call it bA1_irLeft. Details! Details! Sigh. Must keep that devil managed.
The BASIC code deals with what's in F from line 490. At that point the value in F, we see from 3000 onward, is either 0, (game not over yet), 1 (you won) or -1 (you lost) or something greater than zero. (Study those lines... see for yourself how those lines tell us that the "code" used in F is what I say. You have to sort of skim, sort of read. You won't understand every line, but you should find enough that you do understand to crack the code. Lines 490-620 are a help too... but where line 500 says "IF F>0..." you don't know if F can be more than one thing >0, or if it is what the difference would be between those "codes".)
We're going to start with converting...
490 IF F=0 THEN 390 500 IF F>0 THEN 550 510 REM-LOSE 520 PRINT "You lose.":REM (You can make the messages more fun as you go along) 530 GOTO 560 540 REM-WIN 550 PRINT "You won!" 560 FOR J=1 TO 6 570 L[J]=M[J] 580 NEXT J 590 PRINT "Same set-up? (Y/N)"; 600 INPUT I$ 610 IF I$<>"Y" THEN 240 620 GOTO 360
(A detail: At 460, we have a "GOTO 500". The way I am converting this, it is as if I have changed that to a "GOTO 490"... but it won't matter to what the user sees. (The original saved one "if" test, because at 460, F couldn't be 0, but it might be -1 or +1. Saving that one IF "tangled" the code. Doing the unnecessary IF (as I am doing) makes the code tidier. END OF "detail")
Right. Before we even try to convert that, a number of things...
*IF* I have analyzed the code we are converting, correctly... a big if, of course... ESSENTIAL the only way we would enter this block of code is via line 490... although we might have gone straight to 500 in cases where doing 490 would not have had any effect.
It seemed "do-able"... that's why I chose to start with this. When this is "done" the "game" will consist of an app that starts with a question: "Did the player win, lose, or neither?" And then the right things will happen, depending on the answer, and we will either get the question again or the application will close down. We can build from there!
But.. in the above BASIC code? A few details...
Lines 560-580: I don't know WHY the values in L[x] are being changed. But I don't need to know why. I just Do It.
Line 520: The rem on the end, following the colon? You can do that. Put a colon, and then another statement after it. Not often a good idea, but allowed. ("REM" is a statement. Just not the only one that can be "tacked on" with a colon.)
Line 590: When there's a semicolon after a PRINT, it stops the output device from starting a new line. Thus, in this case, the cursor would be flashing just after "Same setup? (Y/N)".
Line 610: That should make easy sense, as you see it above. Note that in the listings, "<&" is shown as "#"
Variable names: We have already seen that I plan to change the BASIC program's "F" to "siF_inished".
If at first we don't know why something was called, say, L, we can use "L_x" until we figure it out, and then use Search and Replace. (Search and Replace would find extra things if we used just "L".)
Line numbers: Lazarus doesn't use line numbers the way BASIC did, but as an aid to going back and forth between the Lazarus code as we create it and the BASIC original, I will be making the line numbers part of the Lazarus.
The bit we are working on is going to be the "DecideIfDone" state... which I will refer to as "DecideIfDone_490-620", to point me to line 490, the source of the code.
Note the "superfluous" material in what follows which allows the user to have text names for the various states. The program may be using the simple "bState" value internally, but programmer-friendly version of the state's id will also be available!
Here are some core elements of the conversion at this early stage. Useless in it's own right, but perhaps what is here will aid your grasp of how a conversion is done. Yes! The Lazarus Wumpus was "grown" out of the seemingly useless little arithmetic exercise. Not so "useless", after all, eh?
Note: For the moment, Timer1.interval is set to 900, so that the there-for-one-cycle-only passage through the first states can be observed...
State 0: Start Session State 1: Start Game State 2: Start Round State 3: FakeMostOfWumpus...
And then the application finishes up in state 7, DealWithWonLostOrNewRound, from whence it goes nowhere... as you can see from the code!
DealWithWonLostOrNewRound is going to do the things that lines 490-620 of the original dealt with.
At this point, the application has an edit box, eUserInput and a memo, meShowUser. When it is run, various messages appear in the memo. The edit box is, so far, irrelevant.
Those are the essentials. There is also a panel, on which certain things... the value in bState, for instance, are displayed. They are not "necessary" to the game, but may be helpful in debugging the code as we build the game.
(*Wumpus: "Round" will be the playing of a turn... hearing where you are deciding what to do hearing result "Game" will be a number of "rounds"... from the first, through to the one where you win or lose "Session" will be from when app starts to when it closes... it may consist of one or more games. *) procedure Tlt3n_wumpus_f1.FormCreate(Sender: TObject); begin lt3n_wumpus_f1.caption:= kPrgmName+' vers:'+kVers; meShowUser.lines.clear; Timer1.interval:=1500; eUserInput.text:=''; Panel1.caption:=''; laTermsDone.caption:=''; FillSState;//Set up strings describing the states MakeState(0);//Initialize bState, display laTxtUserInputUse.caption:='Press the Enter key after'+ chr(13)+'any answers are entered,'+ chr(13)+'to say "answer complete"'; boInputReceived:=false;//Initialize boFirstVisitToStatesHandler:=true; end; procedure Tlt3n_wumpus_f1.FillSState; var bTmp:byte; begin //Establish default values... for bTmp:=0 to kStatesAllowed do sState[bTmp]:='State '+inttostr(bTmp)+' not assigned yet'; //Over-write some of them... //sState[]:=''; sState[0]:='GettingStarted-Session'; sState[1]:='GettingStarted-Game'; sState[2]:='GettingStarted-Round'; sState[3]:='FakeMostOfWumpus'; sState[7]:='DealWithWonLostOrNewRound'; end;//FillSState; procedure Tlt3n_wumpus_f1.eUserInputEditingDone(Sender: TObject); //If you came to this code from one of my earlier essays on // using states, you may be surprised that this isn't more // complex. Previously, the input HAD to be a number, for // the purposes of the rest of the application. In Wumpus // that is not the case. ANY input is acceptable... at this // point in the code. (It may have other requirements imposed // on it elsewhere, later.) begin boInputReceived:=true; eUserInput.text:='';//Clear the edit box for the next //problem. (Happily, this does not trigger an //OnInputEditingDone event.) end; procedure Tlt3n_wumpus_f1.Timer1Timer(Sender: TObject); procedure DoState0Things;//SR of Timer1Timer event handler. //State0: StartSession begin meShowUser.lines.add('Welcome to Wumpus!'); meShowUser.lines.add('-------'); meShowUser.lines.add('("Start Session" done)'); meShowUser.lines.add(''); MakeState(1); end;//DoState0Things procedure DoState1Things;//SR of Timer1Timer event handler. //State1 StartGame begin meShowUser.lines.add('("Start Game" done)'); meShowUser.lines.add(''); MakeState(2); end;//DoState1Things procedure DoState2Things;//SR of Timer1Timer event handler. //State1 StartRound begin meShowUser.lines.add('("Start Round" done)'); meShowUser.lines.add(''); MakeState(3); end;//DoState2Things procedure DoState3Things;//SR of Timer1Timer event handler. //State3: begin meShowUser.lines.add('Doing State3... which FOR NOW is'); meShowUser.lines.add('"Fake most of the application... and'); meShowUser.lines.add('go to state 7 after filling F with'); meShowUser.lines.add('-1,0, or 1 (lost, do another round, won'); meShowUser.lines.add(''); MakeState(7); end;//DoState3Things procedure DoState7Things;//SR of Timer1Timer event handler. //State7: Deal with end of round, where siF_inished holds //a value to say we've won, lost, or neither (go back, //do another round. (Codes 1,-1, and 0, respecitively.) begin if boFirstVisitToStatesHandler then begin boFirstVisitToStatesHandler:=false; meShowUser.lines.add('We reached the bit we need to'); meShowUser.lines.add('write next... When done, it'); meShowUser.lines.add('will act according to the value'); meShowUser.lines.add('in siF_inished. and then'); meShowUser.lines.add('set a new value in bState.'); meShowUser.lines.add(''); end;//if.... end; end;//DoState7Things (*Boilerplate... procedure DoStateThings;//SR of Timer1Timer event handler. begin end;//DoState2Things *) begin //main block of Timer1Timer //boInputReceived:=false;//restore to "waiting" case bState of 0:begin DoState0Things; end; 1:begin DoState1Things; end; 2:begin DoState2Things; end; 3:begin DoState3Things; end; 7:begin DoState7Things; end;//; allowed here, even though Else follows else begin showmessage('An un-programmed State was entered.18b17a'); end;//Handle default case end;//case end;//Timer1Timer procedure Tlt3n_wumpus_f1. MakeState(bNewState:byte); begin if (bState<>bNewState) then boFirstVisitToStatesHandler:=true; bState:=bNewState; laCurrentStateID.caption:=inttostr(bState); laTermsDone.caption:=inttostr(bTermsDone); laStateString.caption:='STATE: '+sState[bNewState]; end;//MakeState</pre>
That first fragment may not seem very impressive, but it gives us a working shell, which we will now proceed to make more complete.
The next stage is mostly writing an improved DoState3Things, and "fixing" odds and end connected with that. Previously, we made it so that only numbers would be accepted via eUserInput. The number entered was returned in the global "extUserEntered". (Non-numeric entries were just ignored. It was as if nothing had been entered.)
A tweak of the event handler now accepts anything, and returns it in "sUserEntered".
And that paved the way for DoState3Things to pass on to the next state a -1, 0 or 1 in siF_inished, depending NOT upon the things which will EVENTUALLY determines fills siF_inished, but depending simply on what we TELL the code to put there. Thus we can test WHAT IS DONE WITH the value in siF_inished easily, get that right (in isolation), and then move on to the next thing.
The code needed...
procedure DoState3Things;//SR of Timer1Timer event handler. //State3: begin meShowUser.lines.add('Doing State3... which FOR NOW is'); meShowUser.lines.add('"Fake most of the application... and'); meShowUser.lines.add('go to state 7 after filling F with'); meShowUser.lines.add('-1,0, or 1 (lost, do another round, won'); meShowUser.lines.add('+++++++++++++++++'); meShowUser.lines.add('Enter W, L or G for simulate "Won",'); meShowUser.lines.add(' simulate "Lost" or simulate "Go on,'); meShowUser.lines.add(' do another round".'); meShowUser.lines.add(''); if boInputReceived then begin sUserEntered:=UpperCase(sUserEntered);//Turns "y" to "Y" if (sUserEntered)='W' then begin siF_inished:=1; MakeState(7);; end;//user entered "W" if (sUserEntered)='L' then begin siF_inished:=-1; MakeState(7);; end;//user entered "L" if (sUserEntered)='G' then begin siF_inished:=0; MakeState(7);; end;//user entered "G" end;//if boInputReceived... end;//DoState3Things
You may have noticed something: This is not, perhaps, the most efficient way forward. But it is the most pedestrian, and thus a way forward that is easiest to understand. (If you wanted to be clever, you could leave setting siF_inished out, if you assume that ALL it does is decide where we go next. Such assumptions tend to come back to bite you someplace painful.
Previously, I called State 7 "DealWithWonLostOrNewRound". Now that we're getting down to some of the details, I have revised that name to "DealWithWonLostOrNewRound-490-620" (I added the line numbers at the end)... to make it easy to refer back to the BASIC code if I am puzzled by something in the Lazarus code dealing with... supposedly!... what lines 490-620 dealt with.
So far, we only have a shell....
We arrive into State7, DealWithWonLostOrNewRound-490-620, with something in siF_inished... but we don't, yet, DO anything WITH it. So. We know the code it is meant to replace. What does that code do? Make the State7 "DoIt"... umm... do it!
Look at the BASIC. It doesn't look like this, but it (almost) boils down to it, if you think about it. (The following is neither Lazarus nor BASIC... but I think you'll follow it, anyway!
Detail: Unless I've missed something, siF_inished can only hold -1, 0 or +1. But the original coder, for good reasons, didn't want to say "If siF_inished=-1", so he used "If siF_inished<0", which of course INCLUDES the times it is -1, but deals with other things too. (End of Detail)
if siF_inished=0 then GOTO 390 if siF_inished<0 then begin Print "You lost" GOTO 560
So far, so good? Make sure you "see" that!
Lazarus, of course, "doesn't do" GOTO's like we might want. But where we see "GOTO"- some- other- bunch- of code, we should be thinking "Ah! Time to tidy up, prepare the way, and then change state. We'll come back to this, but first, the pseudo-code for the rest of what we need to provide for in the State7 do it. We need to deal with...
It's a little tedious... but not too bad if you take a deep breath, and are patient...
if siF_inished>0 then begin Print "You won" For J=1 to 6 L[J]=M[J] Next J: REM line 580 THEN SOME OTHER STUFF: Lines 590-620 end;//Was <0 (1, in fact- code for "you won")
The basic code doesn't use explicit "begins" and "ends", and so what is happening is a little hard to follow. But if you look at 590-620, you will see that a question is asked, and then you either GOTO 240 or to 360.
All WE need to do is to figure out, decide, prepare for, etc, what STATE we should program the application to move to in the lines replicating lines 590-620, and THIS bit is easy, too!
Before we worry about that: What is held in the arrays, L and M??
I want to know, because I want to give them better names, now. Also, because there is no $ in the name, we know they hold numbers... but for Lazarus we want to know what TYPE of numbers... integers, real... small, large?
Lines 240-270 make a good start. The fact that each array has only 6 elements is a help, in itself. The cave has more than 6 rooms.
What's this "FNA" thing? A function. See line 170. I would guess that FNA returns a random integer, in the range 1-20, inclusive. It may be something I will regret later, but I'm going to "go with" that.
But what are they used for?? This will help us choose a name. 240-340 would be a hint... if I could figure it out! Ah! Lines 200,210: It seems that L holds the *L*ocation of various things... My (experienced) guess? Based on 200,210, if L[1] holds 12, you are in cavern 12. (What L[2]-L[6] hold can be told from line 210, if I am right. (The fact that FNA returns 1-20 suggests there are 20 caverns. So I am renaming L bL_ocationOf. And "M" is, at the start, a second copy of L. Again, it is mostly a guess, but I am going to guess that it
Line 590 is the key to the "what is M" mystery. And the code does make it all a bit hard to see... but I'm pretty sure that the array M is used to keep track of what the starting position was of you, the Wumpus, the bats and the pits. If you want to play the game again, from the same starting conditions, they are taken from M. (Actually, and this is what make it less easy than it might have been: The code always puts L[] back to what it was before you started, and if you don't want to play the same game, what's in L (and M) is shuffled.)
So! I will call M[] bM_emoryOfWhereThingsWere. A someone "over the top" name, but we refer to it seldom!
Note how I have transcribed the REMs of the original program to the new one, just after the declaration of the two "where things are" arrays.
====
The good news: We are making progress. The bad? We are still trying to create the Lazarus for lines 490-620, and there's a problem.
Remember the bit I glossed over with "THEN SOME OTHER STUFF: Lines 590-620"?
Here are those lines again...
590 PRINT "Same set-up? (Y/N)"; 600 INPUT I$ 610 IF I$<>"Y" THEN 240 620 GOTO 360
The idea there isn't hard, I hope? The user is asked a question, and what happens next depends on the answer.
The "bad news" is that we don't want to be "stuck" here, waiting for the answer. The application is meant to run through all of "the stuff" in the code that handles the timeout of the timer very quickly. Waiting for an answer from the humnan does NOT come under the heading of "happen quickly".
Happily, there is an answer.
The "stuff" that has to happen never leads BACK to other things inside state 7, "DealWithWonLostOrNewRound-490-620"
In a sense, we are "done" with state 7 when we get to 590. So, in the conversion, we can just create another state, and, just before 590 in DealWithWonLostOrNewRound-490-620 we say: "Finish the DoIt for that, set things up so that the next time we pass though the Timer1 timeout event handler, we will be in the state for 590-620.
NOT hard... really... to START doing that... Or start merely gets us to State7 at the right time, for our "skeleton" of Wumpus, and then, always, goes on to State6, where it stops and stays. When we are done, State7 will not always lead to state 6. AND when we are done, the application will not stop and stay in State6 if it enters it. But we have to start somewhere, and this is where we are starting. With what I just said. We will then "fill in the State7 and State6 DoIts, so that they DO the things that are meant to happen at those stages in the code. And then we will continue the process, turning our attention to other places that are currently only "present" with "placeholders".
The START of building the handlers for State7 and State6...
procedure DoState6Things;//SR of Timer1Timer event handler. //State6 DealWith590-620-part of restart begin if boFirstVisitToStatesHandler then begin boFirstVisitToStatesHandler:=false; meShowUser.lines.add('(Deal with 590-620)'); meShowUser.lines.add(''); MakeState(6);//Tmp... this will NOT be the next //state in the final code. end;//if boFirstVisit.... end;//DoState6Things procedure DoState7Things;//SR of Timer1Timer event handler. //State7: Deal with end of round, where F_inished holds //a value to say we've won, lost, or neither (go back, //do another round. (Codes 1,-1, and 0, respectively.) //Takes care of what lines 490-620 of original dealt with. //Actually... MOSTLY takes care of that... although if it //gets to what was in 590, we go to a different state to //take care of those matters. But if we DO get to what //was in 590, and we change to a state that deals with //590-620's code, we don't need to remember any of the //things we were doing in this state (State7), when we //are done with the state for 590-620. So! Easy-peasy! begin if boFirstVisitToStatesHandler then begin boFirstVisitToStatesHandler:=false; meShowUser.lines.add('We reached the bit we need to'); meShowUser.lines.add('write next... When done, it'); meShowUser.lines.add('will act according to the value'); meShowUser.lines.add('in F_inished. and then'); meShowUser.lines.add('set a new value in bState.'); meShowUser.lines.add(''); end;//if boFirstVisit.... MakeState(6);//Tmp and crude... will need making fancier end;//DoState7Things
====================
The above may seem "sad"... but it isn't! It is a Solid Foundation. And our "build" is under control, can progress without worrying that anything is getting out of hand.
Take a step back. Where are we? We have a skeleton. It works.
Now let's look again at what was in 490-620.
With a bit of work, you can see that the only ways OUT of 490-620 are...
490... GOTO 390 610... GOTO 240 620... GOTO 360
We weren't "lucky" that there are so few connections to the rest of the BASIC code.
Part of the reason is that the original was well written. And the other part is that I chose well, when I said THIS chunk will "split off" well.
Anyway... we DO have quite a nice situation here...
We don't quite know what the purpose is of the code at 390, 240 or 360 is... but we can presume that we should change states at the point where the BASIC code goes to those lines. And because we are just getting going, we can "cheat" a little, and make three states that merely put "We've gone into the state for 390/ 240/ 360" on the screen, and then do nothing more... for now. They don't take us on to further states... for now. EVENTUALLY things will have to happen when we enter those other states, but for now we can concentrate on getting the code for State6 and 7 done.
Divide (well) and you WILL conquer.
Here's the more better State7. It, remember, takes care of....
if siF_inished=0 then GOTO 390 if siF_inished<0 then begin Print "You lost" GOTO 560 if siF_inished>0 then begin Print "You won" 560: For J=1 to 6 L[J]=M[J] Next J 590: THEN change to state 6 to do Lines 590-620 (Where there's a GOTO 240, a GOTO 360 end;//Was <0 (1, in fact- code for "you won")
(of course, before we can run the following, we need to write, and set up the CASE statement to deal with, States13, 14 and which stand in, for now, for GOTO 390, GOTO 560, and GOTO 240. (Not necessarily respectively!)
Here are the relevant bits, as revised...
procedure DoState6Things;//SR of Timer1Timer event handler. //State6 DealWith590-620-part of restart //Deals with acting according to the value //in F_inished. Then sets a new value in bState.'); var bCount:byte; begin if boInputReceived then boFirstVisitToStatesHandler:=true; if boFirstVisitToStatesHandler then begin boFirstVisitToStatesHandler:=false; meShowUser.lines.add('(Dealing with 590-620)'); if siF_inished=0 then begin meShowUser.lines.add('You decided to '+ 'go on... which isn''t coded for yet!'); meShowUser.lines.add(''); MakeState(13);//0: Go on end;//if siF_inished=0 if siF_inished<0 then begin meShowUser.lines.add('You lost. And for now '+ 'program enters a "sit there" loop.'); meShowUser.lines.add(''); MakeState(14);//-1: Lost end;//if siF_inished less than 0 //And now for the last possible case... //... but we still need the "if...", don't we. (Think!) if siF_inished>0 then begin meShowUser.lines.add('You Won. And for now '+ 'program'); meShowUser.lines.add('enters a "sit there" loop.'); meShowUser.lines.add('.. after dealing with ' + 'things that don''t'); meShowUser.lines.add('actually matter for '+ 'now, but WILL.'); meShowUser.lines.add(''); for bCount:=1 to 6 do begin //Put things back as they were //before game played, so it can, //if wanted, be re-played. bL_ocationOf[bCount]:= bM_emoryOfWhereThingsWere[bCount]; end;//for.. MakeState(15);//1: won end;//if siF_inished greater than 0 end;//if boFirstVisit.... end;//DoState6Things procedure DoState7Things;//SR of Timer1Timer event handler. //State7: Deal with end of round, where F_inished holds //a value to say we've won, lost, or neither (go back, //do another round. (Codes 1,-1, and 0, respectively.) //Takes care of what lines 490-620 of original dealt with. //Actually... MOSTLY takes care of that... although if it //gets to what was in 590, we go to a different state to //take care of those matters. But if we DO get to what //was in 590, and we change to a state that deals with //590-620's code, we don't need to remember any of the //things we were doing in this state (State7), when we //are done with the state for 590-620. So! Easy-peasy! begin if boFirstVisitToStatesHandler then begin boFirstVisitToStatesHandler:=false; meShowUser.lines.add('We reached the bit we need to'); meShowUser.lines.add('write next... When done, it'); meShowUser.lines.add('will act according to the value'); meShowUser.lines.add('in F_inished. and then'); meShowUser.lines.add('set a new value in bState.'); meShowUser.lines.add(''); end;//if boFirstVisit.... MakeState(6);//Tmp and crude... will need making fancier end;//DoState7Things //--- procedure DoState13Things;//SR of Timer1Timer event handler. begin if boFirstVisitToStatesHandler then begin boFirstVisitToStatesHandler:=false; meShowUser.lines.add('(Entered Do13Things)'); meShowUser.lines.add(''); //MakeState();//Tmp... this will NOT be // remmed out in the final code. end;//if boFirstVisit.... end;//DoStateThings procedure DoState14Things;//SR of Timer1Timer event handler. begin if boFirstVisitToStatesHandler then begin boFirstVisitToStatesHandler:=false; meShowUser.lines.add('(Entered Do14Things)'); meShowUser.lines.add(''); //MakeState();//Tmp... this will NOT be // remmed out in the final code. end;//if boFirstVisit.... end;//DoStateThings procedure DoState15Things;//SR of Timer1Timer event handler. begin if boFirstVisitToStatesHandler then begin boFirstVisitToStatesHandler:=false; meShowUser.lines.add('(Entered Do15Things)'); meShowUser.lines.add(''); //MakeState();//Tmp... this will NOT be // remmed out in the final code. end;//if boFirstVisit.... end;//DoStateThings begin //main block of Timer1Timer //boInputReceived:=false;//restore to "waiting" case bState of 0:begin DoState0Things; end; 1:begin DoState1Things; end; 2:begin DoState2Things; end; 3:begin DoState3Things; end; 6:begin DoState6Things end; 7:begin DoState6Things end; 13:begin DoState13Things;//go on end; 14:begin DoState14Things;//lost end; 15:begin DoState15Things;//won end;//; allowed here, even though Else follows else begin showmessage('An un-programmed State was entered.18b17a'); end;//Handle default case end;//case end;//Timer1Timer procedure Tlt3n_wumpus_f1.FillSState; var bTmp:byte; begin //Establish default values... for bTmp:=0 to kStatesAllowed do sState[bTmp]:='State '+inttostr(bTmp)+' not assigned yet'; //Over-write some of them... //sState[]:=''; sState[0]:='GettingStarted-Session'; sState[1]:='GettingStarted-Game'; sState[2]:='GettingStarted-Round'; sState[3]:='FakeMostOfWumpus'; sState[6]:='DealWith590-620-part of restart'; sState[7]:='DealWithWonLostOrNewRound-490-620'; sState[13]:='TmpEnterAt240'; sState[14]:='TmpEnterAt360'; sState[15]:='TmpEnterAt390'; end;//FillSState;
.. but I passionately hope that my uncertain belief that we have broken the back of what we need to do is not unfounded!
PS- Umm... That WAS the plan. And the program developed ALONG THOSE LINES... but what was, by the end, in which State didn't always reflect was WAS in a given State back in the early days of the program's evolution.
We have a working skeleton! We "just" have a "few bits" to fill in.
At the moment, the app starts. We TELL it whether "in the middle" the player won or lost or has only completed a round, and now needs to go back to do another. (If player has won or lost, he/she is allowed to start a new game, either with the same cavern system or a new one.)
BUT, instead of the "going back" that is supposed to happen, after one pass through the start and "playing a round", the application just grinds to a halt.
Not impressive! But a way-station along the road to what we want!
Good news! Don't (for now) worry about the stuff in the BASIC listing AFTER line 620... it is all sub-routines. Already neatly parceled up for dealing with later.
Think about what we have so far... Starts, gets to the end of the playing of a round, and then you go back... how far depending on whether you've won, lost, etc.
"Go back" is another way to say "change state". We've even got the "going to a state" in place. We just need to tweak that, so our application goes to a GOOD place, instead of the TEMPORARY place we've got it going so far.
Look again at the diagram. And look at the listings.
A few things... those on lines 10-239... only ever happen ONCE. They, probably, can go in the Lazarus FormCreate. (We'll move the stuff from lines 20-60 down, and put them to 341-348.)
A few things... those on lines 240-349 are a nice tidy block which we don't pass through very often... happen once during the first run, and after that, only if at line 610, which we only get to when someone has finished a game, and is wanting to start another, and the user said he/she wanted a DIFFERENT set-up.
This block is a candidate for a state!! Actually, it looks a lot like the code for what we called State1... "GettingStarted-Game".
I'm going to make State12 into something we reach after learning we've won or lost. It will say "Hope you enjoyed game.", and put two buttons on the screen, and handle clicks from them, restarting the game with or without a new setup being generated, depending on which button you click.
That's so "simple" that I won't do listings. I will tell you that the buttons concerned will be grayed out when irrelevant, and the captions on them will change from time to time!
We've established a solid beach-head!
We've created ways "in" to a game with or without a setup scramble. (When we do a setup scramble (State 12's job, replicating lines 240-359), we merely always just go straight on to State 11, which takes care of all the rest of the Start- A- Game stuff, the work of lines 360 onwards.
So where we were using State 1 to "fake" a game start, we can simply put the application into State 12. That will scramble the setup, and then follow on, as it always should, into the replacements for lines 360 onward... i.e. change the state to State 11.
At the moment, State 11 "does" far too much... it does everything from line 360 to line 489. But it doesn't really "do" it... it merely takes us from where we were when we went into it to a "mock up" of where we COULD be, leaving the block.
We will break State 11 into smaller pieces, using the same techniques as we've used before. More and more, the operation of our Lazarus program will be like the operation of the original. Less and less will we be supplying things like the answer to the current question "At the end of State 11, did you Win, Lose, or want to Go on with the round you are doing?".
Read through the next few paragraphs quickly, once. At the right point, you will be given a link to bring you back here. After a quick skim, work through carefully on a second (or third!) pass.
Also: If you start to puzzle over a mystery concerning the place of State14 in the scheme of things, put THAT worry on hold. (I'll deal with it later!)
In what follows, you will see, twice, instructions about "taking out" some code. I've put those instructions in italic. While, as a general explanation of how you do what this is an example of, "take out" makes sense, in this case there is nothing to take out!. Our "skeleton code" is SO skeletal that, in this case, a tweak of the messages is all the change needed to make State10 and 11 "do" all that they need to... for now.
So what was the point? The point is that we have built some finer detail into our skeleton. Once we have those states existing, and linked into the control mechanism of the "state machine", we can "fill" them with the code for the things that need to happen at those points in the playing of the game.
Our flow analysis diagram tells us that program control sometimes loops back to 390. Fine. We'll make that possible, as follows.
First we'll make a new state (10) that does EVERYTHING that State 11 currently does. Eventually, State 10 will be in charge of only 390-489, instead of the current 360-489, and State 11 will deal with 360-389.
(Note: One of the boundaries is at 389, and another is at 489. Just a coincidence... but a tedious one.)
Creating the starting point is mostly just a "copy/paste", and the creation of the "framework". The "framework" bits are...
sState[10]:='DealWith390-489'; (and change... sState[11]:= ... to... 'DealWith360-389' ... in anticipation of what it will be. And add... 10:begin DoState10Things; end; ... to the Case... statement, And add a skeletal... procedure DoState10Things;//SR of Timer1Timer event handler. //Do what 390-489 did begin end;//DoStateThings
It is into that skeletal DoState10Things that the copy... for now... of the old DoState11Things goes.
Now... go into DoState11Things... the State we're going to make into 'DealWith360-389', the first part of what State11 is doing at the moment. Put a MakeState(10); at the end of it. (Because of where we "broke" the old into states, happily, State 11 always goes on to State10.) Take out most of what was in State 11 before we started making this split. (Remember: That's what you'd GENERALLY do here, when "splitting" a State. In this instance, there's little to take out.) Tidy the rems to reflect the new reality.
Do a similar exercise with the current, work- in- progress code for State 10, but this time you are taking out the stuff that did what lines 360-389 did. (Remember: That's what you'd GENERALLY do here, when "splitting" a State. In this instance, there's little to take out.)
Now go back to where I said "skim first", if this is the first time you've got to here!
A little while ago, I said "If you start to puzzle over a mystery concerning the place of State14 in the scheme of things, put THAT worry on hold."
I'd done the "split State 11 into two States" work, and got the typos out, got it running, when I saw something unwelcome:
....create a a NEW setup, along the way to game restart. CODE EQUIV to 240-359 MUST GO HERE! (Entered Do11Things) Will one day start game, do 360-389. New game, using same setup as before, unless the setup has just been scrambled. (Entered Do10Things) Will one day start game, do 390-489. New game, using same setup as before, unless the setup has just been scrambled. ***(Entered DealWith380- (a Round)) ***Will one day will do heart of session Doing State3... which FOR NOW is "Fake most of the application...
(I've marked the offending two lines with "***"s, which were not present on my screen.)
A quick search of the code for "DealWith380" showed that this was not the disaster I feared.
Long ago, at the start of this, I created State 14, a "stand in" for "Start Round".
Much later in the process, I inadvertently created another stand in for the start of "Start Round"... State 10.
In the rems for one I speak of it doing what lines 380- did, in the other I speak of it doing what 390- did. If you check the code, you will see that 380-389 holds only a "REM". So whether you "start starting a round" at 380 or 390 makes little difference. (If you get to 390 having started at 370, you WILL "do" 380... but it does nothing. However, elsewhere in the code, e.g. 490, we have GOTO 390, so 390 "looked" important. Sigh.
ANYWAY... it is just a case of duplication.
When you start at the beginning, at the moment, you go through the states as follows:
It is at State 6 that things get interesting! From 6, we can go to State 14 or, State 7!
From 14, we would go to 3.
From 7, we would go to 11.
IN EITHER CASE, we now go back around the loop again. If we go from 6 to 3, we don't go back very far, but we DO go back. If we go from 6 to 7, we go back further, but still we are going to repeat things we've done before.
This is not a problem! This looping is normal, and at the heart of how computer programs are powerful.
(Note, by the way, that the scheme above will be modified before we are done. The scheme isn't exactly as above, even in the half done conversion of Wumpus.)
Even though I have fallen to my usual sloppiness, and the sState[x]:= part of the program isn't entirely up to date, a glance at that (after a little tidying) soon turns up the place the duplication arose...
sState[10]:='DealWith390-489'; sState[14]:='DealWith380- (a Round)'
Remember- our look at the DoIts a moment ago said that from State 10 we go to State 14. But look at the descriptions of what they are supposed to do!
From 14, we go to State 3: 'FakeMostOfWumpus'
What's in 14, present state?
meShowUser.lines.add('(Entered DealWith380- (a Round))'); meShowUser.lines.add('Will one day will do heart of session'); meShowUser.lines.add(''); MakeState(3);
Yep! That can go!
Revise 10 to send us straight to 3. Rem out 14... it isn't called from anywhere else.
(The review also threw up the fact that the old State 2 was written out at some stage, but the code was still lingering. Remmed it, and connections to it, out. Such things are "okay"... a program grows in stages. Sometimes "stuff" that was useful in the early stages DOES get written out as you go along. (Though it is best to rem things out when you write them out!))
Ah ha!... no sooner than I had that behind me, and I went looking for "what next", thought for a bit that State 10 was another superfluous duplication.
In fact, it was merely my documentation that was poor. State 10, as it was already saying, was a placeholder for 390-489, created early in the conversion project. (The various bold digits are an attempt to help you notice when something is 390, when it is 490. (or 389, 489.) What a pity that two such similar numbers should both be on boundaries. Oh well.)
State 3, when I was at this stage of the project, was described as "faking" most of the application... i.e. the heart... which before I thought carefully, made me think it was another stand in for 390-489.
In fact, State 3 fakes things that will have happened in the code running up to 490, the code executed during State 10, once the final State 10 code is in place to fully implement State 10. (We pass from State 10 to State 3... at the moment. In State 3 we give siF_inished a value, "by hand". The program should be generating that value, in State 10.) From State 3 we pass to State 6.
When the code for State 10 is finished, we will simply change the MakeState(3) at the end of that to MakeState(6). The code relating to State 3 will be redundant then.
So! We DO need State 10. It and State 3 do not duplicate efforts, the way the two we cut down to one a moment ago were doing. BAD DOCUMENTATION wasted my time. If I'd written what each State was responsible for more clearly while I was creating them the issues would not have arisen.
Earlier I mentioned that I wasn't entirely happy with my flow analysis diagram. The poor section covered the code from 370-500, inclusive.
I've done a new, more careful diagram. (It appears just a little further down the page.)
It threw up a little detail, something that can... but must... be dealt with by adding another State, but one that merely puts "Hunt the Wumpus" into the memo. I.e. a state we pass through, being there for just a cycle, a state to execute line 375 of the original program.
That new state goes "in" State 11.
We will split State 11 into a "new" State 11... which will be the first part of the old... and a new State, State 16. (Don't worry if you see the "but..!" element in that... I'll come to it.)(Also- We have some old States which have become redundant, but I am reluctant to recycle their names, because scraps of documentation may linger. If I call the new State, say, State 4, and see a reference to the old 4, will I realize that's an old reference??)
So... 11 will be split into a new 11 and a 16. And an entirely new 17 will be created to deal with...
meShowUser.lines.add('HuntTheWumpus');
At the end of the new State 11: MakeState(17);
At the end of State 17: MakeState(16);
At the end of State 16: MakeState(... whatever it was at the end of the old State 11... 10, as it happens.
See how well designed, well documented programs are well behaved when you work on them?
I was a bit silly to be so "rigorous" in my approach to hiving off the bit that I put into the "middle" of the old 11. The bit I put into 17... the "middle bit"... was actually a few lines from the END of 11! There's nothing to put in 16, the state that was created to hold "the stuff from 11 from after the stuff we put into 17". Ah well. What I did- link the new 11... the first part of the old 11... to the new 17. And then 17 linked to 16... the "bottom" of the old 11. And then 16 linked to the place the original 11 linked to.
Because there's no need 16, we simply change the MakeState at the end of 17 point to what 16 (and the old 11) pointed to, and write out all references to 16. Sigh.
There were some "bad bits" in the above! The THEORY was fine, but I fluffed some details. And I am NOT re-writing it to reflect the details that I've now ironed out in the sourcecode.
A free downloadable zip archive of the of the application's sourcecode at this point in it's development is available, if you want to dig into the details, or maybe finish the job of conversion!
We've brought the new application a long way forward. We have a sensible "skeleton", for the most part. Think of it as a sketch of the finished application. Every bit will need "filling in". Most will be quite easy. Getting the flow arranged was the hard part.
There is one area that is NOT well "sketched" yet.
We had to start somewhere, and I started at the "outside ends". The parts of the code that start a session, and start a game. And the part that "wraps things up".
However, at the moment, our State 10 needs a LOT of work. In that one state we're taking care of everything that happened in lines 380-489 of the original program. They are the "heart" of the program. A lot goes on there.
I hope you won't be surprised to learn that what is currently "State 10" will be broken up into many smaller pieces?
A long time ago, I presented a diagram, analyzing the flow of the application's execution. And I mentioned that the analysis at the heart of that had flaws.
Here's a new attempt to analyze what's happening due to lines 370 to 490.
The box marked "S17" in green is our existing State 17, which has MakeState(10) at it's end, at the moment.
We're going to have to re-engineer the state machine. We need to add States 20 to 23. (And some of those will, in due course, be transformed to smaller pieces, too.) Sated 20-23 will replace the current State 10.
I hope, after looking at the diagram to the right, that you would expect the current State 10 to have MakeState(3) at it's end?
State 3 is a little unusual. Once we've been in State 3 once, we may pass thorough its code many times. It doesn't always execute a MakeState. It only does the MakeState change when, on the program visiting State 3 again, there is input from the user. Once that input has been provided, State 6 become the machine's state.
There are two arrows leading into many of the states ' boxes. In the original program, there was a "GOTO 480". In the adaptation, State 22 is going to "take care" of what lines *470* and 480 did. References to State 22 speak of lines 470-489. But there's no "GOTO 470" in the original. I hope my attempt to remove a potential source of confusion hasn't been more confusing that the original situation?
State S3/ S6: In the original program, there are GOTO's to 490 and to 500. In creating a State Machine equivalent, this could have been a (minor) nuisance.
However: Look at line 490. It says "IF F=0 then...".
Careful study of all the times in the original program that there is a GOTO 500 reveals that F would never be zero at the times those GOTOs were arising. Thus, we can use GOTO 490 both for this program's GOTO 490s, AND for its GOTO 500's... removing the need for the nuisance, regardless of the fact it wouldn't have been huge.
In State 22, we have lines 450 and 460. If F=0 at that point, we switch to State S3. If it isn't, we switch to State 22. F will have acquired a value during GOSUB 3000.
State 21 sometimes switches the state machine to state 22, sometimes to state 23... and sometimes it leaves the state set to 21.
It leaves the state set to 21 until the application has had some input from the human user. That "stay in state" is indicated by the dashed line at the right side of the box delineating the state. Note that the application doesn't "stay in the state", in a general sense. It is only "in" the state briefly. Fairly often, admittedly. (Every time the Timer times out.) But to play nicely on a multi-tasking operating system, we can't enter a loop like...
(Pseudo code...) Repeat look for input Until input seen
Line 420 either says "GOTO O of 440, 480", or it says "GOTO O of 440, 480, 410".
Perhaps puzzling?
First of all, the programmer did something dreadful: He/ she used the letter O ("oh") for a variable. (Don't use O... it is too much like "zero".
We'll change that variable's name to siO_ption, as I think "Option" was the word the original programmer had in mind when he chose "O" as the variable name. At this point in the program's use, the user if making a choice. He has two options: Move, or try to shoot the Wumpus. In the subroutine at 2500, he's given the chance to say which he wants, and the appropriate value is put into "O"... which we are calling siO_ption. (Which may be harder to read, but you'll never confuse it with "zero", and WHY "Oh" will not need any thought. (si at the start, to say that this is a variable of type "shortint".))
Yes, fine, but still: What does 420 mean?
"GOTO O of ..." was BASIC's idea of what we do with the CASE statement in Lazarus.
In the specific instance of line 420, if siO_ption is holding a 1, the program will go to line 400. If siO_ption is holding a 2, the program will go to line 480. And if it is holding anything else, it will go to line 410... which gives the user another chance to tell the program what he/she wants to go. On the first attempt, he gave it something that was of no use to the program. The program wants 1 or 2.
You can see all of that for yourself, if you look into the code at 2500. The REMs help enormously, of course... particularly the ones at 400, 430 and 470. PUT REMs IN *your* CODE! They are a Good Thing.
THIS TUT IS BEING WRITTEN 19 Nov 18.... I'm afraid that's all there is for the moment... I'm busy writing that code for you just now! If you see this message 21 Nov 18, please write, cite "lt3n-convert-old-basic.htm", and COMPLAIN!
====================
The following is, alas, not 100% accurate... treat it as a starting point, and an indication of how you go about reverse-engineering an existing BASIC program, to tease out the flow control, the places where things happen, where branches and jumps occur. It IS right, I believe, apart from the bit shaded yellow... and there's a second attempt at that part of the flow analysis in a diagram in the body of this tutorial, above.
The material on the right was my first attempt. The material in the cyan panel is derivative, an attempt to identify the "states" the program passes through during execution.
Search across all my sites with the Google search...
|
Page tested for compliance with INDUSTRY (not MS-only) standards, using the free, publicly accessible validator at validator.w3.org. Mostly passes. There were two "unknown attributes" in Google+ button code. Sigh.
....... P a g e . . . E n d s .....