o.O wtFAQ – We Tinker, Fix and Question
◀ 3.3. Programiranje igara4. Logički sklopovi ▶

3.4. Programiranje igara - avantura
Pronađi izlaz

avantura

Uvod

Na ovoj radionici izrađujemo malu tekstualnu escape-room igru za OLED 128x64 zaslon i 4x4 tipkovnicu. Glavni lik je mali seljak Marko koji pokušava pobjeći iz podzemnih katakombi tako da u svakoj fazi pronađe tragove, poveže ih i zatim riješi završnu zagonetku koju čuva vitez.

Tekstualne avanture su odličan izbor za Arduino jer dopuštaju da se fokusiramo na priču, logiku i upravljanje stanjima igre, umjesto na skupu grafiku i animacije.

Koji su izazovi kod programiranja kompleksne igre na Arduinu?

Arduino Uno je vrlo pristupačna platforma, ali ima ograničene resurse: ATmega328P ima 2 KB SRAM-a i 32 KB Flash memorije, što znači da veliki stringovi, bitmap grafika i velike biblioteke brzo postaju problem. Arduino službene smjernice zato preporučuju smanjivanje broja stringova, korištenje manjih tipova podataka, ograničavanje veličine nizova i spremanje konstantnih podataka izvan SRAM-a kad god je moguće.

Za ovu vrstu igre bolji mikrokontroler bio bi ESP32, jer nudi mnogo više memorije: 520 kB (320 kB za podatke i 200 kB za program) i veći prostor za kompleksniji sadržaj, a i dalje ostaje jednostavan za rad s OLED zaslonom, tipkovnicom i Arduino-ekosustavom biblioteka. Arduino Uno je izvrstan za učenje osnova i razumijevanje ograničenja embedded razvoja, ali za veću tekstualnu avanturu s više faza, više soba i bogatijim opisima ESP32 je robusniji izbor.

Tema i kontrole

Tema igre je srednjovjekovni podzemni labirint: u mračnim hodnicima seljak Marko nailazi na duhove, stare natpise, zaključane prolaze i vitezove koji ga puštaju tek kada dokaže da je razumio tragove. Svaka faza ima tri sobe: prve dvije daju clue-ove, a treća je završna soba u kojoj igrač mora donijeti točan zaključak.

Kontrole su jednostavne i prilagođene tipkovnici 4x4:

  • 4 i 6 pomiču igrača lijevo i desno po mapi.
  • A ulazi u sobu ili pokreće akciju.
  • B vraća na mapu.
  • C otvara ekran s tragovima.
  • D služi za izlaz iz poruke, povratak ili pokušaj otvaranja vrata.

Struktura programa

Program je organiziran kao state machine, što znači da igra u svakom trenutku zna u kojem se zaslonu ili stanju nalazi: naslov, pomoć, mapa, soba, clues ili završetak. Takva struktura je dobra praksa za male embedded igre jer je predvidljiva, laka za debug i troši manje memorije nego razgranat sustav s mnogo međusobno ovisnih objekata.

Više o kôdu u nastavku.

Programski kôd:

// C++
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <Keypad.h>

// ============================================
// OLED - page buffer for lower SRAM usage
// ============================================
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// ============================================
// KEYPAD 4x4
// ============================================
const byte ROWS = 4;
const byte COLS = 4;

char keys[ROWS][COLS] = {
  {'1','2','3','A'},
  {'4','5','6','B'},
  {'7','8','9','C'},
  {'*','0','#','D'}
};

byte rowPins[ROWS] = {9, 8, 7, 6};
byte colPins[COLS] = {5, 4, 3, 2};

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

// ============================================
// GAME STATE
// ============================================
enum ScreenState : uint8_t {
  SCR_TITLE,
  SCR_HELP,
  SCR_MAP,
  SCR_ROOM,
  SCR_CLUES,
  SCR_END
};

ScreenState screenState = SCR_TITLE;

uint8_t currentPhase = 0;   // 0..2
uint8_t currentRoom  = 0;   // 0..2

uint8_t solvedMask[3] = {0, 0, 0};
uint8_t clueMask[3]   = {0, 0, 0};

bool waitingAnswer = false;
uint8_t messageId = 0;

// ============================================
// HELPERS
// ============================================
bool isSolved(uint8_t phase, uint8_t room) {
  return solvedMask[phase] & (1 << room);
}

void markSolved(uint8_t phase, uint8_t room) {
  solvedMask[phase] |= (1 << room);
}

bool hasClue(uint8_t phase, uint8_t clueIndex) {
  return clueMask[phase] & (1 << clueIndex);
}

void addClue(uint8_t phase, uint8_t clueIndex) {
  clueMask[phase] |= (1 << clueIndex);
}

bool cluesReady(uint8_t phase) {
  return (clueMask[phase] & 0b00000011) == 0b00000011;
}

bool phaseSolved(uint8_t phase) {
  return (clueMask[phase] & 0b00000111) == 0b00000111;
}

void resetGame() {
  currentPhase = 0;
  currentRoom = 0;
  solvedMask[0] = solvedMask[1] = solvedMask[2] = 0;
  clueMask[0] = clueMask[1] = clueMask[2] = 0;
  waitingAnswer = false;
  messageId = 0;
  screenState = SCR_TITLE;
}

// ============================================
// MINI SPRITES
// ============================================
void drawFarmer(int x, int y) {
  u8g2.drawCircle(x + 4, y + 3, 3);
  u8g2.drawLine(x + 4, y + 6, x + 4, y + 12);
  u8g2.drawLine(x + 4, y + 8, x + 1, y + 10);
  u8g2.drawLine(x + 4, y + 8, x + 7, y + 10);
  u8g2.drawLine(x + 4, y + 12, x + 2, y + 15);
  u8g2.drawLine(x + 4, y + 12, x + 6, y + 15);
}

void drawKnight(int x, int y) {
  u8g2.drawFrame(x + 2, y + 1, 6, 6);
  u8g2.drawLine(x + 5, y + 7, x + 5, y + 14);
  u8g2.drawLine(x + 5, y + 9, x + 1, y + 11);
  u8g2.drawLine(x + 5, y + 9, x + 9, y + 11);
  u8g2.drawLine(x + 5, y + 14, x + 3, y + 16);
  u8g2.drawLine(x + 5, y + 14, x + 7, y + 16);
}

void drawGhost(int x, int y) {
  u8g2.drawCircle(x + 4, y + 4, 4);
  u8g2.drawLine(x + 0, y + 8, x + 0, y + 14);
  u8g2.drawLine(x + 8, y + 8, x + 8, y + 14);
  u8g2.drawLine(x + 0, y + 14, x + 2, y + 12);
  u8g2.drawLine(x + 2, y + 12, x + 4, y + 14);
  u8g2.drawLine(x + 4, y + 14, x + 6, y + 12);
  u8g2.drawLine(x + 6, y + 12, x + 8, y + 14);
}

