Schrijf een Arduino-programma dat een simpele gokkast nabootst. Je programma moet de volgende dingen kunnen:
- De gebruiker kan op een knop drukken om virtueel geld (credits) in te voeren.
- De gebruiker kan op (een andere) knop drukken om drie virtuele rollen virtueel te laten draaien.
- Zodra de rollen gedraaid hebben, word de eindstand van de rollen getoond aan de gebruiker (via de Seriële monitor).
- Die eindstand toont steeds drie symbolen per rol: Elke rol heeft een symbool op de middenlijn, en een symbool erboven, en een symbool eronder. Zie de afbeelding. Dat levert een 3-bij-3 tabel van symbolen op.
- Als er 3 dezelfde symbolen staan in een horizontaleof diagonale lijn, dan wint de speler geld.
- De speler kan, wanneer de rollen niet “draaien”, besluiten dat hij/zij de credits “in geld uitbetaald” wil krijgen, door op (weer een andere) knop te drukken.
- De speler kan besluiten om de inzet te verhogen of te verlagen. De prijs bij winst is altijd een veelvoud van de inzet.
- Het programma gebruikt de LEDjes en debuzzer van het breadboard om het spel op te leuken met special effects.
- Maar de inhoudelijke output ziet de speler in de seriële monitor: hoeveel credits hij/zij heeft, wat de inzet zal zijn van de volgende ronde, wat de symbolen zijn nadat de rollen gedraaid hebben. Of, en hoeveel hij/zij gewonnen heeft na een ronde.
Gokkasten zijn pseudo-random
In een moderne gokkast wordt de stand van de rollen na het draaien niet aan “echt” toeval overgelaten: De kracht waarmee de speler de draaibeweging start, de wrijving of andere mechanische eigenaardigheden van de kast hebben geen enkele invloed op de eindstand van de rollen: Zodra de speler het draaien start, gebruikt de computer van de gokkast zijn (pseudo-)random-functie om de eindstand van iedere rol te bepalen. Dat kun je dus ook in de Arduino zo doen.
Rollen, array’s en posities
Een rol heeft typisch 20 symbolen. In een echte gokkast zijn dat afbeeldinkjes van b.v. fruit. In jouw Arduino-programma is het handiger om daar getallen van te maken. Een rol is dan een lijst (array) van 20 gehele getallen. Bijvoorbeeld:
11,22,33,44,11,22,33,44,11,22,33,44,11,22,33,44,11,22,33,44
of
2, 4, 6, 8, 10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40
of
1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 2, 2, 2
Door getallen meerdere keren in de rollen te laten voorkomen, kun je de kans op winst voor de speler verhogen.
Je mag zelf kiezen hoe je je rollen indeelt, maar de rollen moeten wel van elkaar verschillen.
Je programma hoeft, per rol, alleen maar met de random() functie te kiezen welke positie de rol krijgt: m.a.w. welk element van de lijst er op de middenlijn eindigt. De indeling van de rol bepaalt dan welke getallen er direct boven en direct onderde middenlijn liggen.
Bijvoorbeeld: Als de gokkast een rol heeft zoals de eerste voorbeeld hierboven, en je programma kiest positie 7, dan toont je programma het getal 44 op de middenlijn (Arduino-arrays beginnen ook bij positie 0, dus positie 7 is de 8e waarde in de array). En boven de middenlijn toont deze rol dus het getal 11, en eronder het getal 33.
Ander voorbeeld: Als je kast een rol heeft zoals het tweede voorbeeld, en de je programma kiest voor die rol positie 19, dan toont-ie 40 op de middenlijn, erboven 2 en eronder 38.
Dus: Als een positie precies op de grens van de array wordt gekozen (positie 0 of 19), dan doet je programma alsof de array “rond” is: de aangrenzende waarde wordt van de andere kant vande array gehaald.
Array’s in Arduino
Ook de Arduino-programmeertaal kent array’s, maar die zijn veel beperkter dan die van Javascript. Hier zijn wat overeenkomsten en verschillen:
Overeenkomsten:
- Array-posities beginnen bij 0. Dus het laatste element van een array van 10 elementen heeft positie 9.
- Vierkante haken [ en ] gebruik je om een element uit de array te lezen of te veranderen. Bijvoorbeeld:
Serial.println( mijnLijst[4] );
en
mijnLijst[4] = random(1,6);
- Je kunt array’s binnen array’s bewaren. Lezen en schrijven van elementen ziet er dan zo uit:
while( mijnTabel[4][0] > 100 ) { … } // lezen 5e rij, 1e kolom
en
mijnTabel[4][0] = 101; // schrijven;
- De standaard manier om iets met alle items in een array te doen is weer de for-loop. Die werkt net zo als in Javascript:
for( positie=0; positie < 5; positie++ ) { //let op: niet mijnScores.length totaal += mijnScores[positie] }
Verschillen:
- Array’s in Arduino hebben een vaste lengte: Je kunt ze niet langer of korter maken.
- Alle elementen van een Arduino-array moeten hetzelfde datatype hebben. Je kunt dus niet getallen en tekst-waardes samen in één array combineren.
- Zowel het datatype als de lengte van de array moet je aangeven zodra je de array maakt. Een integer array die plek heeft voor 5 elementen maak je b.v. zo:
int mijnScores[5] = { 0, 100, 200, 300, 400 };
Een array voor 7 floating-point getallen maak je zo:
float mijnTemperaturen[7] = { 42.9, 40.2, 39.7, 39.0, 38.5, 37.9, 37.5 };
Als je arrays-binnen-arrays wil gebruiken dan ziet een declaratie er b.v. zo uit:
int mijnRollen[3][4] = { { 1,1,2,3}, {2,1,3,1}, {3,1,1,2} } // hele kleine rollen.
- Je kunt van een array niet opvragen wat z’n lengte is. B.v.
mijnScores.length
bestaat dus niet. Het idee is dat je als programmeur de lengte zelf bepaald hebt, en dus weet wat de lengte is (array’s groeien en krimpen niet). Vandaar dat de for-loop hierboven niet de lengte van mijnScores opvroeg zoals JS code dat zou doen. - Zoals je hierboven ziet: Een complete array met inhoud schrijf je met accolades { en } in plaats van vierkante haken. Maar je gebruikt wel vierkante haken [ en ] als je een element van de array wil aanwijzen (zie 1e overeenkomst).
- De declaratie van een array-variabele (waar je datatype en lengte opgeeft) is de enige plek waar je een complete array met inhoud mag opgeven. Op andere plaatsen in je code (b.v. als je een array als parameter aan een functie wil geven) moet je de naam van een bestaande array-variabele gebruiken (of een functieaanroep, mits die functie een array als return-waarde oplevert)
- Je mag de inhoud van de array trouwens weglaten in de declaratie. b.v:
int mijnScores[5];
In zo’n geval staan er onvoorspelbare waardes in de array. Je kunt niet voorspellen wat de waardes zullen zijn die je aantreft, maar je kunt er ook niet op rekenen dat dat steeds mooi random gekozen waardes zullen zijn. - Als je een element in de array probeert te lezen of te veranderen, dan wordt er niet gecontroleerd of je een positie gebruikt die OK is. Je kunt dus een negatieve positie, of een positie die (ver) voorbij de lengte van de array ligt gebruiken, maar als je dan leest krijg je weer onvoorspelbare onzin-waardes.
Als je schrijft naar een niet-bestaande positie van een array, dan is de kans aanwezig dat je info uit andere variabelen per ongeluk verandert, of dat je programma crasht. Voorbeeld:
mijnTemperaturen[10] = 36.1; // 11e element, terwijl mijnTemperaturen 7 plekken heeft
of
Serial.println( mijnScores[ -1 ] ) // kans dat je programma crasht.
In deze opdracht gebruik je Arduino-array’s om de inhoud van de rollen op te geven, en uit te lezen in je programma. Je hoeft ze niet te veranderen. Maar je moet ze wel declareren en uitlezen. En: uitkijken dat je niet per ongeluk buiten de array probeert te lezen/schrijven met een foutieve positie!
Meer info over Arduino-array’s vindt je hier: https://www.tutorialspoint.com/arduino/arduino_arrays.htm
Stappenplan voor de Arduino-gokkast:
Stap 1: Let’s roll
Schrijf een Arduino-programma met daarin
- de declaratie van een rol (array van 20 getallen), inclusief zelfbedachte inhoud. Zet die declaratie buiten de
setup()
ofloop()
functie (het wordt dus en globale variabele), - in de loop() functie met Serial.println()die een willekeurige waarde uit die array uitprint. (gebruik de Arduino random() functie om een willekeurige positie te kiezen)
- en een
delay(500)
die je gebruikt om het tempo van uitprinten wat te verlagen.
Stap 2: Seeding the random
Als je je programma meerdere keren start, zal je zien dat je steeds dezelfde reeks waardes terugkrijgt van de random()
functie. Random-functies hebben een soort startwaarde nodig, en als de startwaarde steeds hetzelfde is, dan is de reeks random-getallen ook steeds hetzelfde. Een gewone computer gebruikt de huidige datum/tijd als startwaarde van voor de random-reeks, en dat maakt de reeks waardes steeds verschillend. Maar een Arduino heeft geen klokje dat blijft lopen als de Aruino uit staat, en kan de huidige tijd dus niet gebruiken als startwaarde.
Een aardige oplossing is om een analoge waarde te lezen van een analoge input-pin waar niks op aangesloten is. Dan lees je eigenlijk ruis: waardes die random veranderen van moment tot moment.
- Op ons breadboard zit niks aangesloten op analoge input pin 5. Voeg de volgende regel toe aan je setup() functie:
randomSeed( analogRead(5) );
Check dat je nu na iedere herstart van je programma, een andere reeks random-waarden krijgt.
Stap 3: Three-in-a-row
Maak nog twee rollen (array-declaraties), ieder met een andere indeling dan de andere twee.
- Denk even na over de inhoud: de rollen moeten verschillen, maar wel getallen bevatten die ook in de andere rollen voorkomen. Kijk of je rollen kunt “designen” waarmee je regelmatig wat te winnen valt.
- print nu, in de
loop()
functie, uit iedere rol een willekeurig element. Dus drie posities kiezen, drie getallen uit de array’s lezen en uitprinten. - Als alledrie de waardes die je kiest uit de rollen gelijk zijn aan elkaar, dan print je nog wat extra: “WINST!” of zoiets.
Bonus als je de drie array’s voor de rollen zelf weer in een array stopt: een tweedimensionale array dus. De rol-waardes die je leest van de gekozen posities vormen straks de middenlijn van de uitslag.
Stap 4: You win some, you lose some
Maak nog een globale variabele (in Arduino zijn globale variabele vaker nodig dan in Javascript) die het aantal credits bijhoudt.
- Welk datatype moet die variabele zijn?
- Geef de credits een startwaarde (b.v. 5).
- Als, in de loop()-functie, de rol-waardes op de random-posities gelijk zijn (WINST!), dan komen er 10 credits bij.
- Als er geen WINST! is, dan gaat er 1 credit af.
- Toon, in iedere ronde van loop(), behalve de gekozen rol-waardes, ook het huidige aantal credits, zodat de gebruiker zijn/haar credits kan zien groeien en afnemen.
Pas desnoods de inhoud van je rollen aan, zodat er af-en-toe gewonnen wordt, maar meestal niet. Hiernaast de indeling van de rollen van een bastaande gokkast.
Stap 5: You push my buttons…
Zorg ervoor dat je programma reageert op 2 knoppen:
- De ene knop verhoogt de credits met 1 (speler werpt een munt in).
- De andere knop speelt nu een ronde in het spel: drie rolposities kiezen, rol-waardes uitprinten, winst berekenen, credits updaten (laten we dit een draai-ronde noemen).
- Nadat je programma op een knop heeft gereageerd, wacht het een korte periode (b.v. 100ms) en daarna wacht het tot de knop weer losgelaten wordt. (Weet je nog welk probleem opgelost wordt met die 100ms wachttijd?).
- De delay(500) uit stap 1 kan/moet nu weg.
Dit is een mooi moment om zelf functies te maken. Een stuk of twee of drie, ligt nu voor dehand. Zorg voor goede namen voor de functies.
Stap 6: No credit-no play
Geen gokkast ter wereld is zo dom om de speler geld te lenen. Als de speler door zijn/haar credits heen is, dan:
- reageert je gokkast niet meer op de knop om weer een draai-ronde te starten.
- reageert-ie nog wel op de knop om extra credits toe te voegen.
Zodra de speler meer dan 0 credits heeft, mag hij/zij weer draai-rondes starten.
Stap 7: The stakes get higher
De speler kan besluiten om per draai-ronde meer credits in te zetten (sommige gokkasten hebben deze merkwaardige optie). Gebruik één van de twee potmeters (draaiweerstanden) van de breadboard om de speler de inzet te laten verhogen en verlagen:
- Maak een nieuwe (weer globale) variabeledie de inzet bevat.
- Als de potmeter maximaal laag staat dan is de inzet 1 credit.
- Als de potmeter maximaal hoog staat dan is de inzet 20 credits. Tussenliggende posities worden omgerekend naar gehele getallen tussen 1 en 20 (je kunt geen halve credits inzetten).
- Als de speler WINST! boekt in een draai-ronde, dan krijgt-ie 5 maal de inzet (i.p.v. 10 credits).
- Als een speler geen WINST boekt, dan verliest hij/zij de inzet.
- Als de speler minder credits heeft dan de inzet die hij/zij ingesteld heeft, dan wordt er gespeeld alsof de inzet gelijk is aan de credits die hij wel heeft. Dat wordt met een
Serial.println()
aan de speler vermeld. - Bedenk hoe je bij het starten van het programma de inzet-variabele de goede startwaarde geeft.
- Als de speler aan de potmeter draait, dan worden nieuwe inzet-waardes even uitgeprint naar de Seriële Monitor. Alleen veranderingen dus: als de inzet b.v. 5 is, en de speler draait maar een heel klein beetje aan de potmeter, dan blijft de inzet 5, totdat de speler voldoende gedraaid heeft om de inzet 4 of 6 te laten worden. Pas op dat moment verschijnt er in de Seriële Monitor een tekstje met de nieuwe waarde van de inzet.
Dit mechanisme hoeft alleen te werken als je gokkast-programma niks anders te doen heeft. Als je dus aan het reageren bent op een knop (nieuwe credits of een draaironde),dan hoeft er niet gereageerd te worden op het draaien aan de potmeter.
Stap 8: From the special effects department
Laat het (virtueel) draaien van de rollen een korte tijdsduur krijgen, om de spanning wat op te bouwen.
- Gebruik de LEDjes om die drie seconden op te vullen met geknipper in verschillende kleurtjes.
- Laat de feitelijke tijdsduur afhankelijk zijn van de stand van de tweede potmeter. Minimaal is ongeveer 1 seconde, maximaal is ongeveer 5 seconden. (Komt niet precies)
- Als de speler wat wint, dan speelt de buzzer een vrolijk en snel melodietje van een paar (4-6) noten. Gebruik de volgende header-file (music for arduino.zip) om muzieknootjes af te kunnen spelen via de buzzer. In de header-file zelf staat uitleg over het gebruik. (Er zijn online bronnen over tonen maken met buzzers en Arduino’s, maar de methode die in bijna al die bronnen wordt gebruikt is niet compatible met onze breadboards).
- Voor de bonus: Het zou cool zijn als je met de buzzer een soort van-snel-naar-langzaam effect kunt maken: Eerst volgen de (hele korte) piepjes elkaar snel op, maar de tijdsduur tussen de piepjes wordt steeds groter, om het vertragen van de rollen weer tegeven. (Optioneel)
- Voor de bonus: Het zou ook cool zijn als het voor de gebruiker lijkt alsof de rollen één-voor-één stoppen. Net als een echte gokkast. (Ook optioneel)
Stap 9: What’s up? What’s down?
Tot nu toe werkt je gokkast alleen nog met één winlijn: de middenlijn. Voordat we meerdere winlijnen kunnen inbouwen, moet de gokkast kunnen laten zien, per rol, welk symbool (getallen bij ons) er boven hets ymbool op de middenlijn staat, en welk symbool er onder de middenlijn staat. Zie daarvoor de uitleg onder het kopje “rollen, array’s en posities”.
- Zodra de rol-posities met de
random()
functie gekozen zijn, laat je code dan de waardes boven de gekozen posities (positie+1of 0) en onder de gekozen posities (positie-1 of 19) bepalen. Dat levert 9 getallen op, die samen de uitslag van het rollen-draaien vormen. - Zorg ervoor dat de rollen “rond” zijn: als een positie aan de rand van de rol-array gekozen wordt (positie 0 of 19) dan wordt de waarde onder/boven de middenrij van de andere kant van de array gelezen (zie inleiding over rollen).
- Bonus: Het is mooi als je hiervoor een tweedimensionale array gebruikt, maar dat hoeft niet.
- Toon de gebruiker de hele uitslag netjes in de Seriële Monitor: 9 waardes als een
3-bij-3 tabel.
Hiervoor kun je Serial.print() gebruiken (dus zonder ‘ln’ achteraan de naam). Als je iets print met Serial.print(), dan komt het volgende dat je print op dezelfde regel, in plaats van op de regel eronder. (Het ziet er strakker uit als alle waarden in je rollen – de symbolen dus – evenveel cijfers bevatten. Dus alleen getallen tussen 0 en 9, of alleen getallen tussen 10 en 99).
Stap 10: Win-win (win)
Nu we ook de rijboven de middenlijn hebben, en de rij eronder, kunnen we extra win-mogelijkheden bieden. Dit is het idee:
- Als de uitslag 3 gelijke waardes op de middenlijn toont, dan wint de speler 5 keer de inzet.
- Als de uitslag 3 gelijke waardes op de bovenlijn toont, dan wint de gebruiker 3 keer de inzet. Zelfde voor 3 gelijke waardes op de onderlijn.
- Als de uitslag 3 gelijke waardes op een diagonaal toont, dan wint de gebruiker 1 keer de inzet. Een diagonaal is (natuurlijk) ofwel linksboven-middenmidden-rechtsonder, ofwel linksonder-middenmidden-rechtsboven.
- De speler zou dus nu meerdere winsten kunnen behalen in 1 draai-ronde. Dat is OK. Pas de indeling van je rollen aan zodat dat ook daadwerkelijk kan gebeuren in je code, zodat je makkelijk kunt testen dat je code correct (= meerdere winsten goedkeuren) werkt in dit geval.
- Bonus: Sommige waardes komen wellicht veel minder vaak voor in je rollen dan andere. De kans op een 3-op-een-rij met die waardes is dus klein. Je kunt ervoor kiezen om een winst, behaald met zo’n zeldzame waarde extra te belonen. (Traditioneel hebben gokkasten een BAR-symbool dat heel veel waard is, en zijn sommige vruchten ook meer waard dan andere). (Optioneel)
Iedere winlijn wordt gechecked, en iedere winst wordt gemeld in de Seriële monitor (en de bijbehorende winst toegevoegd aan de credits), en gevierd met een special effect (ledjes, en een melodietje).
Stap 11: Final touch
Vroeger mochten gokkasten geld uitkeren. Laten we dat op een simpele manier simuleren:
- Wanneer de speler op een knop drukt (de derde, laatste die nog beschikbaar is), dan worden de credits op 0 gezet.
- Bedenk een special effect (licht en geluid) voor uitbetalen, en meldt op de Seriële Monitor dat het geld uitbetaald is.
- De speler kan pas weer gaan spelen als hij/zij weer credits toegevoegd heeft (als het goed is heb je deze feature er al in zitten).
Klaar. Gefeliciteerd. Knap werk!
Waar wordt de opdracht op beoordeeld
- Functionaliteit: de hierboven beschreven functionaliteit werkt correct.
- Naamgeving: Variabelen en functies hebben namen die kort en goed beschrijven wat hun rol in de code is.
(Soms besluit je de rol van een variabele, of de functionaliteit van een functie, wat aan te passen. Vergeet niet om dan de naam ook mee te veranderen!) - Functiegebruik: Je code is opgedeeld in meerdere functies. De functies zelf zijn het liefst niet te groot (tussen 1 en 10 regels). Functies gebruiken, als dat handig is, parameters om in meerdere situaties inzetbaar te zijn.
- NIet teveel herhalende code. Dit is niet altijd te vermijden, maar zeker door gebruik van functies wel vaak.
- Correct en handig gebruik van array’s
- Code-kwaliteit: Inspringen. Geen code die niet (meer) gebruikt wordt.
- Commentaar: Leg, op de plaatsen in je code waarin het wat ingewikkeld wordt (waarin een lezer even zou moeten puzzelen om te snappen wat er gebeurt), met commentaar uit wat er gebeurt, of waarom het nodig is.
- Snugger programmeren: oplossingen met weinig code zijn beter dan oplossingen met veel code, mits de compacte code leesbaar blijft.
- Bonusopdrachtjes.
- Special-effects: bonus punten voor het bedenken en maken van leuke special effects.