Scorelijst

Met de pro micro werkt de chip ook bijzonder goed.

Als je een leuk spel hebt bedacht dan wil je de hoogste scores wel kunnen opslaan. Als het maar om een stuk of 10 scores met namen erbij gaat, heb je niet veel geheugen nodig en geheugenchips zijn dan zeer geschikt. De AT24C16 kost bijna niks, is klein, makkelijk aan te slutien en er past gemakkelijk een scorelijst op. Het voorbeeld programma hieronder kun je vrij gemakkelijk geschikt maken voor andere EEPROM chips, door de lees- en schrijffuncties iets aan te passen. Tip: als je de Arduino alleen voor je spel wil gebruiken en niet al te veel geheugen nodig hebt dan kun in plaats van de chip het interne EEPROM van je Arduino gebruiken. Daar is een erg handige bibliotheek voor. Zie hier de versie van dit met de interne EEPROM.

Het gaat mij alleen om een voorbeeld; daarom heb ik het spel simpel gehouden. Er is geen extra hardware nodig is. Het spel gaat via de Seriële monitor. Het is natuurlijk veel leuker een spel te maken met knoppen en displays. Als je dat doet, dan zul je zelf in code moeten duiken, om te zien wat anders moet. In de voorbeelden gebruik ik altijd de standaard Seriële monitor, die je vanuit de Arduino IDE kunt opstarten, maar je kunt elk ander programma voor Seriële communicatie gebruiken.

Eerst het spel: je krijgt drie willekeurige letters te zien na een willekeurige wachttijd. Dan moet je in de Seriële monitor (in de bovenregel) zo snel mogelijk dezelfde letter invoeren en op Verzenden klikken of op Enter drukken. Het programma meet de tijd die je erover doet. Als je in de top 10 zit met je score dan kun je je naam intoetsen. Je krijgt vervolgens de scorelijst te zien. Na enige tijd merk je dat je de plaats van de toetsen op je toetsenbord goed kent en dat je scores beter worden. Als je de na de eerste vraag je muiscursor in de bovenbalk zet, dan kun je elk antwoord direct intypen en met de Enter-toets verzenden. Als je denkt het programma te kunnen foppen door een makkelijke letter in te typen dan heb je pech: als je een foute letter verzendt dan wordt 1 seconde straftijd toegevoegd.

Een paar opmerkingen over de Seriële monitor. Als je Serial.read() gebruikt, dan is het heel veel makkelijker om dat te doen in de stand "geen regeleinde". Bij andere instellingen zoals "zowel NL als CR" ontdek je bijvoorbeeld dat het antwoord op een vraag al is gegeven is terwijl je niets hebt ingevoerd. Het is echter niet fijn om aan de gebruiker te vragen de Seriële monitor in te stellen op "Geen regeleinde" en bovendien weet je niet of zij of hij dat doet. Daarom kun je dit beter oplossen in je programma. Dit lijkt eenvoudig, maar is dat in de praktijk niet. Dit heeft vooral met timing te maken. Een probleem dat hier los van staat is dat ik niet kan voorkomen dat de gebruiker meer dan één teken tegelijk invoert. Dat kan ook per ongeluk gebeuren. Beide problemen worden in dit programma opgelost in de functie lees1(). Deze geeft de eerste letter die is ingevoerd terug en regeleindes worden genegeerd. In deze functie zit een delay. Als je dat weghaalt dan zijn onmiddellijk alle problemen terug. Ik weet niet of in jouw situatie het delay groot genoeg is, maar je kunt het gerust iets groter maken, indien nodig.

Om de scorelijst op volgorde te zetten moet soms vrij veel gegevens worden verplaatst binnen de EEPROM. Je kunt proberen deze tijd te verkorten door het delay in schrijfByte() iets kleiner te maken. Sommige lezers zullen opmerken dat ik het verplaatsen niet efficiënt doe want ik verplaats altijd 32 + 4 bytes ongeacht de werkelijke lengte van de opgeslagen namen. Maar dat heeft ook weer de nodige voordelen. Mijn namen mogen 32 letters lang zijn. Als je minder letters, bijvoorbeeld 24, ook wel genoeg vindt dan kun je MaxLetters aanpassen en dan wordt het verplaatsen iets sneller. Zoals het programma hieronder staat werkt het bij mij tot nu toe perfect (Uno en Pro Micro). Hieronder zie je het spel in werking.