// ============================================
// HEADER
// ============================================
void drawHeader(const char* title) {
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.setFontPosTop();
  u8g2.drawBox(0, 0, 128, 10);
  u8g2.setDrawColor(0);
  u8g2.drawStr(2, 1, title);
  u8g2.setDrawColor(1);
}

// ============================================
// MESSAGES
// ============================================
void drawMessageById(uint8_t id) {
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.setFontPosTop();

  switch (id) {
    case 1:
      u8g2.drawStr(0, 16, "Nasao si trag.");
      u8g2.drawStr(0, 28, "Tipka C = clues");
      u8g2.drawStr(0, 54, "D dalje");
      break;
    case 2:
      u8g2.drawStr(0, 16, "Netocno.");
      u8g2.drawStr(0, 28, "Povezi tragove.");
      u8g2.drawStr(0, 54, "D dalje");
      break;
    case 3:
      u8g2.drawStr(0, 16, "Vrata su");
      u8g2.drawStr(0, 28, "zakljucana.");
      u8g2.drawStr(0, 40, "Rijesi fazu.");
      u8g2.drawStr(0, 54, "D dalje");
      break;
    case 4:
      u8g2.drawStr(0, 16, "Vitez te pusta");
      u8g2.drawStr(0, 28, "dalje.");
      u8g2.drawStr(0, 54, "D dalje");
      break;
    case 5:
      u8g2.drawStr(0, 16, "Soba je vec");
      u8g2.drawStr(0, 28, "rijesena.");
      u8g2.drawStr(0, 54, "D dalje");
      break;
    case 6:
      u8g2.drawStr(0, 16, "Unesi odgovor");
      u8g2.drawStr(0, 28, "1 / 2 / 3");
      u8g2.drawStr(0, 54, "D nazad");
      break;
    case 7:
      u8g2.drawStr(0, 16, "Prvo nadji");
      u8g2.drawStr(0, 28, "oba traga.");
      u8g2.drawStr(0, 40, "Tipka C clues");
      u8g2.drawStr(0, 54, "D dalje");
      break;
  }
}

// ============================================
// CLUES
// ============================================
void drawClueLine(uint8_t phase, uint8_t clueIndex, uint8_t y) {
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.setFontPosTop();

  if (!hasClue(phase, clueIndex)) {
    u8g2.drawStr(0, y, "- ???");
    return;
  }

  if (phase == 0) {
    if (clueIndex == 0) u8g2.drawStr(0, y, "- Srednje je pravo.");
    if (clueIndex == 1) u8g2.drawStr(0, y, "- Trazeni znak je SUNCE.");
  }

  if (phase == 1) {
    if (clueIndex == 0) u8g2.drawStr(0, y, "- Sveti dar je kruh.");
    if (clueIndex == 1) u8g2.drawStr(0, y, "- Vitez bira cvijet.");
  }

  if (phase == 2) {
    if (clueIndex == 0) u8g2.drawStr(0, y, "- Drveni kljuc pase.");
    if (clueIndex == 1) u8g2.drawStr(0, y, "- Cuvar trazi list.");
  }
}

// ============================================
// RENDER
// ============================================
void renderTitle() {
  drawHeader("DUNGEON ESCAPE");
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.setFontPosTop();

  drawFarmer(8, 18);
  drawKnight(102, 18);

  u8g2.drawStr(22, 16, "Seljak Marko");
  u8g2.drawStr(22, 28, "trazi izlaz.");
  u8g2.drawStr(6, 48, "A start");
  u8g2.drawStr(72, 48, "D help");
}

void renderHelp() {
  drawHeader("KONTROLE");
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.setFontPosTop();

  u8g2.drawStr(0, 14, "4/6 kretanje");
  u8g2.drawStr(0, 26, "A akcija");
  u8g2.drawStr(0, 38, "B mapa");
  u8g2.drawStr(0, 50, "C clues D nazad");
}

void renderMap() {
  drawHeader("MAPA");
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.setFontPosTop();

  int rx[3] = {8, 48, 88};
  int ry[3] = {24, 24, 24};

  for (uint8_t i = 0; i < 3; i++) {
    u8g2.drawFrame(rx[i], ry[i], 28, 18);

    if (i == currentRoom) {
      u8g2.drawBox(rx[i] + 1, ry[i] + 1, 26, 16);
      u8g2.setDrawColor(0);
      char n[2] = {char('1' + i), 0};
      u8g2.drawStr(rx[i] + 11, ry[i] + 5, n);
      u8g2.setDrawColor(1);
    } else {
      char n[2] = {char('1' + i), 0};
      u8g2.drawStr(rx[i] + 11, ry[i] + 5, n);
    }

    if (isSolved(currentPhase, i)) {
      u8g2.drawDisc(rx[i] + 24, ry[i] + 4, 2);
    }
  }

  if (currentPhase == 0) u8g2.drawStr(0, 12, "Faza 1");
  if (currentPhase == 1) u8g2.drawStr(0, 12, "Faza 2");
  if (currentPhase == 2) u8g2.drawStr(0, 12, "Faza 3");

  u8g2.drawStr(0, 56, "A soba C clues D vrata");
}

void renderClues() {
  drawHeader("CLUES");
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.setFontPosTop();

  if (currentPhase == 0) u8g2.drawStr(0, 12, "Faza 1");
  if (currentPhase == 1) u8g2.drawStr(0, 12, "Faza 2");
  if (currentPhase == 2) u8g2.drawStr(0, 12, "Faza 3");

  drawClueLine(currentPhase, 0, 24);
  drawClueLine(currentPhase, 1, 36);

  if (phaseSolved(currentPhase)) {
    u8g2.drawStr(0, 50, "- Zavrsna soba rijesena");
  } else {
    u8g2.drawStr(0, 54, "D nazad");
  }
}

