BI-ZUM Základy umělé inteligence
Jdi na navigaci předmětu

Tutorial "Hraduch v bludišti - terminál"

Autor: Mgr. Ing. Ladislava Smítková Janků, Ph.D.

V tomto tutorialu budeme implementovat zobrazení pohybu hradního ducha (zkráceně hraducha :-)) v bludišti. Je zde popsán postup, jak implementovat textové zobrazení bludiště v terminálu včetně zobrazení pohybujícího se agenta. Pohyb agenta bude náhodný.

bludiste c

Zadání

Načtěte bludiště ze souboru a vykreslete ho v terminálu. Do bludiště umístěte agenta a implementujte jeho náhodný pohyb po bludišti.

Prerekvizity

  • funkční překladač gcc

Formát bludiště a souřadnicový systém (= jak je bludiště zadáno v souboru a jak uvádíme souřadnice x,y)

  • Soubor s bludištěm je tvořen řádky obsahujícmi znaky: X (= stěna), 'mezera'(= volné pole, na které může vstoupit agent), S (= Start, agent může vstoupit na S), E (= End, agent může vstoupit na E).
  • Řádky v souboru jsou zakončeny znakem konce řádky, typicky \n. Předokládáme bludiště ve tvaru obdélníku, takže všechny řádky musí být stejně dlouhé.
  • Počátek souřadného systému je vlevo nahoře, kde je bod se souřadnicemi 0,0.
  • Osa x směřuje zleva doprava.
  • Osa y směřuje shora dolů.
  • Rozměr bludiště v ose y tedy odpovídá POČTU ŘÁDKů v souboru.
  • Rozměr bludiště v ose x odpovídá DÉLCE (každého) ŘÁDKU v souboru.

Příklad textového souboru s bludištěm a odpovídající vykreslený grafický výstup:

bludiste c make

Architektura programu

V programu musíme:

  • vyřešit načtení bludiště ze souboru do vhodné datové struktury,
  • vyřešit zobrazení bludiště z datové struktury do terminálu,
  • vyřešit vnitřní reprezentaci agenta a jeho zobrazení v terminálu,
  • vyřešit opakované zobrazení bludiště v okně terminálu.

Vnitřní datová reprezentace bludiště

Bludiště může být vnitřně uchováváno různými způsoby. V tomto příkladu budeme bludiště uchovávat ve formě dvourozměrného pole znaků, přesněji jako pole, kde každým prvkem pole je string (řetězec).

Bludiště je uchováváno v proměnné bludisteStr:

char* (*bludisteStr)[];

Jedná se o ukazatel na pole ukazatelů na řetězec znaků. Tato struktura nám umožní načíst libovolně velké bludiště, budeme limitováni pouze pamětí počítače.

Dále budeme potřebovat proměnné, ve kterých budou uchovávány rozměry bludiště, souřadnice startu, souřadnice cíle a souřadnice agenta.

int X, Y;               // rozměry bludiště
int Sx, Sy;             // souřadnice startu
int Ex, Ey;             // souřadnice cíle
int agentX, agentY;     // souřadnice agenta

Načtení bludiště ze souboru

Nejprve si stanovíme, co všechno musíme při načítání bludiště provést:

  • načtení bludiště ze souboru do proměnné bludisteStr
  • uložení rozměrů bludiště do proměnných X, Y
  • kontrolu, že jsou všechny řádky stejně dlouhé a obsahují pouze povolené znaky
  • kontrolu, že v bludišti je pouze jeden Start a pouze jeden cíl (End)
  • uložení souřadnic Startu do proměnných Sx, Sy
  • uložení souřadnic cíle (End) do proměnných Ex, Ey

Načtení souboru s bludištěm budeme implementovat v proceduře bludisteLoad:

void bludisteLoad( char *fileName)

Parametr fileName obsahuje název souboru s bludištěm.

Teď si postupně projdeme jednotlivé body.

Načtení bludiště ze souboru do proměnné bludisteStr