Ik heb ervoor gekozen om de kleinste scores (dus de kleinste tijden) boven te zetten. Bij andere spellen zul je juist hoge scores moeten halen. Je moet dan dus enkele < tekens door > tekens vervangen.

Nog enkele interessante aspecten van dit programma:

// Demonstratie scorelijst met AT24C EEPROM
#include <Wire.h>
#define Adres 0x50 // I2C basisadres 
#define MaxLetters 32 // Maximale lengte van de opgegeven namen (kies kleiner dan 120)
int aantalScores;  // Houdt bij hoeveel scores er in EEPROM zijn opgeslagen

void schrijfByte(int geheugenplaats, byte data) {
  Wire.beginTransmission( Adres + (geheugenplaats >> 8) );
  Wire.write(geheugenplaats & 0xFF);
  Wire.write(data);
  Wire.endTransmission(); delay(10);
}

void schrijfLong(int geheugenplaats, long data) {
  schrijfByte(geheugenplaats, data >> 24); // schrijf eerste byte
  schrijfByte(geheugenplaats + 1, (data >> 16) & 0xFF);
  schrijfByte(geheugenplaats + 2, (data >> 8) & 0xFF);
  schrijfByte(geheugenplaats + 3, data & 0xFF); // schrijf laatste byte van de long integer
}

byte leesByte(int geheugenplaats) {
  Wire.beginTransmission( Adres + (geheugenplaats >> 8) );
  Wire.write(geheugenplaats & 0xFF);
  Wire.endTransmission();
  Wire.requestFrom(Adres + (geheugenplaats >> 8), 1);
  return Wire.read();
}

long leesLong(int geheugenplaats) { // Snelle versie, gaat met dit programma goed
  Wire.beginTransmission( Adres + (geheugenplaats >> 8) );
  Wire.write(geheugenplaats & 0xFF);
  Wire.endTransmission();
  Wire.requestFrom(Adres + (geheugenplaats >> 8), 4);
  byte x1 = Wire.read(); byte x2 = Wire.read(); byte x3 = Wire.read(); byte x4 = Wire.read();
  return ((long) x1 << 24) | ((long) x2 << 16 ) | ((long) x3 << 8) | x4;
}

void bewaarScore(long score) { // Bij gelijke scores komt de laatste onderaan te staan
  int invoeg = 0;
  if (aantalScores > 0) {
    Serial.println("Even geduld a.u.b.");
    for (int i = 0; i < aantalScores; i++) // aantalScores heeft nog de oude waarde
        if ( score >= leesLong(1 + i * 128) ) invoeg = i + 1; else break; // invoeg is laatste waarvan de score kleiner is dan de huidige score
    int laatste;
    if (aantalScores < 10) laatste = aantalScores - 1; else laatste = 9;
    for (int i = laatste; i >= invoeg; i--) { // Laatste wordt overschreven
      int start = 1 + i * 128;
      for (int j = start; j < start + 5 + MaxLetters; j++) schrijfByte(j + 128, leesByte(j));
    }
  }
  int adres = 1 + invoeg * 128;
  schrijfLong(adres, score);
  adres += 4;
  Serial.println("Zend nu je naam via de seriële monitor.");
  while (!Serial.available()); // Wacht tot er iets in ingetypt
  byte teller = 0;
  while (Serial.available()) {
    delay(5);
    char r = Serial.read();
    if ( (r == '\n') || (r == '\r') ) continue; // verwijder eventuele CR of LF
    if ( teller < MaxLetters ) {
      schrijfByte(adres, r); // Te lange namen worden afgekapt
      adres++; teller++;
    }
  }
  schrijfByte(adres, 0); // Eindig string met een 0 karakter
  if (aantalScores < 10) {
    aantalScores++;
    schrijfByte(0, aantalScores);
  }
}