void renderRoom() {
  drawHeader("SOBA");
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.setFontPosTop();

  if (currentRoom == 0) {
    if (currentPhase == 0) {
      u8g2.drawStr(0, 14, "Nisa");
      u8g2.drawStr(0, 26, "Natpis kaze:");
      u8g2.drawStr(0, 36, "pravo nije");
      u8g2.drawStr(0, 46, "lijevo ni desno.");
    }
    if (currentPhase == 1) {
      u8g2.drawStr(0, 14, "Oltar");
      u8g2.drawStr(0, 26, "Natpis kaze:");
      u8g2.drawStr(0, 36, "sveti dar");
      u8g2.drawStr(0, 46, "je kruh.");
    }
    if (currentPhase == 2) {
      u8g2.drawStr(0, 14, "Skrinja");
      u8g2.drawStr(0, 26, "Natpis kaze:");
      u8g2.drawStr(0, 36, "drveni kljuc");
      u8g2.drawStr(0, 46, "otvara put.");
    }
    drawKnight(106, 34);
  }

  if (currentRoom == 1) {
    if (currentPhase == 0) {
      u8g2.drawStr(0, 14, "Duh");
      u8g2.drawStr(0, 26, "Sapuce:");
      u8g2.drawStr(0, 36, "trazeni znak");
      u8g2.drawStr(0, 46, "je sunce.");
    }
    if (currentPhase == 1) {
      u8g2.drawStr(0, 14, "Tamnica");
      u8g2.drawStr(0, 26, "Duh kaze:");
      u8g2.drawStr(0, 36, "vitez bira");
      u8g2.drawStr(0, 46, "cvijet.");
    }
    if (currentPhase == 2) {
      u8g2.drawStr(0, 14, "Hodnik");
      u8g2.drawStr(0, 26, "Cuvar kaze:");
      u8g2.drawStr(0, 36, "na vratima je");
      u8g2.drawStr(0, 46, "urezan list.");
    }
    drawGhost(108, 36);
  }

  if (currentRoom == 2) {
    if (currentPhase == 0) {
      u8g2.drawStr(0, 14, "Vrata viteza");
      u8g2.drawStr(0, 26, "Koji znak?");
      u8g2.drawStr(0, 36, "1=sunce");
      u8g2.drawStr(0, 46, "2=oko 3=kruna");
    }
    if (currentPhase == 1) {
      u8g2.drawStr(0, 14, "Zavrsna brava");
      u8g2.drawStr(0, 26, "Sto odabrati?");
      u8g2.drawStr(0, 36, "1=kruh");
      u8g2.drawStr(0, 46, "2=cvijet 3=mac");
    }
    if (currentPhase == 2) {
      u8g2.drawStr(0, 14, "Posljednja brava");
      u8g2.drawStr(0, 26, "Koji simbol?");
      u8g2.drawStr(0, 36, "1=list");
      u8g2.drawStr(0, 46, "2=kljuc 3=mac");
    }
    drawKnight(106, 34);
  }

  if (isSolved(currentPhase, currentRoom)) {
    u8g2.drawStr(0, 56, "Rijeseno. D nazad");
  } else if (!waitingAnswer) {
    if (currentRoom < 2) {
      u8g2.drawStr(0, 56, "A uzmi trag B mapa");
    } else {
      u8g2.drawStr(0, 56, "A rijesi C clues");
    }
  }
}

void renderEnd() {
  drawHeader("SLOBODA");
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.setFontPosTop();

  drawFarmer(10, 20);
  u8g2.drawLine(28, 16, 28, 48);
  u8g2.drawLine(28, 16, 40, 22);
  u8g2.drawLine(28, 48, 40, 42);

  u8g2.drawStr(46, 16, "Marko je");
  u8g2.drawStr(46, 28, "izasao iz");
  u8g2.drawStr(46, 40, "katakombi!");
  u8g2.drawStr(0, 54, "A nova igra");
}

void renderCurrentScreen() {
  u8g2.firstPage();
  do {
    switch (screenState) {
      case SCR_TITLE: renderTitle(); break;
      case SCR_HELP:  renderHelp(); break;
      case SCR_MAP:   renderMap(); break;
      case SCR_ROOM:  renderRoom(); break;
      case SCR_CLUES: renderClues(); break;
      case SCR_END:   renderEnd(); break;
    }

    if (messageId != 0 && screenState != SCR_END) {
      u8g2.drawBox(0, 12, 128, 52);
      u8g2.setDrawColor(0);
      drawMessageById(messageId);
      u8g2.setDrawColor(1);
    }

  } while (u8g2.nextPage());
}

// ============================================
// LOGIC
// ============================================
void handleMovement(char key) {
  if (key == '4' && currentRoom > 0) currentRoom--;
  if (key == '6' && currentRoom < 2) currentRoom++;
}

void solveClueRoom(uint8_t phase, uint8_t room) {
  if (isSolved(phase, room)) {
    messageId = 5;
    return;
  }

  markSolved(phase, room);
  addClue(phase, room);
  messageId = 1;
}

void solveFinalRoom(char key) {
  if (isSolved(currentPhase, 2)) {
    waitingAnswer = false;
    messageId = 5;
    return;
  }

  if (!cluesReady(currentPhase)) {
    waitingAnswer = false;
    messageId = 7;
    return;
  }

  char expected = '1';
  if (currentPhase == 0) expected = '1'; // sunce
  if (currentPhase == 1) expected = '2'; // cvijet
  if (currentPhase == 2) expected = '1'; // list

  if (key == expected) {
    markSolved(currentPhase, 2);
    addClue(currentPhase, 2);
    messageId = 1;
  } else {
    messageId = 2;
  }

  waitingAnswer = false;
}

void tryDoor() {
  if (!phaseSolved(currentPhase)) {
    messageId = 3;
    return;
  }

  if (currentPhase < 2) {
    currentPhase++;
    currentRoom = 0;
    waitingAnswer = false;
    messageId = 4;
  } else {
    screenState = SCR_END;
  }
}

// ============================================
// SETUP / LOOP
// ============================================
void setup() {
  u8g2.begin();
  u8g2.setFontMode(1);
  u8g2.setBitmapMode(1);
  u8g2.setFontDirection(0);
  resetGame();
  renderCurrentScreen();
}

void loop() {
  char key = keypad.getKey();
  if (!key) return;

  if (messageId != 0) {
    if (messageId == 6) {
      if (key == 'D') {
        waitingAnswer = false;
        messageId = 0;
      } else if (key == '1' || key == '2' || key == '3') {
        solveFinalRoom(key);
      }
    } else {
      if (key == 'D') {
        messageId = 0;
      }
    }
    renderCurrentScreen();
    return;
  }

  switch (screenState) {
    case SCR_TITLE:
      if (key == 'A') screenState = SCR_MAP;
      else if (key == 'D') screenState = SCR_HELP;
      break;

    case SCR_HELP:
      if (key == 'D') screenState = SCR_TITLE;
      break;

    case SCR_MAP:
      if (key == '4' || key == '6') {
        handleMovement(key);
      } else if (key == 'A') {
        screenState = SCR_ROOM;
      } else if (key == 'C') {
        screenState = SCR_CLUES;
      } else if (key == 'D') {
        tryDoor();
      }
      break;

    case SCR_CLUES:
      if (key == 'D' || key == 'B') {
        screenState = SCR_MAP;
      }
      break;

    case SCR_ROOM:
      if (key == 'B' || key == 'D') {
        waitingAnswer = false;
        screenState = SCR_MAP;
      } else if (key == 'C') {
        screenState = SCR_CLUES;
      } else if (currentRoom < 2 && key == 'A') {
        solveClueRoom(currentPhase, currentRoom);
      } else if (currentRoom == 2 && key == 'A') {
        waitingAnswer = true;
        messageId = 6;
      }
      break;

    case SCR_END:
      if (key == 'A') resetGame();
      break;
  }

  renderCurrentScreen();
}