Protože neznáme velikost bludiště v ose y (tedy počet řádek souboru s bludištěm), budeme muset přečíst soubor dvakrát. Při prvním čtení pouze spočítáme počet řádek. Tím získáme velikost bludiště v ose Y. Poté můžeme alokovat paměť pro uložení jednotlivých řádek souboru do pole bludisteStr.

    FILE *f;
    char line[1024];
    int i;

    // spocitam pocet radek v souboru
    Y = 0;
    if ((f = fopen( fileName, "r")) == NULL)
        printError("Soubor s bludištěm nelze otevřít.");
    while (fgets( line, sizeof( line), f) != NULL) Y++;
    fclose(f);

    // je v souboru neco ?
    if (Y == 0) printError("Soubor s bludištěm je prázdný.");

    // alokuji pamet a pak to doopravdy nactu
    bludisteStr = (char *(*)[]) malloc( Y * sizeof(char*));

Poznámka: Všimněte si, že načtený řádek může být pouze 1022 znaků dlouhý (1024 mínus znak 0 na konci řetězce, mínus znak \n ukončující řádek). Můžete se zamyslet, jak by šel kód upravit, aby mohlo být bludiště skutečně jakkoliv široké.

Nyní máme alokovanou paměť pro pole řádků a můžeme přejít k samotnému načtení bludiště. Znovu otevřeme soubor a přečteme ho.

    if ((f = fopen( fileName, "r")) == NULL)
        printError("Soubor s bludištěm nelze otevřít.");
    for (i=0; i<Y; i++) {
        // nacteni radky
        if (fgets( line, sizeof( line), f) == NULL)
            printError("Chyba při čtení bludiště ze souboru.");

        // ochrana proti preteceni a vymazani konce radku
        line[ sizeof( line)-1] = 0;
        int delka = strlen( line);
        if (line[ delka-1] == '\n') line[ delka-1] = 0;

        // ulozeni radky do pole radku
        (*bludisteStr)[i] = strdup( line);
    }

Funkce strdup(s) vytvoří duplikát řetězce s v dynamicky alokované paměti. V načtených řádcích vymažeme znak konce řádky \n.

Uložení rozměrů bludiště do proměnných X, Y

Velikost bludiště v ose y již máme uloženou v proměnné Y (hodnota Y se rovná počtu řádků v souboru s bludištěm). Velikost v ose x zjistíme z prvního řádku (jedná se o délku řetězce).

    X = strlen((*bludisteStr)[0]);
    if (X == 0) printError("Bludiště je prázdné.");
Kontrola, že jsou všechny řádky stejně dlouhé a obsahují pouze povolené znaky

Projdeme načtené řádky a zkontrolujeme, že jsou stejně dlouhé, a to přesně X znaků. Dále provedeme kontrolu, zda se na řádku nevyskytuje nepovolený znak.

    for (i=0; i<Y; i++) {
        char *s = (*bludisteStr)[i];
        if (strlen(s) != (size_t) X)
            printError("Všechny řádky bludiště musí být stejně dlouhé.");
        int j;
        for (j=0; j<X; j++) {
            char c = *(s++);
            if ((c != 'X') && (c != ' ') && (c != 'S') && (c != 'E'))
                printError("V bludišti se vyskytuje neznámý znak.");
        }
    }
Kontrola, že v bludišti je pouze jeden Start a pouze jeden cíl (End) a uložení jejich souřadnic

Na začátku nastavíme Sx a Ex na hodnotu -1. Tato hodnota bude označovat stav, že jsme doposud nenačetli souřadnice startu (Sx), resp. cíle (Ex). Projdeme všechny řádky bludiště a jakmile narazíme na znak S, zkontrolujeme, že souřadnice startu stále nemáme (Sx je stále -1) a tyto souřadnice načteme. Podobně postupujeme v případě souřadnic cíle.

    Sx = -1;
    Ex = -1;
    for (i=0; i<Y; i++) {
        char *s = (*bludisteStr)[i];
        int j;
        for (j=0; j<X; j++) {
            if (*s == 'S') {
                if (Sx != -1) printError("V bludišti je více míst označených jako start.");
                Sx = j;
                Sy = i;
            }
            if (*s == 'E') {
                if (Ex != -1) printError("V bludišti je více míst označených jako konec.");
                Ex = j;
                Ey = i;
            }
            s++;
        }
    }

Zobrazení bludiště v terminálu

Zobrazení bludiště v terminálu je velmi snadné, stačí vypsat všechny řádky na terminál pomocí funkce printf():

int i;
for (i=0; i<Y; i++) {
    printf("%s\n", (*bludisteStr)[i]);
}

Nicméně tento způsob neřeší zobrazení agenta a hlavně opakované zobrazování v terminálovém okně. Toto vyřešíme později.