void toonScores() {
  Serial.println("Nr  Score      Naam");
  for (int i = 0; i < aantalScores; i++) {
    Serial.print(i + 1); if (i < 9) Serial.print(" "); Serial.print("  ");
    int adres = i * 128 + 1; // Dit maakt namen van iets meer dan 120 letters mogelijk
    long score = leesLong(adres);
    Serial.print(score);
    for (int k = floor(log10(score)); k < 10; k++) Serial.print(" "); // Simuleer een tab, werkt beter dan /t
    adres += 4;
    while (true) {
      char c = leesByte(adres); adres++;
      if (c == 0) break; // De namen zijn immers met een 0-byte afgesloten
      Serial.print(c);
    }
    Serial.println();
  }
}

boolean besteScore(unsigned long score) { // Ga na of de score hoger lager is dan de laagste score
  if (aantalScores > 10) return false;
  if (aantalScores < 10) return true;
  return score < leesLong(1 + 128 * 9);
}

char lees1() { // Lost problemen op als de Seriële monitor niet is ingesteld op "geen regeleinde"
  while ( !Serial.available() ); // Wacht tot er iets gebeurt
  int r; char c = 0;
  while (Serial.available()) {
    r = Serial.read();
    switch (r) {
      case '\n': break; // nieuwe regel
      case '\r': break; // regeleinde
      default: if (c == 0) c = r & 0xFF; // ik kan geen 0-code verzenden, dus dit gaat goed
    } delay(5); // Zeer belangrijk. Deze waarde kun je eventueel vergroten
  }
  return c;
}

void setup() {
  Wire.begin();
  Serial.begin(9600); while(!Serial); // while(!Serial); is alleen nodig bij de Leonarde en Pro Micro
  Serial.println("Spelregels");
  Serial.println("De Arduino kiest een letter. Jij moet zo snel mogelijk dezelfde letter verzenden.");
  Serial.println("Na drie keer wordt je score bepaald. De laagste score wint.");
  aantalScores = leesByte(0); if (aantalScores == 255) aantalScores = 0; // EEPROM waarschijnlijk nog niet gebruikt
  if (aantalScores > 10)  {
    Serial.println("WAARSCHUWING: Er staan al andere gegevens in dit EEPROM.");
    Serial.println("Als die gegevens weg mogen kies dan voor 'nieuwe scorelijst'.");
  }
  char Keuze = ' ';
  if (aantalScores > 0) {
    if (aantalScores <= 10) {
      Serial.println("Huidige scorelijst:");
      toonScores();
    }
    Serial.println("Wis scorelijst? Zend een w. Niet wissen? Zend iets anders.\nHet spel start daarna meteen.");
    Keuze = lees1();
    if (tolower(Keuze) == 'w') {
      schrijfByte(0, 0);
      aantalScores = 0;
    }
  }
  if (aantalScores == 0) Serial.println("Er zijn geen scores...");
  randomSeed(millis()); // kies willekeurige startwaarde voor de random number generator
}

String Nde[] = {"Eerste", "Tweede", "Derde "};
void loop() {
  unsigned long Score = 0, Start;
  for (int i = 0; i < 3; i++) {
    Serial.print(Nde[i]); Serial.print(" letter.. komt zo:   "); delay(1500 + random(0, 1000));
    char c = random(byte('a'), byte('z') + 1);
    while (Serial.available()) Serial.read(); // voor als een speler te vroeg heeft geantwoord
    Start = millis();
    Serial.println(c);
    char t = lees1();
    Score += millis() - Start;
    if (t != c) {
      Serial.println("Foute letter teruggezonden: 1 seconde straf");
      Score += 1000;
    }
    Serial.print("Je tijd is "); Serial.println(Score);
  }
  if ( besteScore(Score) ) {
    Serial.println("Je score is in de top tien.\nZend n of N als je dit NIET wilt bewaren en iets anders als je dat wel wil.");
    if (tolower( lees1() ) != 'n') {
      bewaarScore(Score);
      toonScores();
    }
  }
  Serial.println("Spel over. Opnieuw spelen ? [j/n]");
  if (tolower(lees1()) == 'n') while (true); // Blijf hier tot de Arduino gereset wordt
}