Glavni dijelovi programa su:

  • globalne varijable za stanje igre, fazu, sobu i tragove,
  • funkcije za crtanje ekrana,
  • pomoćne funkcije za provjeru clue-ova i statusa faze,
  • logika unosa s tipkovnice,
  • glavna petlja loop() koja reagira na pritisnute tipke.
Avantura

Dokumentacija funkcija

isSolved(uint8_t phase, uint8_t room)

  • Provjerava je li određena soba već riješena u zadanoj fazi. Prima broj faze i broj sobe, a vraća true ili false.

markSolved(uint8_t phase, uint8_t room)

  • Označava sobu kao riješenu tako da postavlja odgovarajući bit u solvedMask. Poziva se kada je igrač uspješno uzeo trag ili riješio završnu sobu.

hasClue(uint8_t phase, uint8_t clueIndex)

  • Provjerava je li igrač već prikupio određeni clue u zadanoj fazi. Koristi se pri prikazu clue ekrana i pri provjeri može li se rješavati završna soba.

addClue(uint8_t phase, uint8_t clueIndex)

  • Dodaje clue u masku tragova za zadanu fazu. Poziva se kad igrač uspješno aktivira clue sobu ili završi finalnu sobu.

cluesReady(uint8_t phase)

  • Provjerava jesu li prikupljena oba potrebna clue-a u fazi. Koristi se kako bi se spriječilo prerano ili nasumično rješavanje završne sobe.

phaseSolved(uint8_t phase)

  • Provjerava je li cijela faza završena, odnosno jesu li clue-ovi i finalna soba riješeni. Koristi se pri pokušaju otvaranja vrata.

resetGame()

  • Vraća cijelu igru u početno stanje: resetira faze, sobe, maske tragova i zaslon. Poziva se na početku programa i pri ponovnom pokretanju igre.

drawFarmer(int x, int y)

  • Crta jednostavan sprite glavnog lika na zadanoj poziciji. Parametri su x i y koordinata gornjeg lijevog kuta crteža.

drawKnight(int x, int y)
-‘Crta jednostavnog viteza na zadanoj poziciji. Koristi se na ekranima soba i naslovu igre.

drawGhost(int x, int y)

  • Crta duha kao mali tematski grafički element. Parametri određuju položaj na OLED zaslonu.

drawHeader(const char* title)

  • Crta zaglavlje na vrhu zaslona i ispisuje naslov ekrana. Prima C-string s nazivom zaslona i koristi se u gotovo svim render funkcijama.

drawMessageById(uint8_t id)

  • Prikazuje jednu od unaprijed definiranih poruka, primjerice našao si trag ili vrata su zaključana. Parametar id određuje koja će poruka biti nacrtana.

drawClueLine(uint8_t phase, uint8_t clueIndex, uint8_t y)

  • Na clue ekranu ispisuje konkretan trag ili ??? ako još nije pronađen. Prima broj faze, indeks traga i vertikalnu poziciju za ispis.

renderTitle()

  • Crta početni ekran igre s naslovom i osnovnim opcijama za start i pomoć. Poziva se kada je stanje SCR_TITLE.

renderHelp()

  • Crta ekran s kontrolama tipkovnice. Koristi se kad korisnik izabere pomoć.

renderMap()

  • Crta mapu faze i tri sobe kao jednostavne pravokutnike. Također pokazuje koja je soba trenutačno odabrana i koje su već riješene.

renderClues()

  • Prikazuje ekran sa skupljenim tragovima za trenutačnu fazu. Koristi drawClueLine() da pokaže pronađene i neotkrivene clue-ove.

renderRoom()

  • Crta sadržaj trenutačne sobe: opis, trag ili završnu zagonetku, ovisno o fazi i broju sobe. Ovdje je font namjerno smanjen na u8g2_font_5x8_tf kako bi tekst stao pregledno na mali OLED.

renderEnd()

  • Crta završni ekran nakon uspješnog izlaska iz katakombi. Prikazuje kraj igre i mogućnost novog početka.

renderCurrentScreen()

  • Ovo je centralna funkcija za prikaz: unutar U8g2 page-buffer petlje crta odgovarajući ekran prema trenutačnom stanju igre. Po potrebi dodaje i overlay poruku preko osnovnog ekrana, što je dobar način za OLED prikaz uz manju potrošnju memorije nego full-buffer pristup.

handleMovement(char key)

  • Mijenja odabranu sobu na mapi kada igrač pritisne 4 ili 6. Parametar je znak tipke s tipkovnice.

solveClueRoom(uint8_t phase, uint8_t room)

  • Rješava clue sobu: označi sobu riješenom, dodaje odgovarajući trag i pokaže poruku o uspjehu. Koristi se za prve dvije sobe svake faze.

solveFinalRoom(char key)

  • Provjerava je li završni odgovor točan. Ako clue-ovi još nisu prikupljeni, prikazuje poruku da je prerano; ako jesu, provjerava tipku i po potrebi završava fazu.

tryDoor()

  • Pokušava otvoriti vrata prema sljedećoj fazi. Ako faza nije završena, prikazuje poruku o zaključanim vratima; ako jest, otvara sljedeću fazu ili završava igru.

setup()

  • Arduino inicijalizacija: pokreće zaslon, postavlja osnovne opcije biblioteke i resetira igru. Izvodi se jednom pri uključivanju ili resetiranju.

loop()

  • Glavna petlja programa. Čita tipku s keypad-a, upravlja porukama i prebacuje stanja igre prema pravilima sučelja i logike.

Dodatna pitanja i zadaci:

  • Koje upozorenje je gornji kôd prijavio prilikom prijenosa na Arduino?
  • Razmisli kako bi mogao povećati radnu memoriju Arduina, makar i prividno?
  • Kako optimizirati kôd da se troši manje dinamičke memorije?

Optimizacija memorije

Dio našeg programa čine tekstualne konstante, tablice i nizovi. Oni zauzimaju dosta mjesta u dinamičkoj memoriji. Takve podatke možemo pohraniti u programsku memoriju, koja je puno veća, te ih pozivati i učitavati u dinamičku memoriju po potrebi.

