home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
RUN Flagazine Extra: Special 3
/
run-special-3.zip
/
CURSUS.ASC
< prev
next >
Wrap
Text File
|
1992-08-05
|
41KB
|
639 lines
DE GWBASIC INTERPRETER (5)
SUBROUTINES
In deze les gaan we uitvoerig in op subroutines. Daar zijn we hard aan toe
omdat we al een flink stuk van de lange weg achter ons hebben en het stadium
van de gevorderde GWBASIC-programmeur bereiken. Dat wil zeggen: alléén voor
diegenen die de voorgaande vier lessen hebben gevolgd en het geleerde met
zelf bedachte 'toepassingen' in praktijk brengen. Programmeren leren is een
beetje studeren maar heel veel zelf proberen en variëren op de thema's.
╔═════════════════════════╗ Hiernaast ziet u de anatomie van een sub-
║ SUBROUTINE ║ routine.
║ ║ Een subroutine is een programma IN een pro-
║ 10 .. ║ programma. Als ingebouwd programma kan de
║ 20 .. ║ subroutine overal en zo vaak als dat nodig
║┌─── 30 GOSUB 1000<──┐ ║ is, worden aangeroepen. Dat gebeurt met de
║│ 40 .. │ ║ opdracht: GOSUB xx waarin xx het regelnummer
║│ 50 .. │ ║ is waar de subroutine begint. De sprong naar
║│┌── 60 GOSUB 1000<─┐│ ║ dat regelnummer heeft hetzelfde effect als
║││ 70 .. ││ ║ een GOTO-instructie met dit verschil dat de
║││ ┌ 80 GOSUB 1000<┐││ ║ programmagang binnen de subroutine vroeg of
║││ │ 90 END │││ ║ laat opbotst tegen de instructie RETURN.
║└┴─┴>1000 REM │││ ║ Letterlijk betekent dit: Keer terug naar
║ 1010 .. │││ ║ waar je vandaan kwam! Dat gebeurt dan ook en
║ 1020 .. │││ ║ de afwerking van het hoofdprogramma wordt
║ 1030 .. │││ ║ na het subroutine-intermezzo hervat met de
║ 1040 RETURN ──┴┴┘ ║ instructie die na GOSUB xx komt. Dat kan een
╚═════════════════════════╝ volgend regelnummer zijn of een volgende
instructie die achter een dubbele punt staat zoals in: GOSUB 1000:CLS. In
dit geval wordt gebruikt gemaakt van multiple statements.
Enorme voordelen
Het gebruik van subroutines betekent goed (en slim) programmeerwerk.
Subroutines houden de listing kort, ze maken het programma sneller en ze
vergemakkelijken het werk van de programmeur.
Waarom? Wel, heel eenvoudig. Wie programmeert zal in zijn programma bepaalde
onderdelen opnemen die tijdens het gebruik van het programma geregeld
terugkomen en telkens hetzelfde zijn. In een interactief programma wordt
keer op keer om een stukje invoer gevraagd of wordt de gebruiker gevraagd
een bepaalde toets in te drukken om verder te gaan. Nu zul je als
programmeur toch wel gek zijn als je dat elke keer wanneer dat in de
voortgang van het programma aan de orde komt, weer opnieuw programmeert.
Onderin het scherm met knipperende letters de tekst plaatsen: 'Toets <Esc>
om verder te gaan', hoeft maar één keer in een subroutine-tje te worden
opgenomen en telkens wanneer de programmeur dat nodig vindt, roept hij de
subroutine aan.
Voorschriften
De GWBASIC-interpreter houdt een boekhouding bij van regelnummers. U weet
ongetwijfeld dat u in de directe modus, achter de Ok-prompt een opdracht
kunt invoeren. RENUM bijvoorbeeld, reorganiseert de regelnummers met stappen
of increments van 10. U kunt ook een nieuwe regelnummering invoeren die b.v.
begint bij 100 en stappen van 100 aanhoudt. De opdracht luidt dan: RENUM
100,100.
RENUM is een intelligente functie van de interpreter. Hij zorgt er tijdens
het hernummeren ook voor dat de consistentie van het programma intact
blijft. Achter GOTO en GOSUB staat altijd een regelnummer. U begrijpt dat
deze doorverwijzingen moeten meeveranderen als de integrale nummering wordt
veranderd. Diezelfde boekhouding van regelnummers is ook oorzaak van twee
foutmeldingen die bij het gebruik van subroutines kunnen optreden. Ze
luiden:
RETURN without GOSUB
en
GOSUB without RETURN.
U voelt 'm al zitten. Elke subroutine MOET worden afgesloten met de opdracht
RETURN. Vergeet u dit dan kunnen er twee dingen gebeuren: u krijgt de
foutmelding GOSUB without RETURN of er wordt aan de subroutine begonnen en
de interpreter zakt er letterlijk doorheen en gaat vrolijk verder met de
instructies die direct na de lekke subroutine komen. Met andere woorden: een
subroutine is alleen een subroutine als er een RETURN in staat.
In GWBASIC worden subroutines altijd aan het einde van het programma gezet.
Dit omdat subroutines aan de bovenkant open zijn. Zou een programma aan het
begin met een subroutine beginnen dat begint de interpreter er vrolijk aan
totdat hij de opdracht RETURN tegenkomt en zich afvraagt: 'Return? Hoezo?
Waar naar toe? De foutmelding: Return without Gosub is het gevolg.
Variabelen doorgeven
Als GWBASIC-programmeur moet u bij het ontwerpen van uw programma goed
boek houden over de subroutines die u gebruikt. Documenteer uw programma met
REM's of apostrofjes waarin u voor u zelf aantekeningen maakt met betrekking
tot de indeling van uw programma-flow en vooral over wat de subroutines
doen.
In de praktijk zitten onoplettende programmeurs heel vaak veranderingen aan
te brengen in de verkeerde subroutines. Bestudeer en doorgrond het programma
hieronder dat gebruik maakt van één subroutine. De subroutine tekent een
enkellijns òf een dubbellijns kader in het midden van het scherm. Om onder-
┌───────────────────────────────────────────┐ scheid te kunnen maken tussen
│ 100 CLS:KEY OFF │ enkel- en dubbellijns worden
│ 110 LOCATE 12,30 │ variabelen benoemd alvorens
│ 120 PRINT "Druk op de <Enter> toets" │ de sprong naar de subroutine
│ 130 HLB=218:HRB=191:HLO=192:HRO=217 │ vanaf regelnummer 500 wordt
│ 140 LH=196:LV=179 │ gemaakt. Deze variabelen heb-
│ 150 GOSUB 500 'Teken enkellijns kader │ ben herkenbare namen:
│ 160 CLS:LOCATE 12,30 │ HLB = Hoek Links Boven
│ 170 PRINT "Druk op de <Enter> toets" │ HRB = Hoek Rechts Boven
│ 180 HLB=201:HRB=187:HLO=200:HRO=188 │ HLO = Hoek Links Onder
│ 190 LH=205:LV=186 │ HRO = Hoek Rechts Onder
│ 200 GOSUB 500 'Teken dubbellijns kader │ LH = Lijn Horizontaal
│ 210 END │ LV = Lijn Verticaal
│ 500 REM Enkel- of dubbellijns kader │ De waarden die aan deze vari-
│ 510 LOCATE 10,28 │ abelen worden toegekend, cor-
│ 520 PRINT CHR$(HLB)STRING$(26,LH)CHR$(HRB)│ responderen met de ASCII-
│ 530 FOR A%=11 TO 13 │ waarden die u inmiddels hebt
│ 540 LOCATE A%,28 │ leren kennen en programmeren.
│ 550 PRINT CHR$(LV) │ Loop ze na en het zal u dui-
│ 560 LOCATE A%,55 │ lijk worden hoe vóór de eerste
│ 570 PRINT CHR$(LV) │ sprong de variabelen de vol-
│ 580 NEXT A% │ gende betekenissen krijgen:
│ 590 LOCATE 14,28 │ HLB = ┌
│ 600 PRINT CHR$(HLO)STRING$(26,LH)CHR$(HRO)│ HRB = ┐
│ 610 WHILE INKEY$<>CHR$(13):WEND │ HLO = └
│ 620 RETURN │ HRO = ┘
└───────────────────────────────────────────┘ LH = ─
LV = │
Uit deze ASCII-tekens kan een perfect enkellijns kader om de tekst: 'Druk op
de <Enter> toets' worden getekend. Dat vanaf regel 160 hetzelfde gebeurt
maar dan met ASCII-tekens die een dubbellijns kader tekenen, had u al
begrepen.
Praktisch gebruik
In de praktijk worden subroutines veelvuldig gebruikt. In de RUN-listings
van de GWBASIC-rubriek in het Algemeen Dagblad eindigen de laatste regels
bijna altijd op: RETURN. Daar zijn twee dwingende redenen voor. Subroutines
worden gebruikt om de listing zo kort mogelijk te houden (plaatsruimte is
duur in een landelijk dagblad, vooral op zaterdag). Ten tweede komen
subroutines de veelzijdigheid van een programma ten goede, vooral wanneer er
ook nog met variabele waarden wordt gewerkt.
Subroutines zijn ideaal voor het afhandelen van de 'huishoudelijke' taken
binnen een programma. Ik bedoel daarmee dat er gemakkelijk vaak weerkerende
dingen in worden ondergebracht als het invoeren van gegevens door de
gebruiker op het indrukken van een bepaalde toets om het programma verder te
laten gaan.
Toetsen afvangen
'Druk op een toets om verder te gaan'
'Toets <Enter>'
'Druk op <Esc> voor RUN Desktop'
'<Enter> is verder gaan - <Esc> is einde'
'Spiralen --------> 1
Driehoeken ------> 2
Bollen ----------> 3'
Dit zijn zo de 'kreten' die de programmeur gebruikt om zijn gebruiker iets
te laten doen. Direct nadat hij zo'n 'kreet' op het scherm heeft geplaatst,
stuurt hij zijn programma naar een subroutine die gaat staan wachten tot de
gebruiker doet wat hem wordt gevraagd. De subroutine reageert op de
intoetsing door iets te doen (het scherm wissen bijvoorbeeld of een
berekening uitvoeren) en geeft vervolgens de besturing over het programma
terug. De programmeur wéét dat hij de besturing direct na RETURN weer
terugkrijgt. In feite heeft hij naar het gebeuren in de subroutine geen
omkijken, zo lang het 'daar onderin' maar goed geprogrammeerd is.
Omdat in subroutines vaak intoetsingen van de gebruiker worden verwerkt, is
het zaak daar eens even bij stil te staan. Veel programmeurs maken zich hier
met een Jantje van Leiden af en hebben een zeer grote voorkeur voor:
'Druk op een toets'.
Waarom? Omdat een subroutinetje om dit af te handelen heel weinig
programmeerwerk betekent:
500 WHILE INKEY$="":WEND:RETURN
of
500 IF INKEY$="" THEN GOTO 500 ELSE RETURN
Letterlijk staat er: ZO LANG er niet op een toets wordt gedrukt, doe dan
niets. Of: INDIEN er niet op een toets worden gedrukt, GA NAAR hetzelfde
regelnummer. Een beetje programmeur houdt hier niet van. Het is namelijk niet
waar wat er staat. De gebruiker kan niet op 'any' toets drukken om het
programma te laten reageren. Op <Shift>, <Ctrl> en <Alt> wòrdt niet
gereageerd. Eleganter is aan bepaalde intoetsingen een vaste betekenis toe te
kennen. In RUN Flagazine wordt consequent de <Esc> gebruikt om terug te keren
naar de RUN Desktop.
Als u voor speciale intoetsingen kiest, moet u ze in de betreffende
subroutine ook netjes 'afvangen'. U kunt niet op het scherm zetten: 'Toets
<Enter>' en u er in de subroutine van af maken met een regeltje als in de
twee hierboven gegeven voorbeelden. Als de gebruiker ontdekt dat hij behalve
op <Enter> ook op de spatiebalk of een lettertoets kan drukken om hetzelfde
gedaan te krijgen, maakt dat op z'n minste een wat slordige indruk.
De <Enter> vangen we derhalve netjes af:
┌─────────────────────────────────────┐
Er staat letterlijk: ZOLANG er op │ 500 REM SUBROUTINE VANGT <Enter> │
een ANDERE toets wordt gedrukt dan │ 510 WHILE INKEY$ <> CHR$(13):WEND │
de <Enter>, DOE dan niets en blijf │ 510 RETURN │
naar het toetsenbord kijken. Wordt └─────────────────────────────────────┘
er wèl op <Enter> gedrukt, keer dan terug. Dit werkt lekker zo lang het om
slechts één bepaalde toets gaat. Heeft de gebruiker de keuze tussen
bijvoorbeeld de <Enter> en de <Esc> dan wordt het iets ingewikkelder. We
kunnen dan niet meer volstaan met
┌────────────────────────────────────────┐
het handige INKEY$ maar we zullen │ 500 REM SUBROUTINE VANGT <Enter>/<Esc> │
een variabele moeten introduceren │ 510 I$=INKEY$ │
om het onderscheid te kunnen aan- │ 520 WHILE I$<>CHR$(13) AND I$<>CHR$(27)│
geven. Gemakshalve wordt hiervoor │ 530 I$=INKEY$ │
steevast de stringvariabele I$ ge- │ 540 WEND │
gebruikt. In 510 wordt I$ aange- │ 550 IF I$=CHR$(13) THEN CLS:END │
wezen om naar het toetsenbord te │ 560 RETURN │
kijken. De ASCII-waarde van de in- └────────────────────────────────────────┘
toetsing komt in I$ te staan. In 520 kijken we wàt er wordt ingetoetst.
Zolang de ASCII-waarde van I$ niet die van <Enter> (CHR$(13)) is of van
<Esc>
(CHR$(27)), wordt I$ in 530 geleegd en met WEND op het toetsenbord gericht.
Eens moet de tijd komen dat de gebruiker in de gaten krijgt dat hij de keus
heeft uit twee toetsen. Toets hij òf <Enter> òf <Esc> in dan passeert het
programma de WEND in 540. Nu kunnen we kijken welke van de twee is
ingedrukt.
Is het <Enter> dan: scherm schoon en einde programma. In elk ander geval
(maar in ons geval kan het alleen nog maar de <Esc> zijn) wordt regel 550
gepasseerd en komt de subroutine zijn onvermijdelijke RETURN tegen.
Nog bewerkelijker wordt het afvangen van intoetsingen als het om een
keuzemenuutje gaat. Stel dat u uw gebruiker een menuutje voorschotelt voor
╔════════════════════════════╗ een hypotheekberekening zoals hiernaast is
║ U kunt kiezen uit: ║ afgebeeld. Wat verwachten we van de gebrui-
║ ║ ker? Simpel: dat hij 1, 2, 3, 4, 5, 6 òf 7
║ 1. Berekening over 1 jaar ║ indrukt en niets anders. We zullen deze ze-
║ 2. Berekening over 2 jaar ║ ven intoetsingen moeten afvangen. Gemakke-
║ 3. Berekening over 5 jaar ║ lijk zou zijn als we dat niet met een INKEY$
║ 4. Berekening over 10 jaar ║ doen maar met een INPUT of een LINEINPUT, ge-
║ 5. Berekening over 20 jaar ║ volgd door een pets op <Enter>. Dat doen we
║ 6. Berekening over 30 jaar ║ niet omdat we het de gebruiker zo gemakke-
║ 7. Einde programma ║ lijk mogelijk willen maken. Van ons hoeft hij
║ ║ niet op twee toetsen te drukken maar volstaat
║ Uw keus: 1,2,3,4,5,6 of 7 ║ alléén de druk op de gewenste cijfertoets.
╚════════════════════════════╝ Aan een INKEY$-constructies valt dan niet te
ontkomen. Ondergebracht in een subroutine zou dat er als volgt kunnen
uitzien:
┌─────────────────────────────────────────────────────────────────────────┐
│ 500 REM SUBROUTINE VANGT 1 T/M 7 │
│ 510 I$=INKEY$ │
│ 520 IF I$<>"1" AND I$<>"2" AND I$<>"3" AND I$<>"4" AND I$<>"5" AND I$ <>│
│ "6" AND I$<>"7" THEN BEEP:GOTO 510 │
│ 530 IF I$="1" THEN JAAR%=1:GOTO 600 │
│ 540 IF I$="2" THEN JAAR%=2:GOTO 600 │
│ 550 IF I$="3" THEN JAAR%=5:GOTO 600 │
│ 560 IF I$="4" THEN JAAR%=10:GOTO 600 │
│ 570 IF I$="5" THEN JAAR%=20:GOTO 600 │
│ 580 IF I$="6" THEN JAAR%=30:GOTO 600 │
│ 590 IF I$="7" THEN CLS:END │
│ 600 RETURN │
└─────────────────────────────────────────────────────────────────────────┘
Dat is heel wat programmeerwerk om het de gebruiker een ietsje gemakkelijker
te maken. Maar ja, klanten en computergebruikers zijn nu eenmaal koning en
daar doe je alles voor.
Nu we toch bezig zijn puntjes op de i te zetten; maak in uw subroutines voor
het afvangen van intoetsing óók altijd onderscheid tussen hoofd- en kleine
letters wanneer het om letterintoetsingen gaat. Wanneer we willen dat de
gebruiker een lettertoets indrukt, ga ╔═════════════════════╗
er dan niet automatisch van uit dat ║ Toets <I>nkoop ║
er op een kleine letter zal worden ║ <V>erkoop ║
gedrukt. Het kan best zijn dat die ║ <O>verzicht ║
gebruiker in zijn argeloosheid op ║ <S>tatistiek ║
de toets <Caps Lock> heeft gedrukt. ╚═════════════════════╝
Ook deze eventualiteit moet worden afgevangen en wel zodanig dat het niet
uitmaakt of de gebruiker nu op hoofdletters of op kleine letters drukt. En
weer komt het werk daarvoor op de nek van de programmeur terecht.
┌───────────────────────────────────────────────────────────────────────┐
│ 500 REM SUBROUTINE VANGT LETTERS I,V,O EN S │
│ 510 I$=INKEY$ │
│ 520 WHILE I$<>"I" AND I$<>"i" AND I$<>"V" AND I$<>"v" AND I$<>"O" AND │
│ I$<>"o" AND I$<>"S" AND I$<>"s" │
│ 530 I$=INKEY$ │
│ 540 WEND │
│ 550 IF I$="I" OR I$="i" THEN ... : GOTO 580 │
│ 560 IF I$="V" OR I$="v" THEN ... : GOTO 580 │
│ 570 IF I$="O" OR I$="o" THEN ... : GOTO 580 │
│ 570 IF i$="S" OR I$="s" THEN ... │
│ 580 RETURN │
└───────────────────────────────────────────────────────────────────────┘
Operatoren AND en OR
Stiekem hebben we in deze exercitie twee zogenoemde Booleaanse of logische
operatoren geïntroduceerd: AND en OR. De functie ligt voor de hand. Bij het
gebruik van AND in een IF/THEN-constructie moet aan beide voorwaarden worden
voldaan. Bij het gebruik van OR hoeft aan slechts één van de opgenomen
voorwaarden te worden voldaan.
Kijken we nu even naar het afvangen van de letters I,V,O en S dan zien we in
de WHILE/WEND-constructie in regel 520 dat alle voorwaarden in negatieve zin
moet worden voldaan. In gewone taal: ZOLANG de intoetsing in I$ géén I, i,
V, v, O, o, S EN s oplevert, maak I$ dan weer leeg en zet hem weer op het
toetsenbord.
Vanaf regel 550 zeggen we dat INDIEN de intoetsing een gevraagde hoofdletter
OF een gevraagde kleine letter oplevert DAN is aan een voorwaarde voldaan en
kunnen we het programma de maatregelen laten nemen die in overeenstemming
zijn met de wens van de gebruiker.
Tot slot
We hebben nu vijf lessen achter de rug. Heeft u ze allemaal gevolgd dan kunt
u al programmeren. U heeft de gereedschappen in huis om kleine
programmaatjes te maken zoals het maken van tafels of het geven van kleine
rekenopdrachtjes aan kinderen en vervolgens kijken of ze een sommetje goed of
fout hebben. Ook hebt u tekstBEwerking gehad.
Het wordt tijd om van uw kant ook eens iets te laten horen. We weten dan of
deze cursus nog zinvol is. Ik geef u daarom een opdrachtje:
┌─────────────────────┐
┌──────────────────────────┤ PROGRAMMEEROPDRACHT ├─────────────────────────┐
│ └─────────────────────┘ │
│ Maak een programmaatje waarin u een kind enkele zinnen op het scherm │
│ laat lezen. In elk zinnetje zet u een taalfout. Het kind moet de taal- │
│ fout ontdekken en het zinnetje zonder de fout overtypen. │
│ Controleer of de invoer correct is en laat uw programma daarop reageren. │
│ Beloon het kind als hij het goed doet en 'tik hem op de vingers' als hij │
│ het niet goed doet. Geef desnoods een rapportcijfer. │
└──────────────────────────────────────────────────────────────────────────┘
De GWBASIC Interpreter (6)
RANDOMIZERS
Iedere programmeur wil zijn programma's levend en verrassend maken door het
gebruik van randomizers of willekeurige getallenkiezers. Randomizers zijn
nuttig en onmisbaar bij toepassingen als: games, kunstmatige intelligentie,
statistiek en vooral bij simulaties (nabootsingen van de werkelijkheid).
Om tot verrassende elementen of 'puur toeval' in onze programma's te komen,
kunnen we niet volstaan met een beroep op de GWBASIC-functie: RND. De op-
dracht in de directe modus:
PRINT RND
geeft weliswaar steeds een ander resultaat: 'n schijnbaar willekeurig decimaal
getal onder de 10 maar als we RND nader bekijken in een klein onderzoekspro-
grammaatje als hieronder, blijkt er van puur toeval totaal geen sprake meer te
┌────────────────────────────────────────────┐ zijn. Elke RUN levert 'n lijst-
│ 10 REM Genereer 10 willkeurige getallen │ je op met telkens dezelfde ge-
│ 30 FOR A%=1 TO 10 │ tallen: 1,6,8,7,7,0,4,4,1 en 9.
│ 40 PRINT "Willekeurig getal nr.";A%;":"; │ De functie RND moet eenmalig
│ 50 PRINT INT(RND*10) │ worden voorafgegaan door het
│ 60 NEXT A% │ commando: RANDOMIZE.
│ Ok │ Met dit commando wordt intern de
│ run │ randomizer aangezet maar dit ge-
│ Willekeurig getal nr. 1 : 1 │ beurt pas wanneer daarvoor een
│ Willekeurig getal nr. 2 : 6 │ uitgangs- of zaagetal aanwezig
│ Willekeurig getal nr. 3 : 8 │ is. Het programmaatje hiernaast
│ Willekeurig getal nr. 4 : 7 │ kan worden uitgebreid met de re-
│ Willekeurig getal nr. 5 : 7 │ gel:
│ Willekeurig getal nr. 6 : 0 │ 15 RANDOMIZE
│ Willekeurig getal nr. 7 : 4 │ Als we dat doen zal de interpe-
│ Willekeurig getal nr. 8 : 4 │ ter in deze regel 15 de afwer-
│ Willekeurig getal nr. 9 : 1 │ king van het programma onderbre-
│ Willekeurig getal nr. 10 : 9 │ ken om de gebruiker een zaadge-
└────────────────────────────────────────────┘ tal te vragen. Deze vraag luidt:
Random number seed (-32768 to 32767)
Van de gebruiker wordt verwacht dat hij een getal invoert in het gegeven bereik
maar het getuigt natuurlijk van slecht programmeerwerk wanneer een programma
een argeloze gebruiker met een dergelijke vraag overvalt.
Als programmeurs kunnen we de interne randomizer zelf een zaadgetal geven.
Niets hoeft ons ervan te weerhouden om de nieuwe regel 15 alsdus uit te
breiden:
15 RANDOMIZE 25 ┌─────────────────────────────┐
Verscheidene RUN's op het aangepaste programma │ run │
geven elke keer weer het resultaat zoals hier- │ Willekeurig getal nr. 1 : 1 │
naast is weergegeven. We hebben nu wel een an- │ Willekeurig getal nr. 2 : 3 │
getallenreeks gegenereerd maar elke keer ver- │ Willekeurig getal nr. 3 : 1 │
schijnen de getallen in dezelfde volgorde. │ Willekeurig getal nr. 4 : 8 │
De oorzaak laat zich niet moeilijk raden. De │ Willekeurig getal nr. 5 : 1 │
volgorde is bepaald door de waarde 25 die we │ Willekeurig getal nr. 6 : 9 │
als zaadgetal aan RANDOMIZE hebben meegegeven │ Willekeurig getal nr. 7 : 6 │
in regel 15. Om tot puur toeval te komen moe- │ Willekeurig getal nr. 8 : 4 │
ten we als zaadgetal een willekeurig getal op- │ Willekeurig getal nr. 9 : 5 │
geven maar hoe doen we dat? Hoe laten we ons │ Willekeurig getal nr. 10 : 8│
programma een willekeurig zaadgetal bedenken │ Ok │
in een programma dat zelf willekeurige getal- └─────────────────────────────┘
len moet maken?
Er zijn twee manieren voor. De eenvoudigste en meest gebruikte is die welke
gebruik maakt van de microtimer. Met GWBASIC-commando:
PRINT TIMER
krijgen we de inhoud te zien van de interne klok die microseconden telt. Het
kwartskristal levert daarvoor de invoer. Dit tellertje is gestart op het mo-
moment dat we de PC aanzetten. ┌─────────────────────────────────┐
TIMER is een interne systeemvariabele die │ 10 REM BESTUDEER MICROTIMER │
voortdurend anders zal zijn als hij wordt │ 20 CLS:KEY OFF │
opgevraagd met: │ 30 LOCATE 12,25 │
PRINT TIMER │ 40 PRINT "Aanvangstijd:";TIMER │
We krijgen de waarde tot op twee cijfers │ 50 WHILE INKEY$="" │
achter de decimale komma nauwkeurig. Deze │ 60 LOCATE 13,38 │
nauwkeurigheid kunnen we beperken tot hele │ 70 PRINT TIMER │
getallen of integers met de toevoeging: │ 80 WEND │
INT. Dus: │ 90 LOCATE 13,25 │
PRINT INT(TIMER) │ 100 PRINT "Stoptijd :";TIMER │
levert een geheel getal op en in een loop- └─────────────────────────────────┘
je of kringloop geeft dit een bruikbaar secondentellertje. TIMER of INT(TIMER)
kunnen we bij uitstek gebruik als zaadgetal voor RANDOMIZE omdat TIMER op elk
moment een andere waarde zal opleveren. Breiden we het programma van de vorige
pagina uit met:
15 RANDOMIZE TIMER
dan krijgen we onze zin en zal het resulterende lijstje telkens anders zijn. In
plaats van TIMER wordt ook de negatieve waarde: -TIMER gebruikt. Dat maakt
allemaal niets uit omdat het gevraagde zaadgetal zich over een bereik van
negatieve en positieve getallen uitstrekt.
True (?) randomizers
Het klinkt misschien gek maar een digitale computer als de PC is niet in staat
echte willekeurige getallen te genereren. Wanneer we gebruik maken van 'wille-
keurig' gekozen zaadgetallen, krijgen we wat we willen, namelijk telkens een
andere waarde van RND() maar heel principieel beschouwd is het resulterende
random getal niet echt random. Het lijkt erop. Daarom wordt er in het computer-
jargon gesproken van 'true randomizers'.
Studenten in de edele kunst van het programmeren komen niet onder de opgave uit
een spelletje te maken dat een willekeurig geheim getal onder de 100 kiest en
de speler laat raden wat het is. Na invoer krijgt de speler te zien of zijn
ingevoerde getal goed is geraden of dat het ingevoerde getal te hoog of te laag
is gegokt.
U gaat dit hooglaag-spelletje nu ook maken maar met wat we in de zes lessen tot
nu toe hebben geleerd, mogen bepaalde eisen aan het programma worden gesteld.
Het programma mag geen invoer accepteren die kleiner is dan nul of groter dan
99.
Wanneer het programma vaststelt dat de speler het geheime getal heeft geraden,
geeft het tot slot weer hoeveel beurten de speler er over heeft gedaan.
┌────────────────────┐
┌─────────────────────────────┤ Opgave 1 ├──────────────────────────┐
│100 REM HOOG LAAG SPEL └────────────────────┘ │
│110 CLS:KEY OFF:RANDOMIZE TIMER │
│120 COMPUTERKEUS=INT(RND*100) │
│130 INPUT "Welk getal (0 - 99)";SPELERKEUS │
│140 BEURT = BEURT + 1 │
│150 IF SPELERKEUS < 0 OR SPELERKEUS > 99 THEN BEEP:BEURT = BEURT - 1:GOTO 130│
│160 IF SPELERKEUS = COMPUTERKEUS THEN 190 │
│170 IF SPELERKEUS < COMPUTERKEUS THEN 200 │
│180 IF SPELERKEUS > COMPUTERKEUS THEN 210 │
│190 PRINT "Geraden in";BEURT;"beurten":END │
│200 PRINT "Te laag gegokt":GOTO 130 │
│210 PRINT "Te hoog gegokt":GOTO 130 │
└─────────────────────────────────────────────────────────────────────────────┘
De hierboven staande uitwerking van de vingeroefening geeft aanleiding tot een
opmerking. Regel 150 is nogal lang geworden. Dat komt doordat lange numerieke
variabelenamen zijn gebruikt maar ook doordat er na THEN drie dingen aan de
voorwaarden moeten voldoen. ALS de situatie zich voordoet dat de invoer kleiner
is dan 0 OF de situatie zich voordoet dat de invoer groter is dan 100 DAN
moeten er dingen gebeuren die nu volgen: een beepsignaal om de speler tot
opletten te manen, de beurt wordt ongeldig verklaard en de beurtteller wordt
met 1 verminderd en de programma-afwerking springt terug naar regel 130.
Lange regels zijn lelijk in GWBASIC. Zelf probeer ik ze in de RUN-listings voor
het AD zo veel mogelijk te vermijden. Maar bezwijk nooit voor de verleiding om
de opdrachten die aan de IF moeten voldoen in een nieuwe regel te schrijven.
Als u dat doet, voldoen ze niet meer aan de voorwaarden en uw programma gaat
zich misdragen. Het doet niet meer wat u er van verwacht.
Randomizer in DEF FN
Het moet u inmiddels zijn opgevallen dat GWBASIC een kist vol gereedschappen is
waarvan u er al heel wat kunt hanteren voor het maken van programmaatjes. Als u
met RND() aan de gang gaat, kunt u vooral in combinatie met andere gereed-
schappen, tot zeer verrassende resultaten komen. Het beroemde programma
ELIZA bijvoorbeeld is sterk randomizer-afhankelijk. Het lijkt alsof je met dit
programma over koetjes en kalfjes kunt praten maar in werkelijkheid kiest ELIZA
random uit een lijst van tekststrings. In sommige gevallen worden delen van wat
de gebruiker invoert, verweven in de willekeurig gekozen antwoorden. In de
praktijk gaat dan ongeveer zo:
╔════════════════════════════════════════════════════════════════════════════╗
║ Hallo, Ik ben Eliza. Wat is je probleem? ║
║ -- Mijn naam is Nico. ║
║ Namen interesseren me niet. ║
║ -- Ik heb ruzie met mijn broer. ║
║ Vertel daar eens meer over. ║
║ -- Mijn broer is onhebbelijk. ║
║ Misschien ben jij wel onhebbelijk. ║
║ -- Ik houd van reizen. ║
║ Daar wil ik meer over weten. ║
║ -- Ik was laatst in de Belgische plaats Namen. ║
║ Namen interesseren me niet. ║
╚════════════════════════════════════════════════════════════════════════════╝
U voelt 'm al: Dit nepprogramma kiest willekeurig uit nietszeggende zinnetjes
om de conversatie gaande te houden als het geen zinsdelen kan vinden die het
kan verweven in quasi-intelligente antwoorden. Vaak worden antwoorden met
verweven zinsdelen uit de invoer totaal verkeerd gebruikt.
Bij het programmeren van deze en andere programma's die uitbundig van rando-
mizers gebruik maken, moet vele, vele malen de opdracht:
X=INT(RND*x)+1
worden gebruikt. Dat kan ook eenmalig gebeuren met een weinig gebruikte maar
zeer nuttige zelf te maken functie. GWBASIC voorziet in het zelf maken van
functies en het later aanroepen van die functies met: DEF FN en FN. Evenals
RANDOMIZE wordt een DEF FN bovenin de programmalijst geplaatst. De interpreter
hoeft er slechts één keer voorbij te komen.
Een DEF FN kan van alles zijn maar in ons geval kunnen we van te voren bepalen
dat deze 'user DEFined FuNction' een randomizer gaat bevatten. Wanneer we de
volgende zelf te maken function eenmalig en aan het begin van het programma
declareren:
DEF FN X=INT(RND*99)+1
dan kunnen we hem in het werkende programma naar hartelust aanroepen met:
FN X.
De variabele X zal dan elke keer een willekeurig getal tussen 0 en 99 bevatten.
U kunt dit niet in de directe modus van GWBASIC uitproberen. Een DEF FN in de
directe modus geeft als foutmelding: Illegal direct. Daarom een piepklein
programmaatje om dit eens nader te bekijken:
┌───────────────────────────┐
│ 10 RANDOMIZE -TIMER │ Tik deze oefening in GWBASIC en geef RUN. U
│ 20 DEF FN X=INT(RND*99)+1 │ krijgt kolomsgewijs net zo veel willekeurig ge-
│ 30 WHILE INKEY$="" │ kozen getallen onder de 100 als u maar hebben
│ 40 PRINT FN X, │ wilt. Voor verdeling in kolommen zorgt de kom-
│ 50 WEND │ ma die in regel 30 na X als scheidingsteken fun-
└───────────────────────────┘ geert. Het genereren van de willekeurige getal-
len houdt pas op wanneer u op een toets drukt. Dit is het gevolg van de
WHILE/WEND die op een intoetsing wacht. Tussen de dubbele aanhalingstekens in
regel 20 staat niets; geen spatie dus! Als gevorderde kunt u toch lezen wat er
staat? ZOLANG (WHILE) er geen toets ("") wordt ingedrukt, druk het in DEF FN
gedeclareerde random getal (FN X) af (WEND).
Op de niet altijd even simpele maar toch wel erg machtige DEF FN komen we
verderop in de cursus nog uitgebreid terug.
Randomizers in ON x GOTO
Tussen neus en lippen door leren we nu even een handig techniekje dat door
velen vaak wordt gebruikt in een menugestuurd programma. Veel programma's
beginnen met een overzichtelijk menu van opties of keuzemogelijkheden.
Bijvoorbeeld:
┌───────────┐
╔═════════╡ HOOFDMENU ╞═════════╗
║ └───────────┘ ║
║ 1 - Zoeken op trefwoorden ║
║ 2 - Trefwoorden sorteren ║
║ 3 - Documenten invoeren ║
║ 4 - Systeemoverzicht ║
║ 5 - Statistiek ║
║ 6 - Einde programma ║
■═══════════════════════════════■
│ U kiest: 1, 2, 3, 4, 5 of 6 │
└───────────────────────────────┘
Als dit menu op het scherm staat verwacht de programmeur dat de gebruiker nu
de cijfertoetsen 1 tot en met 6 gaat indrukken. Alle andere intoetsingen worden
afgevangen. Het programma ontvangt de intoetsing en daarmee de keuze van de
gebruiker in een numerieke variabele uit, bijvoorbeeld:
400 INPUT K
410 IF K<1 AND K>7 THEN BEEP:GOTO 400
Wanneer in de variabele K een geldige menukeuze staat, gaat het programma
verder. Nu moet de programmeur zorgen dat het programma wegspringt naar het
deel in het programma waar de bestelde functie begint. Hij heeft zijn programma
netjes gestructureerd opgezet. Zoeken op trefwoorden begint op regel 1000,
Trefwoorden sorteren op regel 2000, Documenten invoeren op 3000,
Systeemoverzicht op 4000, Statistiek op 5000 en Einde programma op 6000.
De programmeur mag nu afzien van een constructie waarin komt te staan:
IF K=1 THEN GOTO 1000
IF K=2 THEN GOTO 2000
...enzovoort. In plaats daarvan beschikt hij over de functie:
ON [variabelenaam] GOTO [regelnummer].
De verkeersregeling gaat na regel 410 dan als volgt in zijn werk:
420 ON K GOTO 1000, 2000, 3000, 4000, 5000, 6000
In een programma dat de schijn wil ophouden dat het over iets beschikt dat doet
denken aan de menselijke vrije wil, kunnen we de ON x GOTO constructie
natuurlijk ook gebruiken met een randomizer. Stel dat in een simulatieprogramma
in 'willekeurige' volgorde continue bepaalde delen van het programma moeten
worden afgewerkt. Laten we aannemen dat die geroepen stukken netjes zijn onder-
gebracht in delen. Schematisch geprogrammeerd ziet dat er zo uit:
We hebben gebruik gemaakt van DEF FN ┌───────────────────────────────────────┐
functie in regel 20 om in regel 30 │ 10 CLS:KEY OFF:RANDOMIZE TIMER │
uit FN X steeds een andere waarde │ 20 DEF FN X=INT(RND*5)+1 │
voor de numerieke variabele X te ver- │ 30 ON FN X GOTO 60, 70, 80, 90, 100 │
krijgen. De ON in regel 30 doet braaf │ 40 PRINT X │
wat we willen. Omdat in X telkens een │ 50 REM Hieronder de 5 delen │
1, 2, 3, 4 of 5 komt te staan, wordt │ 60 PRINT "Dit is deel 1":GOTO 110 │
de programmabesturing telkens op een │ 70 PRINT "Dit is deel 2":GOTO 110 │
ander punt in het programma voortge- │ 80 PRINT "Dit is deel 3":GOTO 110 │
zet. We kunnen dat op twee manieren │ 90 PRINT "Dit is deel 4":GOTO 110 │
controleren. In regel 40 wordt X ter │ 100 PRINT "Dit is deel 5 │
controle op het scherm gezet en in de │ 110 PRINT "Toets voor vervolg" │
programmadelen wordt weergegeven om │ 120 WHILE INKEY$="":WEND:GOTO 30 │
welk programmadeel het op dat moment └───────────────────────────────────────┘
gaat. U ziet trouwens ook waarom in regel 100 geen doorverwijzing naar regel
110 nodig is; het is de regel die in de programmaflow volgt op regel 100.
Dit testprogramma gedraagt zich, evenals tal van andere als een perpetuum
mobile. De STOP of END ontbreekt. Stop dit programma met Shift+Break.
Muzikale grapjes met randomizers
Randomizers kunnen de fantasie van de programmeur enorm prikkelen. Sterker nog:
door te stoeien met functies en randomizers kun je tot ontdekkingen komen. Je
kunt tot dingen komen die nog nooit eerder zijn bedacht. Dat geldt bijvoorbeeld
voor grafische en geluidseffecten.
We hebben het muziek- en geluidsgedeelte van GWBASIC nog niet gehad maar niets
hoeft ons er van te weerhouden er onze randomizers op los te laten. Opgemerkt
is al dat randomizers de illusie van kunstmatige intelligentie kunnen opwekken.
Ze kunnen ook de illusie van kunsmatige creativiteit oproepen. Ik heb dat
geprobeerd met onderstaand programma waarin ik de PC muzikale akkoorden laat
genereren. Daarbij is uitgegaan van een simpel principe, namelijk dat muziek
voor een belangrijk deel op herhaling van voorgaande akkoorden maar dan met
verschillende toonhoogte is gebaseerd.
En warempel... dit principe hanterend worden er geluiden ten gehore gebracht
die meestal sterk aan gecomponeerde muziek doen denken.
╔════════════════════════════════════════════════════════════════════════════╗
║10 CLS:KEY OFF:RANDOMIZE TIMER ║
║20 M$="ABCDEFG...." ║
║30 DEF FN X=INT(RND*2)+12 ║
║40 DEF FN Y=INT(RND*11)+1 ║
║50 DEF FN H=INT(RND*2)+2 ║
║60 MUZIEK$="" ║
║70 FOR A%=1 TO FN X ║
║80 Z$=MID$(M$,FN Y,1) ║
║90 MUZIEK$=MUZIEK$+Z$ ║
║100 IF A%/6=INT(A%/6) THEN GOSUB 180:MUZIEK$=MUZIEK$+"MB"+Z$:PRINT MUZIEK$ ║
║110 IF A%/7=INT(A%/7) THEN GOSUB 190:MUZIEK$=MUZIEK$+"ML"+Z$:PRINT MUZIEK$ ║
║120 NEXT A% ║
║130 IF LEFT$(MUZIEK$,1)="." THEN MUZIEK$="":GOTO 30 ║
║140 GOSUB 180 ║
║150 PRINT:PRINT MUZIEK$ ║
║160 MUZIEK$=MUZIEK$+"MLL4EDEGAGFEDMNL2C...C." ║
║170 PLAY MUZIEK$:END ║
║180 Z$="O"+STR$(FN H)+MUZIEK$:RETURN ║
║190 Z$="L"+STR$((FN H)+2)+MUZIEK$:RETURN ║
╚════════════════════════════════════════════════════════════════════════════╝
Print dit programma uit (gebruik desnoods <Shift/Print Scrn>) en luister. U
zult het met me eens zijn dat er iets 'herkenbaars' in de akkoorden zit, maar
over de vraag of dit 'muzikaal' is of 'mooi' mag u van mening verschillen.
Merk op dat in de regels 30 tot en met 50 DEF FN is gebruikt voor de
randomizers die elders in het programma in actie komen.
Voor muziek wordt in GWBASIC het commando PLAY gebruikt. Dit is een nogal
veelzijdig commando dat eigenlijk verspild is aan het goedkope en vaak ook
kwalitatief slechts luidsprekertje waarmee PC's zijn uitgerust. In eerste
aanleg was het luidsprekertje alleen bedoeld om piepjes van SOUND en BEEP
hoorbaar te maken. PLAY verwacht een tekststring en het aardige is natuurlijk
dat zo'n tekststring willekeurig kan worden gemaakt door het gebruik van
randomizers.
Willkeurige getallen die door RND() worden gemaakt moeten voor de tekststring
naar tekst worden omgezet. Dit gebeurt in de regels 180 en 190 met de functie:
STR$(numerieke variabele). STR$ is dus het omgekeerde van VAL(tekst) waarmee
van een getal in de vorm van tekst een echt numeriek en verwerkbaar getal wordt
gemaakt.
Dat was het weer. Oefen vooral veel met het geleerde en bedenk zelf varianten.
Ik zie u over een paar maanden graag weer terug....
(Deze cursus wordt voortgezet in RUN Flagazine)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