Zobrazení agenta v bludišti

Na začátku umístíme agenta na souřadnice startu:

agentX = Sx;
agentY = Sy;

Abychom mohli vykreslit v bludišti agenta, budeme muset upravit for-cyklus pro zobrazení bludiště. Můžeme například nejprve zkopírovat každý řádek do pomocné proměnné a v případě, že se jedná o řádek s agentem (i == agentY), označíme pozici agenta písmenem 'A' (znak v řetězci s na pozici agentX).

    int i;

    char *s = (char *) malloc(X+1);
    for (i=0; i<Y; i++) {
        strcpy( s, (*bludisteStr)[i]);
        if (i == agentY) s[ agentX] = 'A';
        printf("%s\n", s);
    }
    free(s);

Agent se bude automaticky náhodně pohybovat. Musíme tedy implementovat detekci zdi, aby nám agent neutekl kam nemá. K tomu bude sloužit funkce wall(x,y) vracející nenulovou hodnotu, pokud se na zadaných souřadnicích nachází zeď. Funkce také provede jednoduché kontroly na hodnotu souřadnic a v případě, že se jedná o chybné souřadnice mimo bludiště, vrátí hodnotu 1 (sdělí, že na těchto souřadnicích je zeď).

    int wall( int x, int y)
    {
        if ((x < 0) || (x >= X) || (y < 0) || (y >= Y)) return 0;
        return ((*bludisteStr)[y][x] == 'X');
    }

Samotný pohyb agenta implementujeme v těle main(). Ve smyčce navrhneme nové souřadnice agenta a poté provedeme kontrolu, zda není na zadaných souřadnicích zeď. Pokud ano, navrhneme nové souřadnice.

Pro generování nových souřadnic využijeme generátor náhodných čísel. Voláme funkci random(), jejíž návratovou hodnotu omezíme na hodnoty 0 až 3 (tedy modulo 4). Takto určíme jeden ze směrů, kterým se agent může vydat a podle toho vypočteme nové souřadnice:

    // vygeneruj nahodny smer pro posun agenta
    int newX, newY;
    do {
        switch (random() % 4) {
            case 0: {
                newX = agentX;
                newY = agentY + 1;
                break;
            }
            case 1: {
                newX = agentX;
                newY = agentY - 1;
                break;
            }
            case 2: {
                newX = agentX + 1;
                newY = agentY;
                break;
            }
            default: {
                newX = agentX - 1;
                newY = agentY;
                break;
            }
        }
    } while (wall( newX, newY));

    // posun agenta
    agentX = newX;
    agentY = newY;

Generátor náhodných čísel je vhodné na začátku inicializovat na nějakou pseudonáhodnou hodnotu. Můžeme využít např. id procesu v operačním systému (PID):

srandom( getpid());

Opakované zobrazení bludiště v okně terminálu

V terminálu se teoreticky nedá při tisku znaků vrátit zpět. V textové konzoli se však mohou využít zavedené terminálové zkratky, které např. vymažou okno terminálu. Ve skutečnosti se pouze zapíše další znak (nebo sekvence znaků), které musí být terminálovým programem (např. bash) interpretovány. Říká se jim escape sekvence. Pokud se tedy v proudu znaků zapsaných na terminál vyskytuje sekvence pro vymazání okna terminálu, terminálový program provede tento výmaz nezávisle na okamžitých rozměrech okna terminálu. Tento fakt je třeba pochopit. Terminál se nemaže stejně jako grafický výstup, kde se zapíší do grafické karty hodnoty barev jednotlivých pixelů. Do terminálu musíme zapsat sekvenci, která okno terminálu vymaže a umístí kurzor vlevo nahoru.

Tato magická sekvence je \033 [2J \033 [H .

Nyní již známe všechno potřebné. Zobrazení bludiště provedeme v proceduře print():

void print()
{
    int i;

    // vymaz konzolu pomoci escape sekvence
    printf("\033[2J\033[H");

    // zobraz bludiste a agenta
    char *s = (char *) malloc(X+1);
    for (i=0; i<Y; i++) {
        strcpy( s, (*bludisteStr)[i]);
        if (i == agentY) s[ agentX] = 'A';
        printf("%s\n", s);
    }
    free(s);

    // jeste posun kurzor, aby nikde nevadil
    printf("\n");
}

Výsledný projekt

Celý program je ke stažení zde:

bludiste-c.zip