Za tako nešto koristimo ove glavne programske metode:

  • PROGMEM za tablice, nizove, strukture i tekstove u Flashu.
  • F() makro za string literale koji se ispisuju direktno, bez kopiranja u SRAM kao obični literal.
  • pgm_read_byte, pgm_read_word, pgm_read_ptr i slične funkcije za dohvat pojedinačnih vrijednosti iz Flasha.
  • strcpy_P, strlen_P, memcpy_P i slične funkcije kad želiš nešto iz Flasha kopirati u običan RAM buffer i dalje obrađivati.

Kako to izgleda u praksi?

Za brojeve i tablice koristiš PROGMEM, pa po indeksu učitavaš samo ono što ti treba. To je posebno dobro za lookup tablice, nivoe igre, mape, sprite podatke i stalne konfiguracije.

// C++
#include <avr/pgmspace.h>

const uint8_t roomCosts[] PROGMEM = {3, 7, 12, 5};

void setup() {
  Serial.begin(9600);

  uint8_t value = pgm_read_byte(&roomCosts[2]);
  Serial.println(value);   // 12
}

void loop() {}

Ovdje niz ostaje u Flashu, a u SRAM ulazi samo jedna pročitana vrijednost value.

Za kratke tekstualne poruke koje samo ispisuješ najbolje je koristiti F() makro, jer tako tekst ostaje u Flashu i ne troši dragocjeni SRAM. To je jedna od najkorisnijih metoda optimizacije na Unu.
cpp Serial.println(F(“Pokretanje igre…”)); To je bolje od:

// C++
Serial.println("Pokretanje igre...");

jer obični string literal češće završi kao SRAM opterećenje u AVR obrascima rada, dok F() eksplicitno zadržava tekst u Flashu za ispis.

Ako tekst ne želiš samo ispisati nego ga trebaš:

  • uspoređivati,
  • uređivati,
  • slati u OLED funkciju koja traži RAM buffer,
  • spajati s drugim podacima,

onda ga obično držiš u PROGMEM, a po potrebi kopiraš u mali lokalni buffer pomoću strcpy_P() ili memcpy_P().

// C++
#include <avr/pgmspace.h>

const char msg0[] PROGMEM = "Vrata su zakljucana";
const char msg1[] PROGMEM = "Nasao si trag";

const char* const messages[] PROGMEM = {msg0, msg1};

char buffer[24];

void printMessage(uint8_t index) {
  const char* ptr = (const char*)pgm_read_ptr(&messages[index]);
  strcpy_P(buffer, ptr);
  Serial.println(buffer);
}

Ovo je vrlo važan obrazac: tablica pokazivača je u Flashu, i sami stringovi su u Flashu, a u SRAM ulazi samo mali radni buffer.

Možeš spremati i cijele strukture u Flash, pa ih po potrebi čitati u lokalnu varijablu. To je vrlo korisno za opise soba, definicije neprijatelja, iteme i konfiguracije levela.

// C++
#include <avr/pgmspace.h>

struct RoomData {
  uint8_t id;
  uint8_t correctAnswer;
  uint8_t nextRoom;
};

const RoomData rooms[] PROGMEM = {
  {0, 2, 1},
  {1, 1, 2},
  {2, 3, 0}
};

void loadRoom(uint8_t i, RoomData &out) {
  memcpy_P(&out, &rooms[i], sizeof(RoomData));
}

Ovo je često robusnije od čitanja svakog polja zasebno, pogotovo ako radiš data-driven igru.

Najveće usko grlo u ovakvom refaktoru nije brzina nego kompleksnost rada s PROGMEM pokazivačima, posebno kad imaš tablice stringova i više faza. Zato je robustnija arhitektura: sve statične poruke, nazive faza i tekstove soba držati u Flashu, a imati jednu pomoćnu funkciju koja iz Flasha učita jedan red u mali RAM buffer i odmah ga nacrta.

Što ova verzija radi?

  • sprema sve tekstove igre u Flash,
  • koristi mali lineBuffer[] za crtanje na OLED,
  • zadržava istu logiku 3 faze,
  • i znatno smanjuje SRAM pritisak u odnosu na normalne string konstante.

Optimizirani kôd:

// C++
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <Keypad.h>
#include <avr/pgmspace.h>

// ============================================
// OLED - page buffer
// ============================================
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// ============================================
// KEYPAD
// ============================================
const byte ROWS = 4;
const byte COLS = 4;

char keys[ROWS][COLS] = {
  {'1','2','3','A'},
  {'4','5','6','B'},
  {'7','8','9','C'},
  {'*','0','#','D'}
};

byte rowPins[ROWS] = {9, 8, 7, 6};
byte colPins[COLS] = {5, 4, 3, 2};

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

// ============================================
// GAME STATE
// ============================================
enum ScreenState : uint8_t {
  SCR_TITLE,
  SCR_HELP,
  SCR_MAP,
  SCR_ROOM,
  SCR_CLUES,
  SCR_END
};

ScreenState screenState = SCR_TITLE;

uint8_t currentPhase = 0;   // 0..2
uint8_t currentRoom  = 0;   // 0..2
uint8_t solvedMask[3] = {0, 0, 0};
uint8_t clueMask[3]   = {0, 0, 0};

bool waitingAnswer = false;
uint8_t messageId = 0;

// Mali radni buffer za ispis linija s OLED-a
char lineBuffer[26];

// ============================================
// PROGMEM STRINGS
// ============================================

// Headers / static UI
const char s_title[]      PROGMEM = "DUNGEON ESCAPE";
const char s_help[]       PROGMEM = "KONTROLE";
const char s_map[]        PROGMEM = "MAPA";
const char s_room[]       PROGMEM = "SOBA";
const char s_clues[]      PROGMEM = "CLUES";
const char s_end[]        PROGMEM = "SLOBODA";

const char s_phase1[]     PROGMEM = "Faza 1";
const char s_phase2[]     PROGMEM = "Faza 2";
const char s_phase3[]     PROGMEM = "Faza 3";

const char s_start[]      PROGMEM = "A start";
const char s_help_btn[]   PROGMEM = "D help";
const char s_ctrl1[]      PROGMEM = "4/6 kretanje";
const char s_ctrl2[]      PROGMEM = "A akcija";
const char s_ctrl3[]      PROGMEM = "B mapa";
const char s_ctrl4[]      PROGMEM = "C clues D nazad";
const char s_map_help[]   PROGMEM = "A soba C clues D vrata";
const char s_back[]       PROGMEM = "D nazad";
const char s_done[]       PROGMEM = "Rijeseno. D nazad";
const char s_room_clue[]  PROGMEM = "A uzmi trag B mapa";
const char s_room_solve[] PROGMEM = "A rijesi C clues";
const char s_end1[]       PROGMEM = "Marko je";
const char s_end2[]       PROGMEM = "izasao iz";
const char s_end3[]       PROGMEM = "katakombi!";
const char s_restart[]    PROGMEM = "A nova igra";

