Een spelletje met drukknoppen: Simon says

In deze tutorial leren we hoe je een het spelletje "Simon says" kan programmeren op het Dwengo-bord. Daarin is het de bedoeling dat een speler een getoonde sequentie probeert te reproduceren. De sequentie zal getoond worden door middel van 3 LEDs, terwijl de speler de sequentie kan ingeven door middel van de drukknoppen op het Dwengo-bord. De verschillende aspecten van de vorige tutorials komen nu samen in een complexer programma. Bovendien leer je hoe je de drukknoppen van het Dwengo-bord moet gebruiken.



Benodigdheden

  1. Een Dwengo-bord
  2. Een Dwengo-programmer
  3. De bijbehorende kabels

Simon says

In het spel "Simon says" moet de speler een getoonde sequentie weten te reproduceren. Deze sequentie wordt telkens langer en langer waardoor het spel alsmaar moeilijker wordt.

Uiteraard kan een maximale lengte voor de sequentie ingesteld worden. Het tonen van de sequentie gaan we doen door LED0 tot en met LED2 op te lichten op het bord. Overeenkomstig moet dan door de speler drukknop E(ast), C(entral) of W(est) ingedrukt worden naargelang respectievelijk LED0, LED1 of LED2 oplichtte.

Aan de slag ...

Stap 1: project aanmaken en initialisatie

Begin zoals steeds met het aanmaken van een nieuw project. Vervolgens maak je een nieuw C-bestand aan voor het project (bijv. SimonSays.c). Bovenaan in het C-bestand voor dit project (SimonSays.c) voeg je de code in om de nodige bibliotheken in te laden. Dat zijn er deze keer 2:

  1. #include <dwengoConfig.h>
  2. #include <dwengoBoard.h>

Merk op dat ondanks dat we het LCD-scherm gebruiken en de wacht-functionaliteit de headers dwengoLCD.h en dwengoDelay.h niet hebben toegevoegd. We hebben dit gedaan omdat deze headers reeds toegevoegd worden in de header dwengoBoard.h. De headers dwengoLCD.h en dwengoDelay.h nogmaals toevoegen mag maar moet niet en geeft dus geen enkele invloed op het verloop van ons programma.

We definieren eveneens twee macro's die de code wat leesbaarder zullen maken. Het is steeds proper om te vermijden dat er in jouw code getallen staan waarvan niet duidelijk is waar ze vandaan komen (zogenaamde magic numbers). Met behulp van macro's krijgen deze getallen meer betekenis en wordt de code veel leesbaarder.

  1. #define MAX_LENGTH_SEQ 10
  2. #define ALFA 3

De code in de main begint met de definitie van de nodige variabelen en met enkele initialisaties. Merk op dat we zoveel mogelijk variabelen slechts 1 byte groot maken, dat volstaat voor alle variabelen behalve seed. Het datatype dat we daarvoor gebruiken is unsigned char, maar dankzij de macro's in dwengoBoard.h kunnen we gewoon BYTE schrijven. Maak er steeds de gewoonte om een zo klein mogelijk datatype te kiezen.

Eveneens maken we een rij aan van unsigned chars met de naam sequence. Hierin komen de getallen die overeenkomen met de te tonen LEDs, de sequentie die de speler moet reproduceren.
Vervolgens initialiseren we het Dwengo-bord waardoor de pinnen aangesloten op de LEDs als uitgang worden ingesteld en het LCD-scherm wordt geinitialiseerd.

