Traditionally programming was taught by teaching people to break things down into logical, progressive steps. Sometimes this is so easy you don't consciously do it. For example in the section on user defined characters, we have to set the character for the alien before making him walk across the screen. It wouldn't be any use to do this the other way round. Designing a 'proper' program follows a similar line but on a much larger scale. So much so that it might be days before we actually get round to typing in any code. The general advice is to concentrate on the generalities first, the big picture, and then break these tasks into progressively smaller ones until they get so small the code just drops out of its own accord.
It's very easy to wave your hands in the air trying to make generalized gestures in the hope that everyone will see what you mean, like playing Charades. To avoid this, I intend to walk through the design of an actual program, explaining the steps as we go so you can apply them to your own creations. The process is often referred to as developing from the 'top down'. This is a common technique used by many programmers, not something I dreamed up after three nights of reading William Gibson novels. The reason many programmers use it is it's easily adapted to different languages and it gives good results, i.e. it works.
If we take user defined characters, we can see that these are very useful. What can be a pain is actually setting up the grid and doing the mental arithmetic to derive the necessary row totals. This looks like a good candidate for a program to me.
How do we start? There is a very loose language called pseudo-code whose exact definition is somewhat vague as everyone tends to develop their own. What it usually looks like is a mixture of standard English with the odd word of BASIC thrown in. What is represents is the logical sequence of events necessary to accomplish the task in hand.
We start by getting the big picture. By this I mean we break down the tasks our program has to deal with into large chunks each of which can be summed up in a single line of several words.
Something like this:
That's our basic program design. As you see, no specifics, everything is delegated to sub tasks. The next step is to take each of these sub tasks and refine them in a similar fashion. We'll leave the first two lines until later because at this early stage we have no idea what variables we need or how to initialize them. We start with Draw main screen. For this we need a rough idea of what our user will be presented with when the program is running. Sketch this on paper first, use squared paper if you want so you get a better idea of size. Here's what we're aiming for:
Declare global arrays and structures Initialization Draw main screen Draw character Draw cursor REPEAT Get user action Process user action UNTIL User chooses exit Shutdown END
+--------------------------------------------------+ |
| Character Designer | |
| ****************** | |
| | |
| Instructions | |
| Use arrow keys to move cursor | |
| Press space to toggle selected cell | |
| Press X or ESC to exit | |
| | |
| | |
| [][][][][][][][] 000 | |
| [][][][][][][][] 000 | |
| [][][][][][][][] 000 | |
| [][][][][][][][] 000 | |
| [][][][][][][][] 000 | |
| [][][][][][][][] 000 | |
| [][][][][][][][] 000 | |
| [][][][][][][][] 000 | |
| | |
| * * * * * * * * | |
| | |
| BASIC code to produce this character: | |
| | |
| VDU 23,240,000,000,000,000,000,000,000,000 | |
| | |
| | |
+--------------------------------------------------+ |
The top half of the screen is what I would term the main screen, it won't change much during the course of the program. The lower part is the grid, VDU codes and life-sized characters. This will change a lot, so the design must deal with this separately. The line of stars underneath the grid represents a line of the actual characters. One thing we can instantly see is that the VDU line is bigger than the 40 characters that MODE 6 gives up, so we'll need to select another mode to accommodate this. MODE 21 gives us 50 columns which should be fine. We now know enough to work out what Draw main screen should look like.
CLS was used instead of 'clear the screen' because it's obvious this is the required command.
Draw main screen Set background colour CLS PRINT Title PRINT help instructions END
Not much to do there, the next one promises to be a little more challenging: Draw character. This can be seen to consist of three separate parts: the grid, the actual character and the VDU codes. Keeping it general we get the following.
It's pretty obvious there's more work to be done here, but this is a first pass so we'll leave the detail until later. During the design, things often change, so there is no point getting too involved at this stage.
Draw character Draw grid Draw actual character Print VDU codes END
Next up: Draw cursor. This will be used to move the cursor around the grid. To make the cursor appear to move, we need to first erase it from its old position and redraw it in the new one. We can use a different colour to highlight the cursor as it travels round the grid, so we need to know whether the cell it's on is set or not.
Now we dive into the REPEAT loop and find Get user action. This will be responsible for intercepting key presses from the keyboard, filtering out the unwanted and translating needed ones into a code for the rest of the program to use. From our sketched screen, we can deduce that the keys we are interested in are the arrow keys, the space bar and X or x. Using ESC to exit is a built-in feature, so we don't need to deal with it as such. The program can sit and wait for a key to be pressed as all other actions depend on this. Once pressed we can decide if the key is useful or not and translate it to a code if it is.
Draw cursor Set to normal colour Erase cursor at old position Set to highlight colour Highlight cell at current position END
Lastly, for the moment, we have Process action. This will take the action from the previous routine and do something with it. The options are quite straightforward so we can write it without further ado.
Get user action REPEAT Wait for key press Translate key press into code if valid UNTIL valid key found END
Okay, that's our first pass, let's bring it all together so we can see it in one place.
Process action CASE Code OF WHEN Up: Move cursor up Draw cursor WHEN Right: Move cursor right Draw cursor WHEN Down: Move cursor down Draw cursor WHEN Left: Move cursor left Draw cursor WHEN Exit: Set global Exit flag ENDCASE END
If you were at all intimidated by the thought of writing a reasonable size program like this, I hope you can now see how we are beginning to split it up into the bite-sized pieces you've been used to working with so far.
Main program Declare global arrays and structures Initialization Draw main screen Draw character Draw cursor REPEAT Get user action Process user action UNTIL User chooses exit Shutdown END Draw main screen Set background colour CLS PRINT Title PRINT help instructions END Draw character Draw grid Draw actual character Print VDU codes END Draw cursor Set to normal colour Erase cursor at old position Set to highlight colour Highlight cell at current position END Get user action REPEAT Wait for key press Translate key press into code if valid UNTIL valid key found END Process action CASE Code OF WHEN Up: Move cursor up Draw cursor WHEN Right: Move cursor right Draw cursor WHEN Down: Move cursor down Draw cursor WHEN Left: Move cursor left Draw cursor WHEN Toggle:Toggle grid position WHEN Exit: Set global Exit flag ENDCASE END
Several of the routines look like they could use more refinement. These are Draw character and Process action.
Draw character has three lines that need thinking about. The first says 'Draw grid'. Drawing the grid includes the row totals at the end of each row too. What we need to do is decide if drawing the grid and calculating the row totals will need sufficient code to merit its own routine or will it go into this one without overloading it? By now it's becoming obvious that we're going to need an array to hold the grid. To draw the grid, we'll use one screen position for each cell, so all we need to do is go through each row and column printing the correct character as we go. As we are already embroiled in the grid, it makes sense to work out the row totals here as well. For each row, we need to set the total to zero, then, when we find a grid position that's 'on', add the value of that column to the total. If we have a value set to 128 and halve it for each column, we can work out the value for that column.
Personally, I think that fits comfortably in the existing routine, no need for a new one. Let's dissect the next line: Draw actual character. We have already found the values for each row in the previous lump of code so this is now quite easy.
Draw grid and calculate row totals for each row FOR each row Set column value to 128 FOR each column Set row total to 0 IF Grid at row column position = 0 THEN PRINT at position empty cell character ELSE PRINT at position filled cell character Set row total to row total + column value ENDIF Set column value to column value / 2 NEXT column PRINT at end of row row total NEXT row
Finally we need to print the totals out in a nice line so users can copy them into their programs.
Call VDU 23 with chr 240 and row totals PRINT in position character 240 eight times
So our entire routine looks like this:
PRINT in position "BASIC code to produce this character:" PRINT in position "VDU 23, 240"; FOR each row PRINT ",";total for row; NEXT row
Draw character Draw grid and calculate row totals for each row FOR each row Set column value to 128 FOR each column Set row total to 0 IF Grid at row column position = 0 THEN PRINT at position empty cell character ELSE PRINT at position filled cell character Set row total to row total + column value ENDIF Set column value to column value / 2 NEXT column PRINT at end of row row total NEXT row
Call VDU 23 with chr 240 and row totals PRINT in position character 240 eight times
See how we still kept the original task descriptions? These usually end up as REMs in the completed program as they tell the reader what each section is trying to achieve.
PRINT in position "BASIC code to produce this character:" PRINT in position "VDU 23, 240"; FOR each row PRINT ",";total for row; NEXT row END
Onto Process action. Two things here. The first one is a slight restructure. Each action involves redrawing the cursor, so maybe we should pull this out of each statement and move it to the end, saves space and typing. That's easy enough because we haven't written any code yet. The other is the use of move cursor. We have a choice again: make a separate routine or write code in each place. I chose the first option because then we can write a generic routine to handle all cursor movement. We can make a note of this and design the routine in a minute. Meanwhile, is there anything else? We haven't dealt with what happens when the space bar is pressed. In reality, this is not too much of a challenge:
All we need to do is update the grid, Draw character in the main program will sort the rest out. This is our revised Process action routine:
WHEN Toggle: Toggle grid position IF Grid at cursor position = 0 THEN Set Grid at cursor position to 1 ELSE Set Grid at cursor position to 0 ENDIF
After all this, we can return to our cursor movement routine. All that is needed here is to take the direction the cursor wants to move in and check that it is a valid position i.e. not off the end of the grid. If the position checks out move the cursor there. Before moving the cursor, we need to remember the previous position so Draw cursor can erase it before highlighting the new position.
Process action CASE Code OF WHEN Up: Move cursor up WHEN Right: Move cursor right WHEN Down: Move cursor down WHEN Left: Move cursor left WHEN Toggle:Toggle grid position IF Grid at cursor position = 0 THEN Set Grid at cursor position to 1 ELSE Set Grid at cursor position to 0 ENDIF WHEN Exit: Set global Exit flag ENDCASE Draw cursor END
Version 2 of our program design now looks like this:
Move cursor Save current position in old position CASE Direction OF WHEN Up: IF Cursor row > 1 THEN Decrease Cursor row by 1 WHEN Right: IF Cursor column < 8 THEN Increase Cursor column by 1 WHEN Down: IF Cursor row < 8 THEN Increase Cursor row by 1 WHEN Left: IF Cursor column > 1 THEN Decrease Cursor column by 1 ENDCASE END
Main program Declare global arrays and structures Initialization Draw main screen Draw character Draw cursor REPEAT Get user action Process user action UNTIL User chooses exit Shutdown END Draw main screen Set background colour CLS PRINT Title PRINT help instructions END Draw character Draw grid and calculate row totals for each row FOR each row Set column value to 128 FOR each column Set row total to 0 IF Grid at row column position = 0 THEN PRINT at position empty cell character ELSE PRINT at position filled cell character Set row total to row total + column value ENDIF Set column value to column value / 2 NEXT column PRINT at end of row row total NEXT row
Call VDU 23 with chr 240 and row totals Call PRINT in position character 240 eight times
Isn't cut and paste wonderful?
PRINT in position "BASIC code to produce this character:" PRINT in position "VDU 23, 240"; FOR each row PRINT ",";total for row; NEXT row END Draw cursor Set to normal colour Erase cursor at old position Set to highlight colour Highlight cell at current position END Get user action REPEAT Wait for key press Translate key press into code if valid UNTIL valid key found END Process action CASE Code OF WHEN Up: Move cursor up WHEN Right: Move cursor right WHEN Down: Move cursor down WHEN Left: Move cursor left WHEN Toggle:Toggle grid position IF Grid at cursor position = 0 THEN Set Grid at cursor position to 1 ELSE Set Grid at cursor position to 0 ENDIF WHEN Exit: Set global Exit flag ENDCASE Draw cursor END Move cursor Save current position in old position CASE Direction OF WHEN Up: IF Cursor row > 1 THEN Decrease Cursor row by 1 WHEN Right: IF Cursor column < 8 THEN Increase Cursor column by 1 WHEN Down: IF Cursor row < 8 THEN Increase Cursor row by 1 WHEN Left: IF Cursor column > 1 THEN Decrease Cursor column by 1 ENDCASE END
Finding the variables
The design has reached a point where you can start to 'see' the underlying code i.e. we've refined it enough. It's now time to invite the friends and family round so we can play 'hunt the variable'. At this point, I usually get a red pen and scribble on the pseudo-code. This is a bit difficult to do in Notepad or Internet Explorer, so we'll do a blow by blow analysis of each routine. Fortunately this will not take as long as the previous section. For each routine, we need to know the name, type and scope of each variable we find.
Looking through the main program, there are two variables that spring out. One is the flag that is used to Exit, the other is the choice of action from Get user action routine. Both are integers and by virtue of the fact that they are in the main program, global. Exit will need to be set up to false when the program is started, so this is a job for the initialization routine.
Draw main screen doesn't have any variables, it's all print statements. Easy!
Draw character is a bit more involved. We will need two local integers for the FOR loops, call them Row% and Col%. Another integer is needed for the value of each bit, call this ColValue%, again, no-one else needs to know about this, so it's local. The main character data itself is crying out to be held in a two dimensional array. We'll call this Grid% and it is global as other routines need access to it. Another array is needed to hold the totals for the rows, RowTotal%. This will be a single dimension and be integers. Again, we'll make this global.
Draw cursor seems to have four integers associated with the current and previous position of the cursor. It would be nice to keep all these together, so let's put them in a structure. The structure needs to be global as we know that the Move cursor routine uses it as well. All are integers and the initial position will be set in the initialization routine.
Get user action has one integer variable to hold the key press, Key%, and one integer to hold the return code, Code%. Both are local.
Process action can have the value for the action code passed to it. The Exit flag is global as previously described.
Move cursor manipulates the cursor structure as described in draw cursor.
There are several other details that need to be considered. For example, the position of each PRINT statement needs to be decided. We can take a rough guess at this based on the screen layout, but be warned, you never get it right first time round!
One thing we have not considered is how to represent the grid. Each position can be depicted by one character position. If it's filled we can use a solid block. If it's empty a space would seem to be the obvious choice. However, if we use a space, how do we know where the cursor is? We need a character for an empty cell as well. One with a single bit outline will suffice. So we need two user defined characters:
Where 'X' = filled and '.' = empty. Characters 241 and 242 will do fine for these. Setting them up is a job for the initialization routine.
XXXXXXXX 255 XXXXXXXX 255 XXXXXXXX 255 X......X 129 XXXXXXXX 255 X......X 129 XXXXXXXX 255 X......X 129 XXXXXXXX 255 X......X 129 XXXXXXXX 255 X......X 129 XXXXXXXX 255 X......X 129 XXXXXXXX 255 XXXXXXXX 255
Using this information, we can now write the initialization routine.
... and lastly, it's complement, Shutdown, whose job it is to do any tidying up before the program stops. We don't want to clear the screen here as the user might want to make a note of his new creation after the program has finished. We'll just turn the text cursor back on and move it to the bottom of the screen so it's not in the way.
Initialize Set graphics mode Setup user characters 240, 241 and 242 Setup cursor location Turn off text cursor END
Shutdown Re-enable text cursor Send cursor to bottom of screen END
At last we are in a position to write the code. As you have probably guessed by now, each task such as Draw character should be kept separate from the main body of the code. To achieve this, we use PROCs and FNs. I'm not going to give a line by line account of this as if you have followed all the above, there's nothing that should shock or astound you. Click the link for the complete program. I like to leave blank lines to make things easier to read. Also each task is separated by a line of stars and a little description, this makes it easier to find when scanning through the code. I know I said at the beginning that you should type all the code in by hand, please feel free to do this, but just in case you are itching to see what the finished creation looks like, here's a cut and paste friendly version. Click the link below then, if you're in Internet Explorer, click the Edit menu and Select All, Edit again and Copy. You can then paste the whole thing into the editor. If you have another browser, there will probably be a similar method lurking in there somewhere. Use Back on your browser to return here when you've looked at it.
Conclusion
And there is our program. Not too bad, was it? The problem with writing this down is it makes it appear like a completely linear process. It's not. Often the pseudo-code will go through a number of revisions before settling on the final version. Even then when allocating variables or writing code you will encounter situations that necessitate going back and rehashing something. Far from being a waste of time, it's the best way to develop. Why? Simply because the more you sit down and think about something, chewing over different methods of doing it, the more chance you stand of getting it right. If you had an important interview or appointment, you wouldn't trust to luck that you could find the way to your destination without planning a route. So how would you expect to write a decent program if you just roll up your sleeves and start typing? Should you do this, on having typed in your program it's all too easy to try and bodge round the bits that don't behave rather than retrace your steps and admit that you made a wrong decision early on. If you didn't plan, there are no steps to retrace and everywhere you turn is quicksand. If you did plan, chances are with a bit of experience you would have recognised the problems with your approach and thrown it out before it ever reached the coding stage. As I previously stated, none of the ideas here are new or mine. I have no vested interest in pushing this method except that I was taught it early in my career and have used it in PASCAL,C/C++, and VB/VBA. Oh, and BBC BASIC too. So have hundreds of other programmers. Use it. It works. (End of rant.)
Exercises
1) I'm sure you can see lots of improvements here, try adding two more
commands, one to completely clear the grid and one to fill it. You
could use C and F to do this. Notice how it is easy to code
modifications like this because the structure makes everything easy to
find.
2) It's nice to be able to reverse engineer characters too, given the
row totals. Modify the project to allow the user to enter a total for
the row he's currently on. Then redisplay the character.
CONTENTS |
CHAPTER 21 |