// Title text
const char s_marko1[]      PROGMEM = "Seljak Marko";
const char s_marko2[]      PROGMEM = "trazi izlaz.";

// Message texts
const char m1_1[] PROGMEM = "Nasao si trag.";
const char m1_2[] PROGMEM = "Tipka C = clues";
const char m1_3[] PROGMEM = "D dalje";

const char m2_1[] PROGMEM = "Netocno.";
const char m2_2[] PROGMEM = "Povezi tragove.";
const char m2_3[] PROGMEM = "D dalje";

const char m3_1[] PROGMEM = "Vrata su";
const char m3_2[] PROGMEM = "zakljucana.";
const char m3_3[] PROGMEM = "Rijesi fazu.";
const char m3_4[] PROGMEM = "D dalje";

const char m4_1[] PROGMEM = "Vitez te pusta";
const char m4_2[] PROGMEM = "dalje.";
const char m4_3[] PROGMEM = "D dalje";

const char m5_1[] PROGMEM = "Soba je vec";
const char m5_2[] PROGMEM = "rijesena.";
const char m5_3[] PROGMEM = "D dalje";

const char m6_1[] PROGMEM = "Unesi odgovor";
const char m6_2[] PROGMEM = "1 / 2 / 3";
const char m6_3[] PROGMEM = "D nazad";

const char m7_1[] PROGMEM = "Prvo nadji";
const char m7_2[] PROGMEM = "oba traga.";
const char m7_3[] PROGMEM = "Tipka C clues";
const char m7_4[] PROGMEM = "D dalje";

// Clues
const char clue_unknown[] PROGMEM = "- ???";

const char c10[] PROGMEM = "- Srednje je pravo.";
const char c11[] PROGMEM = "- Trazeni znak je SUNCE.";

const char c20[] PROGMEM = "- Sveti dar je kruh.";
const char c21[] PROGMEM = "- Vitez bira cvijet.";

const char c30[] PROGMEM = "- Drveni kljuc pase.";
const char c31[] PROGMEM = "- Cuvar trazi list.";

const char clue_done[] PROGMEM = "- Zavrsna soba rijesena";

// Room lines
const char r100[] PROGMEM = "Nisa";
const char r101[] PROGMEM = "Natpis kaze:";
const char r102[] PROGMEM = "pravo nije";
const char r103[] PROGMEM = "lijevo ni desno.";

const char r110[] PROGMEM = "Duh";
const char r111[] PROGMEM = "Sapuce:";
const char r112[] PROGMEM = "trazeni znak";
const char r113[] PROGMEM = "je sunce.";

const char r120[] PROGMEM = "Vrata viteza";
const char r121[] PROGMEM = "Koji znak?";
const char r122[] PROGMEM = "1=sunce";
const char r123[] PROGMEM = "2=oko 3=kruna";

const char r200[] PROGMEM = "Oltar";
const char r201[] PROGMEM = "Natpis kaze:";
const char r202[] PROGMEM = "sveti dar";
const char r203[] PROGMEM = "je kruh.";

const char r210[] PROGMEM = "Tamnica";
const char r211[] PROGMEM = "Duh kaze:";
const char r212[] PROGMEM = "vitez bira";
const char r213[] PROGMEM = "cvijet.";

const char r220[] PROGMEM = "Zavrsna brava";
const char r221[] PROGMEM = "Sto odabrati?";
const char r222[] PROGMEM = "1=kruh";
const char r223[] PROGMEM = "2=cvijet 3=mac";

const char r300[] PROGMEM = "Skrinja";
const char r301[] PROGMEM = "Natpis kaze:";
const char r302[] PROGMEM = "drveni kljuc";
const char r303[] PROGMEM = "otvara put.";

const char r310[] PROGMEM = "Hodnik";
const char r311[] PROGMEM = "Cuvar kaze:";
const char r312[] PROGMEM = "na vratima je";
const char r313[] PROGMEM = "urezan list.";

const char r320[] PROGMEM = "Posljednja brava";
const char r321[] PROGMEM = "Koji simbol?";
const char r322[] PROGMEM = "1=list";
const char r323[] PROGMEM = "2=kljuc 3=mac";

// ============================================
// HELPERS FOR PROGMEM TEXT
// ============================================
void drawTextP(uint8_t x, uint8_t y, const char* p) {
  strcpy_P(lineBuffer, p);
  u8g2.drawStr(x, y, lineBuffer);
}

void drawPhaseName(uint8_t phase, uint8_t x, uint8_t y) {
  if (phase == 0) drawTextP(x, y, s_phase1);
  else if (phase == 1) drawTextP(x, y, s_phase2);
  else drawTextP(x, y, s_phase3);
}

// ============================================
// HELPERS
// ============================================
bool isSolved(uint8_t phase, uint8_t room) {
  return solvedMask[phase] & (1 << room);
}

void markSolved(uint8_t phase, uint8_t room) {
  solvedMask[phase] |= (1 << room);
}

bool hasClue(uint8_t phase, uint8_t clueIndex) {
  return clueMask[phase] & (1 << clueIndex);
}

void addClue(uint8_t phase, uint8_t clueIndex) {
  clueMask[phase] |= (1 << clueIndex);
}

bool cluesReady(uint8_t phase) {
  return (clueMask[phase] & 0b00000011) == 0b00000011;
}

bool phaseSolved(uint8_t phase) {
  return (clueMask[phase] & 0b00000111) == 0b00000111;
}

void resetGame() {
  currentPhase = 0;
  currentRoom = 0;
  solvedMask[0] = solvedMask[1] = solvedMask[2] = 0;
  clueMask[0] = clueMask[1] = clueMask[2] = 0;
  waitingAnswer = false;
  messageId = 0;
  screenState = SCR_TITLE;
}

// ============================================
// SPRITES
// ============================================
void drawFarmer(int x, int y) {
  u8g2.drawCircle(x + 4, y + 3, 3);
  u8g2.drawLine(x + 4, y + 6, x + 4, y + 12);
  u8g2.drawLine(x + 4, y + 8, x + 1, y + 10);
  u8g2.drawLine(x + 4, y + 8, x + 7, y + 10);
  u8g2.drawLine(x + 4, y + 12, x + 2, y + 15);
  u8g2.drawLine(x + 4, y + 12, x + 6, y + 15);
}

void drawKnight(int x, int y) {
  u8g2.drawFrame(x + 2, y + 1, 6, 6);
  u8g2.drawLine(x + 5, y + 7, x + 5, y + 14);
  u8g2.drawLine(x + 5, y + 9, x + 1, y + 11);
  u8g2.drawLine(x + 5, y + 9, x + 9, y + 11);
  u8g2.drawLine(x + 5, y + 14, x + 3, y + 16);
  u8g2.drawLine(x + 5, y + 14, x + 7, y + 16);
}