Voor de poort B-pinnen moeten ook de interne pull-up-weerstanden geactiveerd worden. Op het ogenblik dat een drukknop wordt ingedrukt, komt de overeenkomstige pin op 0 V. De pull-up-weerstand zorgt ervoor dat de pin op 5 V komt, als de knop niet ingedrukt is. Dit gebeurt echter ook al wanneer je de functie initBoard() oproept. Wens je hierover toch meer technische informatie dan kan je steeds de pagina over het gebruiken van knoppen raadplegen.

  1. void main(void) {
  2. unsigned int seed;
  3. BYTE sequence[MAX_LENGTH_SEQ];
  4. BYTE error, n, i, j, is_button_pressed, button_pressed;
  5.  
  6. // initialise the board
  7. initBoard();

Stap 2: begin van het spel

Het spel begint pas nadat de speler op drukknop C heeft gedrukt. We beginnen dus met die boodschap naar het scherm te sturen.

Om te controleren of drukknop C is ingedrukt, moeten kijken of poort RB2 laag komt te staan (op 0 V). Via de macro's hebben we SW_C gedefinieerd als poort RB2 en PRESSED als 0, dus kunnen we onderstaande while-lus laten lopen zolang SW_C niet ingedrukt is. Wanneer op de knop wordt gedrukt, springen we uit de lus.

We tellen ondertussen ook hoe lang het duurt voor de speler op de knop drukt. Dit gebeurt in de variabele seed die telken met 1 verhoogt wordt. Deze seed zullen we gebruiken om een willekeurige sequentie te genereren voor het spel. Merk op dat computers geen echt willekeurige sequenties kunnen genereren, maar enkel pseudo-willekeurige sequenties. Via srand(seed) definiëren we eigenlijk welke vaste sequentie we willen. Door het feit dat we srand(seed) laten afhangen van een min of meer willekeurige gebeurtenis (de tijd die nodig is voor de speler op de knop duwt) krijgen we uiteindelijk toch willekeurige getallen met de functie rand().

  1. backlightOn();
  2. appendStringToLCD("Press button C to play");
  3.  
  4. seed = 0;
  5. while (SW_C != PRESSED) {
  6. seed++;
  7. }
  8. srand(seed); // seed of pseudo random sequence

Stap 3: het eigenlijke spel

Vervolgens beginnen we met het spelletje zelf. Als we het spel omzetten in een algoritme, dan krijgen we volgende stappen:

  1. We starten met een lege sequentie (geen elementen in de sequentie)
  2. Verleng de sequentie (1 element toevoegen)
  3. Toon de volledige sequentie
  4. Wacht tot de speler een drukknop indrukt
  5. Controleer of de ingedrukte drukknop overeenkomt met de juiste LED in de sequentie
  6. Indien er een fout gemaakt werd: stop het programma, de speler is verloren
  7. Herhaal stap 4, 5 en 6 tot de volledige sequentie is ingetoetst door de speler
  8. Keer terug naar stap 2 indien de maximale lengte van de sequentie nog niet bereikt is
  9. De maximale lengte sequentie werd bereikt: de speler is gewonnen

Als we dit omzetten naar code dan initialiseren we eerst de variabele error op FALSE om aan te geven dat er nog geen fouten zijn gemaakt door de speler. We zetten de teller n op 0 omdat de sequentie voorlopig nog leeg is.

  1. error = FALSE; // no errors made yet
  2. n = 0;

Vervolgens bouwen we een grote while-lus op die blijft lopen zolang we nog niet aan de maximale sequentie-lengte zitten én de speler geen fouten heeft gemaakt. We tonen op het scherm in welke ronde we zitten.

  1. while (n < MAX_LENGTH_SEQ && error != TRUE) {
  2. clearLCD();
  3. printStringToLCD("Round ", 0, 0);
  4. appendIntToLCD(n+1);
  5. appendStringToLCD(": showingsequence ");

Omdat de LEDs verbonden zijn met de datalijnen van het LCD-scherm, gaan de LEDs ook kortstondig oplichten terwijl we tekst naar het scherm schrijven. Eenmaal de tekst op het scherm staat, moeten we de LEDs dus allemaal expliciet uitschakelen, om zeker te zijn dat er geen meer branden.

Vervolgens wachten we 250 ms om de speler tijd te geven op zijn ogen te wenden naar de LEDs.

  1. LEDS = 0;
  2. delay_ms(250); // wait 250 ms

Vervolgens voegen we een nieuw element toe aan de sequentie, op positie n. De waarde berekenen we via de pseudo-random-generator. rand() genereert een willekeurig natuurlijk getal. We nemen hiervan de modulo 3 (ALFA) zodat de getallen tussen de 0 en 2 liggen. We gebruiken immers maar 3 LEDs. Hierbij komt LED0 overeen met het getal 0 en drukknop W, LED1 met getal 1 en drukknop C en LED2 met getal 2 en drukknop E.

Merk op dat de ingebouwde pseudo-random-generator, in combinatie met de seed, volstaat voor dit spelletje, maar dat deze getallen dus niet echt willekeurig zijn.

  1. sequence[n] = rand() % ALFA; // pick random number from 0 to 2

Nu lopen we de hele sequentie af en tonen we elk element afzonderlijk. De eerste keer zal n nog 0 zijn en tonen we dus enkel het eerste element in de rij. Merk op dat we elke LED precies 500 ms laten oplichten en dat we tussen twee elementen van de sequentie 250 ms wachten. Je kan zelf deze tijden aanpassen zodat de sequentie sneller of trager getoond wordt.

  1. for (i=0;i<=n;i++) { // show sequence
  2. if (sequence[i] == 0)
  3. LEDS = 1;
  4. else if (sequence[i] == 1)
  5. LEDS = 2;
  6. else
  7. LEDS = 4;
  8. delay_ms(500); // wait 500 ms
  9. LEDS = 0;
  10. delay_ms(250); // wait 250 ms
  11. }

Nadat de sequentie is getoond, tonen we een andere tekst op het LCD-scherm waarin we de speler vragen om de juiste drukknoppen in te drukken.

  1. clearLCD();
  2. appendStringToLCD("Round ");
  3. appendIntToLCD(n+1);
  4. appendStringToLCD(": press correct buttons ");

Hierna wachten we op het reproduceren van de sequentie door de speler. Hiervoor lopen we opnieuw de volledig sequentie af met behulp van een for-lus. In deze for-lus wachten we eerst telkens totdat de speler één van de toegelaten drukknoppen (E, C of W) heeft ingedrukt. Hiervoor worden alle drukknoppen afgelopen in een while-lus waarin alle drukknoppen gecontroleerd worden op het al dan niet ingedrukt zijn. Dit wachten tot een bepaalde toestand bereikt wordt (in dit geval: 1 van de knoppen is ingedrukt), noemt men polling.

Wanneer de speler een drukknop heeft ingedrukt, wordt het nummer van de knop opgeslagen in de variabele button_pressed. Op het LCD-scherm tonen we een sterretje zodat de speler ziet dat hij knop weer mag loslaten. Na het indrukken van een drukknop wachten we 250 ms om te vermijden dat het indrukken van een drukknop meerdere keren geteld wordt. De speler mag dan immers nog zo kort een drukknop indrukken, voor de snelle microcontroller van het Dwengo-bord lijkt dit alsof de drukknop gedurende langere tijd (= meerdere instructiecycli) blijft ingedrukt. Nu mag de speler 250 ms lang de drukknop ingedrukt houden wat overeenkomt met het normaal indrukken van een toets door een mens.

Tot slot moet het programma controleren of de ingedrukte drukknop, opgeslagen in de variabele button_pressed, overeenkomt met het opgelichte LED in de sequentie. Dit gebeurt door middel van een if-instructie. Wanneer het de ingedrukte drukknop fout is, zetten we de variabele error op TRUE waardoor de lus: while(n < MAX_LENGTH_SEQ && error != TRUE) zal gestopt worden. Wanneer de speler een fout maakte in het midden van de sequentie zal de rest van de sequentie ook niet meer gecontroleerd worden. Immers de variabele i kennen een waarde toe groter dan n waardoor er uit de lus for(i=0;i<=n;i++) gesprongen worden. De speler is dan verloren.

  1. for (i=0;i<=n;i++) {
  2. is_button_pressed = FALSE;
  3. while (!is_button_pressed) {
  4. is_button_pressed = TRUE;
  5. if (SW_E == PRESSED)
  6. button_pressed = 0;
  7. else if (SW_C == PRESSED)
  8. button_pressed = 1;
  9. else if (SW_W == PRESSED)
  10. button_pressed = 2;
  11. else
  12. is_button_pressed = FALSE;
  13. }
  14. appendStringToLCD("*"); // give feedback about pressed buttons
  15. delay_ms(250); // wait 250 ms, avoids users press too long
  16.  
  17. if (button_pressed != sequence[i]) {
  18. error = TRUE;
  19. i = n + 1; // jump out for-loop if error
  20. }
  21. }

Wanneer de sequentie juist werd ingegeven, mag de sequentie verlengd worden met behulp van n++. We keren dan terug naar het begin van de while-lus.

  1. n++;
  2. } // ending large while-loop

Wanneer de lus while(n < MAX_LENGTH_SEQ && error != TRUE) gedaan is, kan dit maar twee zaken betekenen:

  1. De speler heeft een fout gemaakt en het spel wordt vroegtijdig gestopt. De variabele error staat dan op TRUE.
  2. Het spel loopt af omdat de sequentie de maximale lengte heeft bereikt. De variabele error staat dan op FALSE.

Dit controleren we met behulp van een if-lus en we tonen de juiste boodschap op het scherm.

  1. if (error == TRUE) {
  2. clearLCD();
  3. appendStringToLCD(" :( You LOSE :( ");
  4. }
  5. else {
  6. clearLCD();
  7. appendStringToLCD(" :) You WIN :) ");
  8. }

Het spel is nu gedaan maar dit betekent niet het einde van onze code. Wanneer we hier het haakje van de main-functie zouden sluiten dan zou de compiler er immers voor zorgen (door het toevoegen van enkele instructies) dat de main-functie terug gestart wordt van in het begin. Dit is uiteraard ongewenst want de boodschap op het LCD-scherm of de speler al dan niet wint zou dan onmiddellijk overschreven worden door de boodschap die vraagt aan de speler om drukknop C in te drukken.

Dit kunnen we vermijden door een oneindige lus in de vorm van een while(TRUE)-lus toe te voegen. Het programma blijft dan in die lus vastzitten tot de speler zelf op de reset-knop drukt.

  1. delay_s(1);
  2. printStringToLCD("RESET to restart", 1,0);
  3. delay_s(2);
  4. backlightOff();
  5. while (TRUE);

De volledige broncode van het spelletje met de nodige LCD-bibliotheek kan je onderaan downloaden. Veel programmeer- en speelplezier!

BroncodeBronbestanden zijn enkel toegankelijk voor geregistreerde gebruikers. Als je al een account hebt op de Dwengo site, gelieve dan in te loggen, anders kan je je eerst registreren.