void drawGhost(int x, int y) {
  u8g2.drawCircle(x + 4, y + 4, 4);
  u8g2.drawLine(x + 0, y + 8, x + 0, y + 14);
  u8g2.drawLine(x + 8, y + 8, x + 8, y + 14);
  u8g2.drawLine(x + 0, y + 14, x + 2, y + 12);
  u8g2.drawLine(x + 2, y + 12, x + 4, y + 14);
  u8g2.drawLine(x + 4, y + 14, x + 6, y + 12);
  u8g2.drawLine(x + 6, y + 12, x + 8, y + 14);
}

// ============================================
// UI
// ============================================
void drawHeader(const char* titleP) {
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.setFontPosTop();
  u8g2.drawBox(0, 0, 128, 10);
  u8g2.setDrawColor(0);
  drawTextP(2, 1, titleP);
  u8g2.setDrawColor(1);
}

void drawMessageById(uint8_t id) {
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.setFontPosTop();

  switch (id) {
    case 1:
      drawTextP(0, 16, m1_1);
      drawTextP(0, 28, m1_2);
      drawTextP(0, 54, m1_3);
      break;
    case 2:
      drawTextP(0, 16, m2_1);
      drawTextP(0, 28, m2_2);
      drawTextP(0, 54, m2_3);
      break;
    case 3:
      drawTextP(0, 16, m3_1);
      drawTextP(0, 28, m3_2);
      drawTextP(0, 40, m3_3);
      drawTextP(0, 54, m3_4);
      break;
    case 4:
      drawTextP(0, 16, m4_1);
      drawTextP(0, 28, m4_2);
      drawTextP(0, 54, m4_3);
      break;
    case 5:
      drawTextP(0, 16, m5_1);
      drawTextP(0, 28, m5_2);
      drawTextP(0, 54, m5_3);
      break;
    case 6:
      drawTextP(0, 16, m6_1);
      drawTextP(0, 28, m6_2);
      drawTextP(0, 54, m6_3);
      break;
    case 7:
      drawTextP(0, 16, m7_1);
      drawTextP(0, 28, m7_2);
      drawTextP(0, 40, m7_3);
      drawTextP(0, 54, m7_4);
      break;
  }
}

void drawClueLine(uint8_t phase, uint8_t clueIndex, uint8_t y) {
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.setFontPosTop();

  if (!hasClue(phase, clueIndex)) {
    drawTextP(0, y, clue_unknown);
    return;
  }

  if (phase == 0) {
    if (clueIndex == 0) drawTextP(0, y, c10);
    else drawTextP(0, y, c11);
  } else if (phase == 1) {
    if (clueIndex == 0) drawTextP(0, y, c20);
    else drawTextP(0, y, c21);
  } else {
    if (clueIndex == 0) drawTextP(0, y, c30);
    else drawTextP(0, y, c31);
  }
}

// ============================================
// RENDER
// ============================================
void renderTitle() {
  drawHeader(s_title);
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.setFontPosTop();

  drawFarmer(8, 18);
  drawKnight(102, 18);

  drawTextP(22, 16, s_marko1);
  drawTextP(22, 28, s_marko2);
  drawTextP(6, 48, s_start);
  drawTextP(72, 48, s_help_btn);
}

void renderHelp() {
  drawHeader(s_help);
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.setFontPosTop();

  drawTextP(0, 14, s_ctrl1);
  drawTextP(0, 26, s_ctrl2);
  drawTextP(0, 38, s_ctrl3);
  drawTextP(0, 50, s_ctrl4);
}

void renderMap() {
  drawHeader(s_map);
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.setFontPosTop();

  int rx[3] = {8, 48, 88};
  int ry[3] = {24, 24, 24};

  for (uint8_t i = 0; i < 3; i++) {
    u8g2.drawFrame(rx[i], ry[i], 28, 18);

    if (i == currentRoom) {
      u8g2.drawBox(rx[i] + 1, ry[i] + 1, 26, 16);
      u8g2.setDrawColor(0);
      char n[2] = {char('1' + i), 0};
      u8g2.drawStr(rx[i] + 11, ry[i] + 5, n);
      u8g2.setDrawColor(1);
    } else {
      char n[2] = {char('1' + i), 0};
      u8g2.drawStr(rx[i] + 11, ry[i] + 5, n);
    }

    if (isSolved(currentPhase, i)) {
      u8g2.drawDisc(rx[i] + 24, ry[i] + 4, 2);
    }
  }

  drawPhaseName(currentPhase, 0, 12);
  drawTextP(0, 56, s_map_help);
}

void renderClues() {
  drawHeader(s_clues);
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.setFontPosTop();

  drawPhaseName(currentPhase, 0, 12);
  drawClueLine(currentPhase, 0, 24);
  drawClueLine(currentPhase, 1, 36);

  if (phaseSolved(currentPhase)) drawTextP(0, 50, clue_done);
  else drawTextP(0, 54, s_back);
}

void renderRoom() {
  drawHeader(s_room);
  u8g2.setFont(u8g2_font_5x8_tf);
  u8g2.setFontPosTop();

  if (currentPhase == 0 && currentRoom == 0) {
    drawTextP(0, 14, r100); drawTextP(0, 26, r101);
    drawTextP(0, 36, r102); drawTextP(0, 46, r103);
    drawKnight(106, 34);
  } else if (currentPhase == 0 && currentRoom == 1) {
    drawTextP(0, 14, r110); drawTextP(0, 26, r111);
    drawTextP(0, 36, r112); drawTextP(0, 46, r113);
    drawGhost(108, 36);
  } else if (currentPhase == 0 && currentRoom == 2) {
    drawTextP(0, 14, r120); drawTextP(0, 26, r121);
    drawTextP(0, 36, r122); drawTextP(0, 46, r123);
    drawKnight(106, 34);
  } else if (currentPhase == 1 && currentRoom == 0) {
    drawTextP(0, 14, r200); drawTextP(0, 26, r201);
    drawTextP(0, 36, r202); drawTextP(0, 46, r203);
    drawKnight(106, 34);
  } else if (currentPhase == 1 && currentRoom == 1) {
    drawTextP(0, 14, r210); drawTextP(0, 26, r211);
    drawTextP(0, 36, r212); drawTextP(0, 46, r213);
    drawGhost(108, 36);
  } else if (currentPhase == 1 && currentRoom == 2) {
    drawTextP(0, 14, r220); drawTextP(0, 26, r221);
    drawTextP(0, 36, r222); drawTextP(0, 46, r223);
    drawKnight(106, 34);
  } else if (currentPhase == 2 && currentRoom == 0) {
    drawTextP(0, 14, r300); drawTextP(0, 26, r301);
    drawTextP(0, 36, r302); drawTextP(0, 46, r303);
    drawKnight(106, 34);
  } else if (currentPhase == 2 && currentRoom == 1) {
    drawTextP(0, 14, r310); drawTextP(0, 26, r311);
    drawTextP(0, 36, r312); drawTextP(0, 46, r313);
    drawGhost(108, 36);
  } else if (currentPhase == 2 && currentRoom == 2) {
    drawTextP(0, 14, r320); drawTextP(0, 26, r321);
    drawTextP(0, 36, r322); drawTextP(0, 46, r323);
    drawKnight(106, 34);
  }

  if (isSolved(currentPhase, currentRoom)) drawTextP(0, 56, s_done);
  else if (!waitingAnswer) {
    if (currentRoom < 2) drawTextP(0, 56, s_room_clue);
    else drawTextP(0, 56, s_room_solve);
  }
}

void renderEnd() {
  drawHeader(s_end);
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.setFontPosTop();

  drawFarmer(10, 20);
  u8g2.drawLine(28, 16, 28, 48);
  u8g2.drawLine(28, 16, 40, 22);
  u8g2.drawLine(28, 48, 40, 42);

  drawTextP(46, 16, s_end1);
  drawTextP(46, 28, s_end2);
  drawTextP(46, 40, s_end3);
  drawTextP(0, 54, s_restart);
}

void renderCurrentScreen() {
  u8g2.firstPage();
  do {
    switch (screenState) {
      case SCR_TITLE: renderTitle(); break;
      case SCR_HELP:  renderHelp(); break;
      case SCR_MAP:   renderMap(); break;
      case SCR_ROOM:  renderRoom(); break;
      case SCR_CLUES: renderClues(); break;
      case SCR_END:   renderEnd(); break;
    }

    if (messageId != 0 && screenState != SCR_END) {
      u8g2.drawBox(0, 12, 128, 52);
      u8g2.setDrawColor(0);
      drawMessageById(messageId);
      u8g2.setDrawColor(1);
    }
  } while (u8g2.nextPage());
}

// ============================================
// LOGIC
// ============================================
void handleMovement(char key) {
  if (key == '4' && currentRoom > 0) currentRoom--;
  if (key == '6' && currentRoom < 2) currentRoom++;
}

void solveClueRoom(uint8_t phase, uint8_t room) {
  if (isSolved(phase, room)) {
    messageId = 5;
    return;
  }

  markSolved(phase, room);
  addClue(phase, room);
  messageId = 1;
}

void solveFinalRoom(char key) {
  if (isSolved(currentPhase, 2)) {
    waitingAnswer = false;
    messageId = 5;
    return;
  }

  if (!cluesReady(currentPhase)) {
    waitingAnswer = false;
    messageId = 7;
    return;
  }

  char expected = '1';
  if (currentPhase == 0) expected = '1'; // sunce
  if (currentPhase == 1) expected = '2'; // cvijet
  if (currentPhase == 2) expected = '1'; // list

  if (key == expected) {
    markSolved(currentPhase, 2);
    addClue(currentPhase, 2);
    messageId = 1;
  } else {
    messageId = 2;
  }

  waitingAnswer = false;
}

void tryDoor() {
  if (!phaseSolved(currentPhase)) {
    messageId = 3;
    return;
  }

  if (currentPhase < 2) {
    currentPhase++;
    currentRoom = 0;
    waitingAnswer = false;
    messageId = 4;
  } else {
    screenState = SCR_END;
  }
}

// ============================================
// SETUP / LOOP
// ============================================
void setup() {
  u8g2.begin();
  u8g2.setFontMode(1);
  u8g2.setBitmapMode(1);
  u8g2.setFontDirection(0);
  resetGame();
  renderCurrentScreen();
}

void loop() {
  char key = keypad.getKey();
  if (!key) return;

  if (messageId != 0) {
    if (messageId == 6) {
      if (key == 'D') {
        waitingAnswer = false;
        messageId = 0;
      } else if (key == '1' || key == '2' || key == '3') {
        solveFinalRoom(key);
      }
    } else {
      if (key == 'D') {
        messageId = 0;
      }
    }
    renderCurrentScreen();
    return;
  }

  switch (screenState) {
    case SCR_TITLE:
      if (key == 'A') screenState = SCR_MAP;
      else if (key == 'D') screenState = SCR_HELP;
      break;

    case SCR_HELP:
      if (key == 'D') screenState = SCR_TITLE;
      break;

    case SCR_MAP:
      if (key == '4' || key == '6') {
        handleMovement(key);
      } else if (key == 'A') {
        screenState = SCR_ROOM;
      } else if (key == 'C') {
        screenState = SCR_CLUES;
      } else if (key == 'D') {
        tryDoor();
      }
      break;

    case SCR_CLUES:
      if (key == 'D' || key == 'B') {
        screenState = SCR_MAP;
      }
      break;

    case SCR_ROOM:
      if (key == 'B' || key == 'D') {
        waitingAnswer = false;
        screenState = SCR_MAP;
      } else if (key == 'C') {
        screenState = SCR_CLUES;
      } else if (currentRoom < 2 && key == 'A') {
        solveClueRoom(currentPhase, currentRoom);
      } else if (currentRoom == 2 && key == 'A') {
        waitingAnswer = true;
        messageId = 6;
      }
      break;

    case SCR_END:
      if (key == 'A') resetGame();
      break;
  }

  renderCurrentScreen();
}

Dodatna pitanja i zadaci:

  • Koliko ova verzija kôda koristi memorije u odnosu na prethodnu verziju?

ligtbulb Što smo naučili?

  • I vrlo mali mikrokontroler može pokretati smislenu igru ako dobro ograničimo opseg projekta i pažljivo biramo arhitekturu. Vidjeli smo kako se priča pretvara u state machine, kako se OLED zaslon koristi za više različitih ekrana i zašto su memorijska ograničenja na Arduino Uno stvarna projektna granica, a ne samo tehnički detalj.
  • Razliku između ideje koja dobro zvuči i implementacije koja stvarno radi na hardveru: tekstualna avantura mora biti kratka, jasna i logički strukturirana da bi bila stabilna i igriva na Uno platformi. Najvažnija pouka je da su u embedded igrama jednostavnost, čitljivost i dobra organizacija koda često važniji od više sadržaja.
  • Kako optimizirati memoriju u samom programu, tako da ne opterećujemo mikrokontroler bez potrebe.
◀ 3.3. Programiranje igara4. Logički sklopovi ▶